multivendor-notification 0.0.1
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/index.cjs +467 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +450 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var app = require('firebase-admin/app');
|
|
4
|
+
var messaging = require('firebase-admin/messaging');
|
|
5
|
+
|
|
6
|
+
// src/client.ts
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var FcmConfigError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "FcmConfigError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var FcmPayloadError = class extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "FcmPayloadError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var FcmApiError = class extends Error {
|
|
22
|
+
constructor(message, options = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "FcmApiError";
|
|
25
|
+
this.statusCode = options.statusCode;
|
|
26
|
+
this.errorCode = options.errorCode;
|
|
27
|
+
this.attemptCount = options.attemptCount ?? 1;
|
|
28
|
+
this.details = options.details;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
var PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
|
|
34
|
+
var PEM_FOOTER = "-----END PRIVATE KEY-----";
|
|
35
|
+
function normalizePrivateKey(raw) {
|
|
36
|
+
const trimmed = raw.trim();
|
|
37
|
+
if (trimmed.includes("\\n")) {
|
|
38
|
+
return trimmed.replace(/\\n/g, "\n");
|
|
39
|
+
}
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
function isValidPemPrivateKey(key) {
|
|
43
|
+
const normalized = normalizePrivateKey(key);
|
|
44
|
+
return normalized.includes(PEM_HEADER) && normalized.includes(PEM_FOOTER);
|
|
45
|
+
}
|
|
46
|
+
function resolveConfig(config) {
|
|
47
|
+
const projectId = config.projectId?.trim() || "";
|
|
48
|
+
const clientEmail = config.clientEmail?.trim() || "";
|
|
49
|
+
const privateKeyRaw = config.privateKey?.trim() || "";
|
|
50
|
+
if (!projectId) {
|
|
51
|
+
throw new FcmConfigError("FCM projectId is required");
|
|
52
|
+
}
|
|
53
|
+
if (!clientEmail || !clientEmail.includes("@")) {
|
|
54
|
+
throw new FcmConfigError("FCM clientEmail must be a valid service account email");
|
|
55
|
+
}
|
|
56
|
+
if (!privateKeyRaw) {
|
|
57
|
+
throw new FcmConfigError("FCM privateKey is required");
|
|
58
|
+
}
|
|
59
|
+
const privateKey = normalizePrivateKey(privateKeyRaw);
|
|
60
|
+
if (!isValidPemPrivateKey(privateKey)) {
|
|
61
|
+
throw new FcmConfigError("FCM privateKey must be a valid PEM private key");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
projectId,
|
|
65
|
+
clientEmail,
|
|
66
|
+
privateKey,
|
|
67
|
+
retryBackoffMs: config.retryBackoffMs ?? 250,
|
|
68
|
+
maxPayloadBytes: config.maxPayloadBytes ?? 4096
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/validate.ts
|
|
73
|
+
var DEFAULT_TITLE_MAX = 200;
|
|
74
|
+
var DEFAULT_BODY_MAX = 1e3;
|
|
75
|
+
function estimatePayloadBytes(message) {
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
const notification = message.notification;
|
|
78
|
+
if (notification?.title) {
|
|
79
|
+
bytes += Buffer.byteLength(notification.title, "utf8");
|
|
80
|
+
}
|
|
81
|
+
if (notification?.body) {
|
|
82
|
+
bytes += Buffer.byteLength(notification.body, "utf8");
|
|
83
|
+
}
|
|
84
|
+
if (notification?.imageUrl) {
|
|
85
|
+
bytes += Buffer.byteLength(notification.imageUrl, "utf8");
|
|
86
|
+
}
|
|
87
|
+
if (message.data) {
|
|
88
|
+
for (const [key, value] of Object.entries(message.data)) {
|
|
89
|
+
bytes += Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (message.collapseKey) {
|
|
93
|
+
bytes += Buffer.byteLength(message.collapseKey, "utf8");
|
|
94
|
+
}
|
|
95
|
+
if (message.messageId) {
|
|
96
|
+
bytes += Buffer.byteLength(message.messageId, "utf8");
|
|
97
|
+
}
|
|
98
|
+
return bytes;
|
|
99
|
+
}
|
|
100
|
+
function validatePayload(message, options = {}) {
|
|
101
|
+
const maxPayloadBytes = options.maxPayloadBytes ?? 4096;
|
|
102
|
+
const titleMax = options.titleMax ?? DEFAULT_TITLE_MAX;
|
|
103
|
+
const bodyMax = options.bodyMax ?? DEFAULT_BODY_MAX;
|
|
104
|
+
const title = message.notification?.title;
|
|
105
|
+
const body = message.notification?.body;
|
|
106
|
+
if (title && title.length > titleMax) {
|
|
107
|
+
throw new FcmPayloadError(`Notification title exceeds ${titleMax} characters`);
|
|
108
|
+
}
|
|
109
|
+
if (body && body.length > bodyMax) {
|
|
110
|
+
throw new FcmPayloadError(`Notification body exceeds ${bodyMax} characters`);
|
|
111
|
+
}
|
|
112
|
+
const estimated = estimatePayloadBytes(message);
|
|
113
|
+
if (estimated > maxPayloadBytes) {
|
|
114
|
+
throw new FcmPayloadError(
|
|
115
|
+
`Push payload estimated at ${estimated} bytes exceeds limit of ${maxPayloadBytes} bytes`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/batch.ts
|
|
121
|
+
var FCM_MULTICAST_BATCH_SIZE = 500;
|
|
122
|
+
function dedupeTokens(tokens) {
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
const result = [];
|
|
125
|
+
for (const raw of tokens) {
|
|
126
|
+
const token = raw?.trim();
|
|
127
|
+
if (!token || seen.has(token)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
seen.add(token);
|
|
131
|
+
result.push(token);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
function chunkTokens(tokens, batchSize = FCM_MULTICAST_BATCH_SIZE) {
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for (let i = 0; i < tokens.length; i += batchSize) {
|
|
138
|
+
chunks.push(tokens.slice(i, i + batchSize));
|
|
139
|
+
}
|
|
140
|
+
return chunks;
|
|
141
|
+
}
|
|
142
|
+
function mergeMulticastResults(results) {
|
|
143
|
+
const merged = {
|
|
144
|
+
successCount: 0,
|
|
145
|
+
failureCount: 0,
|
|
146
|
+
invalidTokens: [],
|
|
147
|
+
transientFailures: [],
|
|
148
|
+
tokenFailures: []
|
|
149
|
+
};
|
|
150
|
+
const invalidSet = /* @__PURE__ */ new Set();
|
|
151
|
+
for (const result of results) {
|
|
152
|
+
merged.successCount += result.successCount;
|
|
153
|
+
merged.failureCount += result.failureCount;
|
|
154
|
+
for (const token of result.invalidTokens) {
|
|
155
|
+
if (!invalidSet.has(token)) {
|
|
156
|
+
invalidSet.add(token);
|
|
157
|
+
merged.invalidTokens.push(token);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
merged.transientFailures.push(...result.transientFailures);
|
|
161
|
+
merged.tokenFailures.push(...result.tokenFailures);
|
|
162
|
+
}
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
function emptyMulticastResult() {
|
|
166
|
+
return {
|
|
167
|
+
successCount: 0,
|
|
168
|
+
failureCount: 0,
|
|
169
|
+
invalidTokens: [],
|
|
170
|
+
transientFailures: [],
|
|
171
|
+
tokenFailures: []
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function appendTokenFailure(target, failure) {
|
|
175
|
+
target.tokenFailures.push(failure);
|
|
176
|
+
target.failureCount += 1;
|
|
177
|
+
if (failure.permanent) {
|
|
178
|
+
target.invalidTokens.push(failure.token);
|
|
179
|
+
} else {
|
|
180
|
+
target.transientFailures.push(failure);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/retry.ts
|
|
185
|
+
var PERMANENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
186
|
+
"invalid-registration-token",
|
|
187
|
+
"registration-token-not-registered",
|
|
188
|
+
"messaging/registration-token-not-registered",
|
|
189
|
+
"messaging/invalid-registration-token",
|
|
190
|
+
"not-registered",
|
|
191
|
+
"permission-denied",
|
|
192
|
+
"messaging/permission-denied",
|
|
193
|
+
"sender-id-mismatch",
|
|
194
|
+
"messaging/sender-id-mismatch",
|
|
195
|
+
"invalid-argument",
|
|
196
|
+
"messaging/invalid-argument"
|
|
197
|
+
]);
|
|
198
|
+
var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
199
|
+
"unavailable",
|
|
200
|
+
"messaging/unavailable",
|
|
201
|
+
"internal",
|
|
202
|
+
"messaging/internal",
|
|
203
|
+
"resource-exhausted",
|
|
204
|
+
"messaging/resource-exhausted",
|
|
205
|
+
"deadline-exceeded",
|
|
206
|
+
"messaging/deadline-exceeded"
|
|
207
|
+
]);
|
|
208
|
+
function extractErrorCode(err) {
|
|
209
|
+
if (!err || typeof err !== "object") {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
const e = err;
|
|
213
|
+
return e.code || e.errorInfo?.code;
|
|
214
|
+
}
|
|
215
|
+
function extractMessage(err) {
|
|
216
|
+
if (err instanceof Error) {
|
|
217
|
+
return err.message;
|
|
218
|
+
}
|
|
219
|
+
return String(err);
|
|
220
|
+
}
|
|
221
|
+
function isPermanentTokenError(err) {
|
|
222
|
+
const code = (extractErrorCode(err) || "").toLowerCase();
|
|
223
|
+
if (PERMANENT_ERROR_CODES.has(code)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
const msg = extractMessage(err).toLowerCase();
|
|
227
|
+
return msg.includes("not-registered") || msg.includes("invalid-registration-token") || msg.includes("registration-token-not-registered");
|
|
228
|
+
}
|
|
229
|
+
function isTransientError(err) {
|
|
230
|
+
const code = (extractErrorCode(err) || "").toLowerCase();
|
|
231
|
+
if (TRANSIENT_ERROR_CODES.has(code)) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (!err || typeof err !== "object") {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const e = err;
|
|
238
|
+
if (typeof e.status === "number" && e.status >= 500) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
const msg = String(e.message || e.code || "").toLowerCase();
|
|
242
|
+
return msg.includes("timeout") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("network") || msg.includes("socket") || msg.includes("unavailable");
|
|
243
|
+
}
|
|
244
|
+
function shouldRetryError(err) {
|
|
245
|
+
if (isPermanentTokenError(err)) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return isTransientError(err);
|
|
249
|
+
}
|
|
250
|
+
function classifyTokenError(err) {
|
|
251
|
+
const errorCode = extractErrorCode(err);
|
|
252
|
+
const message = extractMessage(err);
|
|
253
|
+
const permanent = isPermanentTokenError(err) || !isTransientError(err) && !shouldRetryError(err);
|
|
254
|
+
return { permanent, errorCode, message };
|
|
255
|
+
}
|
|
256
|
+
async function sleep(ms) {
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/client.ts
|
|
261
|
+
function buildAdminMessage(message, token) {
|
|
262
|
+
const payload = {};
|
|
263
|
+
if (message.notification) {
|
|
264
|
+
payload.notification = {
|
|
265
|
+
title: message.notification.title,
|
|
266
|
+
body: message.notification.body,
|
|
267
|
+
imageUrl: message.notification.imageUrl
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (message.data) {
|
|
271
|
+
payload.data = message.data;
|
|
272
|
+
}
|
|
273
|
+
if (message.webpush) {
|
|
274
|
+
payload.webpush = message.webpush;
|
|
275
|
+
}
|
|
276
|
+
if (message.android) {
|
|
277
|
+
payload.android = message.android;
|
|
278
|
+
}
|
|
279
|
+
if (message.apns) {
|
|
280
|
+
payload.apns = message.apns;
|
|
281
|
+
}
|
|
282
|
+
if (message.collapseKey) {
|
|
283
|
+
payload.collapseKey = message.collapseKey;
|
|
284
|
+
}
|
|
285
|
+
if (token) {
|
|
286
|
+
payload.token = token;
|
|
287
|
+
}
|
|
288
|
+
if (message.ttlSeconds !== void 0) {
|
|
289
|
+
payload.android = {
|
|
290
|
+
...payload.android || {},
|
|
291
|
+
ttl: message.ttlSeconds * 1e3
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return payload;
|
|
295
|
+
}
|
|
296
|
+
var FcmClient = class {
|
|
297
|
+
constructor(config, messagingClient, app$1) {
|
|
298
|
+
this.closed = false;
|
|
299
|
+
try {
|
|
300
|
+
this.resolved = resolveConfig(config);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
throw new FcmConfigError(err.message);
|
|
303
|
+
}
|
|
304
|
+
if (messagingClient) {
|
|
305
|
+
this.messaging = messagingClient;
|
|
306
|
+
this.app = app$1 ?? null;
|
|
307
|
+
} else {
|
|
308
|
+
const existing = app.getApps().find((a) => a.name === "multivendor-notification");
|
|
309
|
+
this.app = existing ?? app.initializeApp({
|
|
310
|
+
credential: app.cert({
|
|
311
|
+
projectId: this.resolved.projectId,
|
|
312
|
+
clientEmail: this.resolved.clientEmail,
|
|
313
|
+
privateKey: this.resolved.privateKey
|
|
314
|
+
}),
|
|
315
|
+
projectId: this.resolved.projectId
|
|
316
|
+
}, "multivendor-notification");
|
|
317
|
+
this.messaging = messaging.getMessaging(this.app);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
getConfig() {
|
|
321
|
+
return this.resolved;
|
|
322
|
+
}
|
|
323
|
+
async validate() {
|
|
324
|
+
resolveConfig(this.resolved);
|
|
325
|
+
}
|
|
326
|
+
async close() {
|
|
327
|
+
this.closed = true;
|
|
328
|
+
if (this.app) {
|
|
329
|
+
await app.deleteApp(this.app).catch(() => void 0);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async send(message) {
|
|
333
|
+
return this.sendToToken({ token: message.token, message });
|
|
334
|
+
}
|
|
335
|
+
async sendToToken(params) {
|
|
336
|
+
this.assertOpen();
|
|
337
|
+
const token = params.token?.trim();
|
|
338
|
+
if (!token) {
|
|
339
|
+
throw new FcmConfigError("FCM token is required");
|
|
340
|
+
}
|
|
341
|
+
validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });
|
|
342
|
+
let lastError;
|
|
343
|
+
const maxAttempts = 2;
|
|
344
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
345
|
+
try {
|
|
346
|
+
const messageId = await this.messaging.send(
|
|
347
|
+
buildAdminMessage(params.message, token)
|
|
348
|
+
);
|
|
349
|
+
return { messageId, attemptCount: attempt };
|
|
350
|
+
} catch (err) {
|
|
351
|
+
lastError = err;
|
|
352
|
+
if (isPermanentTokenFailure(err) || !shouldRetryError(err) || attempt >= maxAttempts) {
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
await sleep(this.resolved.retryBackoffMs);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const classified = classifyTokenError(lastError);
|
|
359
|
+
throw new FcmApiError(classified.message, {
|
|
360
|
+
errorCode: classified.errorCode,
|
|
361
|
+
attemptCount: maxAttempts,
|
|
362
|
+
details: lastError
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async sendMulticast(params) {
|
|
366
|
+
this.assertOpen();
|
|
367
|
+
validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });
|
|
368
|
+
const uniqueTokens = dedupeTokens(params.tokens);
|
|
369
|
+
if (uniqueTokens.length === 0) {
|
|
370
|
+
return emptyMulticastResult();
|
|
371
|
+
}
|
|
372
|
+
const chunks = chunkTokens(uniqueTokens);
|
|
373
|
+
const batchResults = [];
|
|
374
|
+
for (const batch of chunks) {
|
|
375
|
+
batchResults.push(await this.sendMulticastBatch(batch, params.message));
|
|
376
|
+
}
|
|
377
|
+
return mergeMulticastResults(batchResults);
|
|
378
|
+
}
|
|
379
|
+
async sendMulticastBatch(tokens, message) {
|
|
380
|
+
const result = emptyMulticastResult();
|
|
381
|
+
let lastBatchError;
|
|
382
|
+
const maxAttempts = 2;
|
|
383
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
384
|
+
try {
|
|
385
|
+
const response = await this.messaging.sendEachForMulticast({
|
|
386
|
+
tokens,
|
|
387
|
+
notification: message.notification ? {
|
|
388
|
+
title: message.notification.title,
|
|
389
|
+
body: message.notification.body,
|
|
390
|
+
imageUrl: message.notification.imageUrl
|
|
391
|
+
} : void 0,
|
|
392
|
+
data: message.data,
|
|
393
|
+
webpush: message.webpush,
|
|
394
|
+
android: message.android,
|
|
395
|
+
apns: message.apns
|
|
396
|
+
});
|
|
397
|
+
result.successCount += response.successCount;
|
|
398
|
+
result.failureCount += response.failureCount;
|
|
399
|
+
response.responses.forEach((res, index) => {
|
|
400
|
+
if (res.success) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const token = tokens[index];
|
|
404
|
+
const classified2 = classifyTokenError(res.error);
|
|
405
|
+
const failure = {
|
|
406
|
+
token,
|
|
407
|
+
errorCode: classified2.errorCode,
|
|
408
|
+
message: classified2.message,
|
|
409
|
+
permanent: classified2.permanent
|
|
410
|
+
};
|
|
411
|
+
appendTokenFailure(result, failure);
|
|
412
|
+
});
|
|
413
|
+
return result;
|
|
414
|
+
} catch (err) {
|
|
415
|
+
lastBatchError = err;
|
|
416
|
+
if (!shouldRetryError(err) || attempt >= maxAttempts) {
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
await sleep(this.resolved.retryBackoffMs);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const classified = classifyTokenError(lastBatchError);
|
|
423
|
+
if (!classified.permanent && shouldRetryError(lastBatchError)) {
|
|
424
|
+
for (const token of tokens) {
|
|
425
|
+
appendTokenFailure(result, {
|
|
426
|
+
token,
|
|
427
|
+
errorCode: classified.errorCode,
|
|
428
|
+
message: classified.message,
|
|
429
|
+
permanent: false
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
throw new FcmApiError(classified.message, {
|
|
435
|
+
errorCode: classified.errorCode,
|
|
436
|
+
attemptCount: maxAttempts,
|
|
437
|
+
details: lastBatchError
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
assertOpen() {
|
|
441
|
+
if (this.closed) {
|
|
442
|
+
throw new FcmConfigError("FcmClient is closed");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function isPermanentTokenFailure(err) {
|
|
447
|
+
return classifyTokenError(err).permanent;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
exports.FCM_MULTICAST_BATCH_SIZE = FCM_MULTICAST_BATCH_SIZE;
|
|
451
|
+
exports.FcmApiError = FcmApiError;
|
|
452
|
+
exports.FcmClient = FcmClient;
|
|
453
|
+
exports.FcmConfigError = FcmConfigError;
|
|
454
|
+
exports.FcmPayloadError = FcmPayloadError;
|
|
455
|
+
exports.chunkTokens = chunkTokens;
|
|
456
|
+
exports.classifyTokenError = classifyTokenError;
|
|
457
|
+
exports.dedupeTokens = dedupeTokens;
|
|
458
|
+
exports.estimatePayloadBytes = estimatePayloadBytes;
|
|
459
|
+
exports.isPermanentTokenError = isPermanentTokenError;
|
|
460
|
+
exports.isTransientError = isTransientError;
|
|
461
|
+
exports.isValidPemPrivateKey = isValidPemPrivateKey;
|
|
462
|
+
exports.mergeMulticastResults = mergeMulticastResults;
|
|
463
|
+
exports.resolveConfig = resolveConfig;
|
|
464
|
+
exports.shouldRetryError = shouldRetryError;
|
|
465
|
+
exports.validatePayload = validatePayload;
|
|
466
|
+
//# sourceMappingURL=index.cjs.map
|
|
467
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/validate.ts","../src/batch.ts","../src/retry.ts","../src/client.ts"],"names":["app","getApps","initializeApp","cert","getMessaging","deleteApp","classified"],"mappings":"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","file":"index.cjs","sourcesContent":["export class FcmConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'FcmConfigError';\n }\n}\n\nexport class FcmPayloadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'FcmPayloadError';\n }\n}\n\nexport class FcmApiError extends Error {\n public readonly statusCode?: number;\n public readonly errorCode?: string;\n public readonly attemptCount: number;\n public readonly details?: unknown;\n\n constructor(\n message: string,\n options: { statusCode?: number; errorCode?: string; attemptCount?: number; details?: unknown } = {},\n ) {\n super(message);\n this.name = 'FcmApiError';\n this.statusCode = options.statusCode;\n this.errorCode = options.errorCode;\n this.attemptCount = options.attemptCount ?? 1;\n this.details = options.details;\n }\n}\n","import { FcmConfigError } from './errors';\n\nexport interface FcmConfig {\n projectId?: string;\n clientEmail?: string;\n privateKey?: string;\n retryBackoffMs?: number;\n maxPayloadBytes?: number;\n}\n\nexport interface ResolvedFcmConfig {\n projectId: string;\n clientEmail: string;\n privateKey: string;\n retryBackoffMs: number;\n maxPayloadBytes: number;\n}\n\nconst PEM_HEADER = '-----BEGIN PRIVATE KEY-----';\nconst PEM_FOOTER = '-----END PRIVATE KEY-----';\n\nfunction normalizePrivateKey(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed.includes('\\\\n')) {\n return trimmed.replace(/\\\\n/g, '\\n');\n }\n return trimmed;\n}\n\nexport function isValidPemPrivateKey(key: string): boolean {\n const normalized = normalizePrivateKey(key);\n return normalized.includes(PEM_HEADER) && normalized.includes(PEM_FOOTER);\n}\n\nexport function resolveConfig(config: FcmConfig): ResolvedFcmConfig {\n const projectId = config.projectId?.trim() || '';\n const clientEmail = config.clientEmail?.trim() || '';\n const privateKeyRaw = config.privateKey?.trim() || '';\n\n if (!projectId) {\n throw new FcmConfigError('FCM projectId is required');\n }\n if (!clientEmail || !clientEmail.includes('@')) {\n throw new FcmConfigError('FCM clientEmail must be a valid service account email');\n }\n if (!privateKeyRaw) {\n throw new FcmConfigError('FCM privateKey is required');\n }\n\n const privateKey = normalizePrivateKey(privateKeyRaw);\n if (!isValidPemPrivateKey(privateKey)) {\n throw new FcmConfigError('FCM privateKey must be a valid PEM private key');\n }\n\n return {\n projectId,\n clientEmail,\n privateKey,\n retryBackoffMs: config.retryBackoffMs ?? 250,\n maxPayloadBytes: config.maxPayloadBytes ?? 4096,\n };\n}\n","import { FcmPayloadError } from './errors';\nimport type { PushMessage } from './types';\n\nconst DEFAULT_TITLE_MAX = 200;\nconst DEFAULT_BODY_MAX = 1000;\n\nexport function estimatePayloadBytes(message: PushMessage): number {\n let bytes = 0;\n const notification = message.notification;\n if (notification?.title) {\n bytes += Buffer.byteLength(notification.title, 'utf8');\n }\n if (notification?.body) {\n bytes += Buffer.byteLength(notification.body, 'utf8');\n }\n if (notification?.imageUrl) {\n bytes += Buffer.byteLength(notification.imageUrl, 'utf8');\n }\n if (message.data) {\n for (const [key, value] of Object.entries(message.data)) {\n bytes += Buffer.byteLength(key, 'utf8') + Buffer.byteLength(value, 'utf8');\n }\n }\n if (message.collapseKey) {\n bytes += Buffer.byteLength(message.collapseKey, 'utf8');\n }\n if (message.messageId) {\n bytes += Buffer.byteLength(message.messageId, 'utf8');\n }\n return bytes;\n}\n\nexport function validatePayload(\n message: PushMessage,\n options: { maxPayloadBytes?: number; titleMax?: number; bodyMax?: number } = {},\n): void {\n const maxPayloadBytes = options.maxPayloadBytes ?? 4096;\n const titleMax = options.titleMax ?? DEFAULT_TITLE_MAX;\n const bodyMax = options.bodyMax ?? DEFAULT_BODY_MAX;\n\n const title = message.notification?.title;\n const body = message.notification?.body;\n\n if (title && title.length > titleMax) {\n throw new FcmPayloadError(`Notification title exceeds ${titleMax} characters`);\n }\n if (body && body.length > bodyMax) {\n throw new FcmPayloadError(`Notification body exceeds ${bodyMax} characters`);\n }\n\n const estimated = estimatePayloadBytes(message);\n if (estimated > maxPayloadBytes) {\n throw new FcmPayloadError(\n `Push payload estimated at ${estimated} bytes exceeds limit of ${maxPayloadBytes} bytes`,\n );\n }\n}\n","import type { MulticastResult, TokenSendFailure } from './types';\n\nexport const FCM_MULTICAST_BATCH_SIZE = 500;\n\nexport function dedupeTokens(tokens: string[]): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const raw of tokens) {\n const token = raw?.trim();\n if (!token || seen.has(token)) {\n continue;\n }\n seen.add(token);\n result.push(token);\n }\n return result;\n}\n\nexport function chunkTokens(tokens: string[], batchSize: number = FCM_MULTICAST_BATCH_SIZE): string[][] {\n const chunks: string[][] = [];\n for (let i = 0; i < tokens.length; i += batchSize) {\n chunks.push(tokens.slice(i, i + batchSize));\n }\n return chunks;\n}\n\nexport function mergeMulticastResults(results: MulticastResult[]): MulticastResult {\n const merged: MulticastResult = {\n successCount: 0,\n failureCount: 0,\n invalidTokens: [],\n transientFailures: [],\n tokenFailures: [],\n };\n\n const invalidSet = new Set<string>();\n\n for (const result of results) {\n merged.successCount += result.successCount;\n merged.failureCount += result.failureCount;\n for (const token of result.invalidTokens) {\n if (!invalidSet.has(token)) {\n invalidSet.add(token);\n merged.invalidTokens.push(token);\n }\n }\n merged.transientFailures.push(...result.transientFailures);\n merged.tokenFailures.push(...result.tokenFailures);\n }\n\n return merged;\n}\n\nexport function emptyMulticastResult(): MulticastResult {\n return {\n successCount: 0,\n failureCount: 0,\n invalidTokens: [],\n transientFailures: [],\n tokenFailures: [],\n };\n}\n\nexport function appendTokenFailure(\n target: MulticastResult,\n failure: TokenSendFailure,\n): void {\n target.tokenFailures.push(failure);\n target.failureCount += 1;\n if (failure.permanent) {\n target.invalidTokens.push(failure.token);\n } else {\n target.transientFailures.push(failure);\n }\n}\n","const PERMANENT_ERROR_CODES = new Set([\n 'invalid-registration-token',\n 'registration-token-not-registered',\n 'messaging/registration-token-not-registered',\n 'messaging/invalid-registration-token',\n 'not-registered',\n 'permission-denied',\n 'messaging/permission-denied',\n 'sender-id-mismatch',\n 'messaging/sender-id-mismatch',\n 'invalid-argument',\n 'messaging/invalid-argument',\n]);\n\nconst TRANSIENT_ERROR_CODES = new Set([\n 'unavailable',\n 'messaging/unavailable',\n 'internal',\n 'messaging/internal',\n 'resource-exhausted',\n 'messaging/resource-exhausted',\n 'deadline-exceeded',\n 'messaging/deadline-exceeded',\n]);\n\nfunction extractErrorCode(err: unknown): string | undefined {\n if (!err || typeof err !== 'object') {\n return undefined;\n }\n const e = err as { code?: string; errorInfo?: { code?: string } };\n return e.code || e.errorInfo?.code;\n}\n\nfunction extractMessage(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\nexport function isPermanentTokenError(err: unknown): boolean {\n const code = (extractErrorCode(err) || '').toLowerCase();\n if (PERMANENT_ERROR_CODES.has(code)) {\n return true;\n }\n const msg = extractMessage(err).toLowerCase();\n return msg.includes('not-registered')\n || msg.includes('invalid-registration-token')\n || msg.includes('registration-token-not-registered');\n}\n\nexport function isTransientError(err: unknown): boolean {\n const code = (extractErrorCode(err) || '').toLowerCase();\n if (TRANSIENT_ERROR_CODES.has(code)) {\n return true;\n }\n if (!err || typeof err !== 'object') {\n return false;\n }\n const e = err as { status?: number; message?: string; code?: string };\n if (typeof e.status === 'number' && e.status >= 500) {\n return true;\n }\n const msg = String(e.message || e.code || '').toLowerCase();\n return msg.includes('timeout')\n || msg.includes('econnreset')\n || msg.includes('enotfound')\n || msg.includes('network')\n || msg.includes('socket')\n || msg.includes('unavailable');\n}\n\nexport function shouldRetryError(err: unknown): boolean {\n if (isPermanentTokenError(err)) {\n return false;\n }\n return isTransientError(err);\n}\n\nexport function classifyTokenError(err: unknown): { permanent: boolean; errorCode?: string; message: string } {\n const errorCode = extractErrorCode(err);\n const message = extractMessage(err);\n const permanent = isPermanentTokenError(err) || (!isTransientError(err) && !shouldRetryError(err));\n return { permanent, errorCode, message };\n}\n\nexport async function sleep(ms: number): Promise<void> {\n await new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n","import type { Message, Messaging } from 'firebase-admin/messaging';\nimport { initializeApp, cert, getApps, deleteApp, type App } from 'firebase-admin/app';\nimport { getMessaging } from 'firebase-admin/messaging';\nimport { resolveConfig, type FcmConfig, type ResolvedFcmConfig } from './config';\nimport { FcmApiError, FcmConfigError } from './errors';\nimport { validatePayload } from './validate';\nimport {\n appendTokenFailure,\n chunkTokens,\n dedupeTokens,\n emptyMulticastResult,\n mergeMulticastResults,\n} from './batch';\nimport {\n classifyTokenError,\n shouldRetryError,\n sleep,\n} from './retry';\nimport type {\n MulticastResult,\n PushMessage,\n SendMulticastParams,\n SendResult,\n SendToTokenParams,\n TokenSendFailure,\n} from './types';\n\nexport type FcmMessagingClient = Pick<Messaging, 'send' | 'sendEachForMulticast'>;\n\nfunction buildAdminMessage(message: PushMessage, token?: string): Record<string, unknown> {\n const payload: Record<string, unknown> = {};\n if (message.notification) {\n payload.notification = {\n title: message.notification.title,\n body: message.notification.body,\n imageUrl: message.notification.imageUrl,\n };\n }\n if (message.data) {\n payload.data = message.data;\n }\n if (message.webpush) {\n payload.webpush = message.webpush;\n }\n if (message.android) {\n payload.android = message.android;\n }\n if (message.apns) {\n payload.apns = message.apns;\n }\n if (message.collapseKey) {\n payload.collapseKey = message.collapseKey;\n }\n if (token) {\n payload.token = token;\n }\n if (message.ttlSeconds !== undefined) {\n payload.android = {\n ...(payload.android as Record<string, unknown> || {}),\n ttl: message.ttlSeconds * 1000,\n };\n }\n return payload;\n}\n\nexport class FcmClient {\n private readonly resolved: ResolvedFcmConfig;\n private readonly messaging: FcmMessagingClient;\n private readonly app: App | null;\n private closed = false;\n\n constructor(config: FcmConfig, messagingClient?: FcmMessagingClient, app?: App) {\n try {\n this.resolved = resolveConfig(config);\n } catch (err) {\n throw new FcmConfigError((err as Error).message);\n }\n\n if (messagingClient) {\n this.messaging = messagingClient;\n this.app = app ?? null;\n } else {\n const existing = getApps().find((a) => a.name === 'multivendor-notification');\n this.app = existing ?? initializeApp({\n credential: cert({\n projectId: this.resolved.projectId,\n clientEmail: this.resolved.clientEmail,\n privateKey: this.resolved.privateKey,\n }),\n projectId: this.resolved.projectId,\n }, 'multivendor-notification');\n this.messaging = getMessaging(this.app);\n }\n }\n\n getConfig(): ResolvedFcmConfig {\n return this.resolved;\n }\n\n async validate(): Promise<void> {\n resolveConfig(this.resolved);\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.app) {\n await deleteApp(this.app).catch(() => undefined);\n }\n }\n\n async send(message: PushMessage & { token: string }): Promise<SendResult> {\n return this.sendToToken({ token: message.token, message });\n }\n\n async sendToToken(params: SendToTokenParams): Promise<SendResult> {\n this.assertOpen();\n const token = params.token?.trim();\n if (!token) {\n throw new FcmConfigError('FCM token is required');\n }\n\n validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });\n\n let lastError: unknown;\n const maxAttempts = 2;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const messageId = await this.messaging.send(\n buildAdminMessage(params.message, token) as unknown as Message,\n );\n return { messageId, attemptCount: attempt };\n } catch (err) {\n lastError = err;\n if (isPermanentTokenFailure(err) || !shouldRetryError(err) || attempt >= maxAttempts) {\n break;\n }\n await sleep(this.resolved.retryBackoffMs);\n }\n }\n\n const classified = classifyTokenError(lastError);\n throw new FcmApiError(classified.message, {\n errorCode: classified.errorCode,\n attemptCount: maxAttempts,\n details: lastError,\n });\n }\n\n async sendMulticast(params: SendMulticastParams): Promise<MulticastResult> {\n this.assertOpen();\n validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });\n\n const uniqueTokens = dedupeTokens(params.tokens);\n if (uniqueTokens.length === 0) {\n return emptyMulticastResult();\n }\n\n const chunks = chunkTokens(uniqueTokens);\n const batchResults: MulticastResult[] = [];\n\n for (const batch of chunks) {\n batchResults.push(await this.sendMulticastBatch(batch, params.message));\n }\n\n return mergeMulticastResults(batchResults);\n }\n\n private async sendMulticastBatch(tokens: string[], message: PushMessage): Promise<MulticastResult> {\n const result = emptyMulticastResult();\n\n let lastBatchError: unknown;\n const maxAttempts = 2;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const response = await this.messaging.sendEachForMulticast({\n tokens,\n notification: message.notification\n ? {\n title: message.notification.title,\n body: message.notification.body,\n imageUrl: message.notification.imageUrl,\n }\n : undefined,\n data: message.data,\n webpush: message.webpush as never,\n android: message.android as never,\n apns: message.apns as never,\n });\n\n result.successCount += response.successCount;\n result.failureCount += response.failureCount;\n\n response.responses.forEach((res, index) => {\n if (res.success) {\n return;\n }\n const token = tokens[index];\n const classified = classifyTokenError(res.error);\n const failure: TokenSendFailure = {\n token,\n errorCode: classified.errorCode,\n message: classified.message,\n permanent: classified.permanent,\n };\n appendTokenFailure(result, failure);\n });\n\n return result;\n } catch (err) {\n lastBatchError = err;\n if (!shouldRetryError(err) || attempt >= maxAttempts) {\n break;\n }\n await sleep(this.resolved.retryBackoffMs);\n }\n }\n\n const classified = classifyTokenError(lastBatchError);\n if (!classified.permanent && shouldRetryError(lastBatchError)) {\n for (const token of tokens) {\n appendTokenFailure(result, {\n token,\n errorCode: classified.errorCode,\n message: classified.message,\n permanent: false,\n });\n }\n return result;\n }\n\n throw new FcmApiError(classified.message, {\n errorCode: classified.errorCode,\n attemptCount: maxAttempts,\n details: lastBatchError,\n });\n }\n\n private assertOpen(): void {\n if (this.closed) {\n throw new FcmConfigError('FcmClient is closed');\n }\n }\n}\n\nfunction isPermanentTokenFailure(err: unknown): boolean {\n return classifyTokenError(err).permanent;\n}\n"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Messaging } from 'firebase-admin/messaging';
|
|
2
|
+
import { App } from 'firebase-admin/app';
|
|
3
|
+
|
|
4
|
+
interface FcmConfig {
|
|
5
|
+
projectId?: string;
|
|
6
|
+
clientEmail?: string;
|
|
7
|
+
privateKey?: string;
|
|
8
|
+
retryBackoffMs?: number;
|
|
9
|
+
maxPayloadBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
interface ResolvedFcmConfig {
|
|
12
|
+
projectId: string;
|
|
13
|
+
clientEmail: string;
|
|
14
|
+
privateKey: string;
|
|
15
|
+
retryBackoffMs: number;
|
|
16
|
+
maxPayloadBytes: number;
|
|
17
|
+
}
|
|
18
|
+
declare function isValidPemPrivateKey(key: string): boolean;
|
|
19
|
+
declare function resolveConfig(config: FcmConfig): ResolvedFcmConfig;
|
|
20
|
+
|
|
21
|
+
interface PushNotificationPayload {
|
|
22
|
+
title?: string;
|
|
23
|
+
body?: string;
|
|
24
|
+
imageUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
interface PushMessage {
|
|
27
|
+
notification?: PushNotificationPayload;
|
|
28
|
+
data?: Record<string, string>;
|
|
29
|
+
webpush?: Record<string, unknown>;
|
|
30
|
+
android?: Record<string, unknown>;
|
|
31
|
+
apns?: Record<string, unknown>;
|
|
32
|
+
collapseKey?: string;
|
|
33
|
+
messageId?: string;
|
|
34
|
+
ttlSeconds?: number;
|
|
35
|
+
priority?: 'normal' | 'high';
|
|
36
|
+
}
|
|
37
|
+
interface SendToTokenParams {
|
|
38
|
+
token: string;
|
|
39
|
+
message: PushMessage;
|
|
40
|
+
}
|
|
41
|
+
interface SendMulticastParams {
|
|
42
|
+
tokens: string[];
|
|
43
|
+
message: PushMessage;
|
|
44
|
+
}
|
|
45
|
+
interface SendResult {
|
|
46
|
+
messageId: string;
|
|
47
|
+
attemptCount: number;
|
|
48
|
+
}
|
|
49
|
+
interface TokenSendFailure {
|
|
50
|
+
token: string;
|
|
51
|
+
errorCode?: string;
|
|
52
|
+
message: string;
|
|
53
|
+
permanent: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface MulticastResult {
|
|
56
|
+
successCount: number;
|
|
57
|
+
failureCount: number;
|
|
58
|
+
invalidTokens: string[];
|
|
59
|
+
transientFailures: TokenSendFailure[];
|
|
60
|
+
tokenFailures: TokenSendFailure[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type FcmMessagingClient = Pick<Messaging, 'send' | 'sendEachForMulticast'>;
|
|
64
|
+
declare class FcmClient {
|
|
65
|
+
private readonly resolved;
|
|
66
|
+
private readonly messaging;
|
|
67
|
+
private readonly app;
|
|
68
|
+
private closed;
|
|
69
|
+
constructor(config: FcmConfig, messagingClient?: FcmMessagingClient, app?: App);
|
|
70
|
+
getConfig(): ResolvedFcmConfig;
|
|
71
|
+
validate(): Promise<void>;
|
|
72
|
+
close(): Promise<void>;
|
|
73
|
+
send(message: PushMessage & {
|
|
74
|
+
token: string;
|
|
75
|
+
}): Promise<SendResult>;
|
|
76
|
+
sendToToken(params: SendToTokenParams): Promise<SendResult>;
|
|
77
|
+
sendMulticast(params: SendMulticastParams): Promise<MulticastResult>;
|
|
78
|
+
private sendMulticastBatch;
|
|
79
|
+
private assertOpen;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare class FcmConfigError extends Error {
|
|
83
|
+
constructor(message: string);
|
|
84
|
+
}
|
|
85
|
+
declare class FcmPayloadError extends Error {
|
|
86
|
+
constructor(message: string);
|
|
87
|
+
}
|
|
88
|
+
declare class FcmApiError extends Error {
|
|
89
|
+
readonly statusCode?: number;
|
|
90
|
+
readonly errorCode?: string;
|
|
91
|
+
readonly attemptCount: number;
|
|
92
|
+
readonly details?: unknown;
|
|
93
|
+
constructor(message: string, options?: {
|
|
94
|
+
statusCode?: number;
|
|
95
|
+
errorCode?: string;
|
|
96
|
+
attemptCount?: number;
|
|
97
|
+
details?: unknown;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
declare function estimatePayloadBytes(message: PushMessage): number;
|
|
102
|
+
declare function validatePayload(message: PushMessage, options?: {
|
|
103
|
+
maxPayloadBytes?: number;
|
|
104
|
+
titleMax?: number;
|
|
105
|
+
bodyMax?: number;
|
|
106
|
+
}): void;
|
|
107
|
+
|
|
108
|
+
declare function isPermanentTokenError(err: unknown): boolean;
|
|
109
|
+
declare function isTransientError(err: unknown): boolean;
|
|
110
|
+
declare function shouldRetryError(err: unknown): boolean;
|
|
111
|
+
declare function classifyTokenError(err: unknown): {
|
|
112
|
+
permanent: boolean;
|
|
113
|
+
errorCode?: string;
|
|
114
|
+
message: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
declare const FCM_MULTICAST_BATCH_SIZE = 500;
|
|
118
|
+
declare function dedupeTokens(tokens: string[]): string[];
|
|
119
|
+
declare function chunkTokens(tokens: string[], batchSize?: number): string[][];
|
|
120
|
+
declare function mergeMulticastResults(results: MulticastResult[]): MulticastResult;
|
|
121
|
+
|
|
122
|
+
export { FCM_MULTICAST_BATCH_SIZE, FcmApiError, FcmClient, type FcmConfig, FcmConfigError, type FcmMessagingClient, FcmPayloadError, type MulticastResult, type PushMessage, type PushNotificationPayload, type ResolvedFcmConfig, type SendMulticastParams, type SendResult, type SendToTokenParams, type TokenSendFailure, chunkTokens, classifyTokenError, dedupeTokens, estimatePayloadBytes, isPermanentTokenError, isTransientError, isValidPemPrivateKey, mergeMulticastResults, resolveConfig, shouldRetryError, validatePayload };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Messaging } from 'firebase-admin/messaging';
|
|
2
|
+
import { App } from 'firebase-admin/app';
|
|
3
|
+
|
|
4
|
+
interface FcmConfig {
|
|
5
|
+
projectId?: string;
|
|
6
|
+
clientEmail?: string;
|
|
7
|
+
privateKey?: string;
|
|
8
|
+
retryBackoffMs?: number;
|
|
9
|
+
maxPayloadBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
interface ResolvedFcmConfig {
|
|
12
|
+
projectId: string;
|
|
13
|
+
clientEmail: string;
|
|
14
|
+
privateKey: string;
|
|
15
|
+
retryBackoffMs: number;
|
|
16
|
+
maxPayloadBytes: number;
|
|
17
|
+
}
|
|
18
|
+
declare function isValidPemPrivateKey(key: string): boolean;
|
|
19
|
+
declare function resolveConfig(config: FcmConfig): ResolvedFcmConfig;
|
|
20
|
+
|
|
21
|
+
interface PushNotificationPayload {
|
|
22
|
+
title?: string;
|
|
23
|
+
body?: string;
|
|
24
|
+
imageUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
interface PushMessage {
|
|
27
|
+
notification?: PushNotificationPayload;
|
|
28
|
+
data?: Record<string, string>;
|
|
29
|
+
webpush?: Record<string, unknown>;
|
|
30
|
+
android?: Record<string, unknown>;
|
|
31
|
+
apns?: Record<string, unknown>;
|
|
32
|
+
collapseKey?: string;
|
|
33
|
+
messageId?: string;
|
|
34
|
+
ttlSeconds?: number;
|
|
35
|
+
priority?: 'normal' | 'high';
|
|
36
|
+
}
|
|
37
|
+
interface SendToTokenParams {
|
|
38
|
+
token: string;
|
|
39
|
+
message: PushMessage;
|
|
40
|
+
}
|
|
41
|
+
interface SendMulticastParams {
|
|
42
|
+
tokens: string[];
|
|
43
|
+
message: PushMessage;
|
|
44
|
+
}
|
|
45
|
+
interface SendResult {
|
|
46
|
+
messageId: string;
|
|
47
|
+
attemptCount: number;
|
|
48
|
+
}
|
|
49
|
+
interface TokenSendFailure {
|
|
50
|
+
token: string;
|
|
51
|
+
errorCode?: string;
|
|
52
|
+
message: string;
|
|
53
|
+
permanent: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface MulticastResult {
|
|
56
|
+
successCount: number;
|
|
57
|
+
failureCount: number;
|
|
58
|
+
invalidTokens: string[];
|
|
59
|
+
transientFailures: TokenSendFailure[];
|
|
60
|
+
tokenFailures: TokenSendFailure[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type FcmMessagingClient = Pick<Messaging, 'send' | 'sendEachForMulticast'>;
|
|
64
|
+
declare class FcmClient {
|
|
65
|
+
private readonly resolved;
|
|
66
|
+
private readonly messaging;
|
|
67
|
+
private readonly app;
|
|
68
|
+
private closed;
|
|
69
|
+
constructor(config: FcmConfig, messagingClient?: FcmMessagingClient, app?: App);
|
|
70
|
+
getConfig(): ResolvedFcmConfig;
|
|
71
|
+
validate(): Promise<void>;
|
|
72
|
+
close(): Promise<void>;
|
|
73
|
+
send(message: PushMessage & {
|
|
74
|
+
token: string;
|
|
75
|
+
}): Promise<SendResult>;
|
|
76
|
+
sendToToken(params: SendToTokenParams): Promise<SendResult>;
|
|
77
|
+
sendMulticast(params: SendMulticastParams): Promise<MulticastResult>;
|
|
78
|
+
private sendMulticastBatch;
|
|
79
|
+
private assertOpen;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare class FcmConfigError extends Error {
|
|
83
|
+
constructor(message: string);
|
|
84
|
+
}
|
|
85
|
+
declare class FcmPayloadError extends Error {
|
|
86
|
+
constructor(message: string);
|
|
87
|
+
}
|
|
88
|
+
declare class FcmApiError extends Error {
|
|
89
|
+
readonly statusCode?: number;
|
|
90
|
+
readonly errorCode?: string;
|
|
91
|
+
readonly attemptCount: number;
|
|
92
|
+
readonly details?: unknown;
|
|
93
|
+
constructor(message: string, options?: {
|
|
94
|
+
statusCode?: number;
|
|
95
|
+
errorCode?: string;
|
|
96
|
+
attemptCount?: number;
|
|
97
|
+
details?: unknown;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
declare function estimatePayloadBytes(message: PushMessage): number;
|
|
102
|
+
declare function validatePayload(message: PushMessage, options?: {
|
|
103
|
+
maxPayloadBytes?: number;
|
|
104
|
+
titleMax?: number;
|
|
105
|
+
bodyMax?: number;
|
|
106
|
+
}): void;
|
|
107
|
+
|
|
108
|
+
declare function isPermanentTokenError(err: unknown): boolean;
|
|
109
|
+
declare function isTransientError(err: unknown): boolean;
|
|
110
|
+
declare function shouldRetryError(err: unknown): boolean;
|
|
111
|
+
declare function classifyTokenError(err: unknown): {
|
|
112
|
+
permanent: boolean;
|
|
113
|
+
errorCode?: string;
|
|
114
|
+
message: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
declare const FCM_MULTICAST_BATCH_SIZE = 500;
|
|
118
|
+
declare function dedupeTokens(tokens: string[]): string[];
|
|
119
|
+
declare function chunkTokens(tokens: string[], batchSize?: number): string[][];
|
|
120
|
+
declare function mergeMulticastResults(results: MulticastResult[]): MulticastResult;
|
|
121
|
+
|
|
122
|
+
export { FCM_MULTICAST_BATCH_SIZE, FcmApiError, FcmClient, type FcmConfig, FcmConfigError, type FcmMessagingClient, FcmPayloadError, type MulticastResult, type PushMessage, type PushNotificationPayload, type ResolvedFcmConfig, type SendMulticastParams, type SendResult, type SendToTokenParams, type TokenSendFailure, chunkTokens, classifyTokenError, dedupeTokens, estimatePayloadBytes, isPermanentTokenError, isTransientError, isValidPemPrivateKey, mergeMulticastResults, resolveConfig, shouldRetryError, validatePayload };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { getApps, initializeApp, cert, deleteApp } from 'firebase-admin/app';
|
|
2
|
+
import { getMessaging } from 'firebase-admin/messaging';
|
|
3
|
+
|
|
4
|
+
// src/client.ts
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var FcmConfigError = class extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "FcmConfigError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var FcmPayloadError = class extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "FcmPayloadError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var FcmApiError = class extends Error {
|
|
20
|
+
constructor(message, options = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "FcmApiError";
|
|
23
|
+
this.statusCode = options.statusCode;
|
|
24
|
+
this.errorCode = options.errorCode;
|
|
25
|
+
this.attemptCount = options.attemptCount ?? 1;
|
|
26
|
+
this.details = options.details;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/config.ts
|
|
31
|
+
var PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
|
|
32
|
+
var PEM_FOOTER = "-----END PRIVATE KEY-----";
|
|
33
|
+
function normalizePrivateKey(raw) {
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (trimmed.includes("\\n")) {
|
|
36
|
+
return trimmed.replace(/\\n/g, "\n");
|
|
37
|
+
}
|
|
38
|
+
return trimmed;
|
|
39
|
+
}
|
|
40
|
+
function isValidPemPrivateKey(key) {
|
|
41
|
+
const normalized = normalizePrivateKey(key);
|
|
42
|
+
return normalized.includes(PEM_HEADER) && normalized.includes(PEM_FOOTER);
|
|
43
|
+
}
|
|
44
|
+
function resolveConfig(config) {
|
|
45
|
+
const projectId = config.projectId?.trim() || "";
|
|
46
|
+
const clientEmail = config.clientEmail?.trim() || "";
|
|
47
|
+
const privateKeyRaw = config.privateKey?.trim() || "";
|
|
48
|
+
if (!projectId) {
|
|
49
|
+
throw new FcmConfigError("FCM projectId is required");
|
|
50
|
+
}
|
|
51
|
+
if (!clientEmail || !clientEmail.includes("@")) {
|
|
52
|
+
throw new FcmConfigError("FCM clientEmail must be a valid service account email");
|
|
53
|
+
}
|
|
54
|
+
if (!privateKeyRaw) {
|
|
55
|
+
throw new FcmConfigError("FCM privateKey is required");
|
|
56
|
+
}
|
|
57
|
+
const privateKey = normalizePrivateKey(privateKeyRaw);
|
|
58
|
+
if (!isValidPemPrivateKey(privateKey)) {
|
|
59
|
+
throw new FcmConfigError("FCM privateKey must be a valid PEM private key");
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
projectId,
|
|
63
|
+
clientEmail,
|
|
64
|
+
privateKey,
|
|
65
|
+
retryBackoffMs: config.retryBackoffMs ?? 250,
|
|
66
|
+
maxPayloadBytes: config.maxPayloadBytes ?? 4096
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/validate.ts
|
|
71
|
+
var DEFAULT_TITLE_MAX = 200;
|
|
72
|
+
var DEFAULT_BODY_MAX = 1e3;
|
|
73
|
+
function estimatePayloadBytes(message) {
|
|
74
|
+
let bytes = 0;
|
|
75
|
+
const notification = message.notification;
|
|
76
|
+
if (notification?.title) {
|
|
77
|
+
bytes += Buffer.byteLength(notification.title, "utf8");
|
|
78
|
+
}
|
|
79
|
+
if (notification?.body) {
|
|
80
|
+
bytes += Buffer.byteLength(notification.body, "utf8");
|
|
81
|
+
}
|
|
82
|
+
if (notification?.imageUrl) {
|
|
83
|
+
bytes += Buffer.byteLength(notification.imageUrl, "utf8");
|
|
84
|
+
}
|
|
85
|
+
if (message.data) {
|
|
86
|
+
for (const [key, value] of Object.entries(message.data)) {
|
|
87
|
+
bytes += Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (message.collapseKey) {
|
|
91
|
+
bytes += Buffer.byteLength(message.collapseKey, "utf8");
|
|
92
|
+
}
|
|
93
|
+
if (message.messageId) {
|
|
94
|
+
bytes += Buffer.byteLength(message.messageId, "utf8");
|
|
95
|
+
}
|
|
96
|
+
return bytes;
|
|
97
|
+
}
|
|
98
|
+
function validatePayload(message, options = {}) {
|
|
99
|
+
const maxPayloadBytes = options.maxPayloadBytes ?? 4096;
|
|
100
|
+
const titleMax = options.titleMax ?? DEFAULT_TITLE_MAX;
|
|
101
|
+
const bodyMax = options.bodyMax ?? DEFAULT_BODY_MAX;
|
|
102
|
+
const title = message.notification?.title;
|
|
103
|
+
const body = message.notification?.body;
|
|
104
|
+
if (title && title.length > titleMax) {
|
|
105
|
+
throw new FcmPayloadError(`Notification title exceeds ${titleMax} characters`);
|
|
106
|
+
}
|
|
107
|
+
if (body && body.length > bodyMax) {
|
|
108
|
+
throw new FcmPayloadError(`Notification body exceeds ${bodyMax} characters`);
|
|
109
|
+
}
|
|
110
|
+
const estimated = estimatePayloadBytes(message);
|
|
111
|
+
if (estimated > maxPayloadBytes) {
|
|
112
|
+
throw new FcmPayloadError(
|
|
113
|
+
`Push payload estimated at ${estimated} bytes exceeds limit of ${maxPayloadBytes} bytes`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/batch.ts
|
|
119
|
+
var FCM_MULTICAST_BATCH_SIZE = 500;
|
|
120
|
+
function dedupeTokens(tokens) {
|
|
121
|
+
const seen = /* @__PURE__ */ new Set();
|
|
122
|
+
const result = [];
|
|
123
|
+
for (const raw of tokens) {
|
|
124
|
+
const token = raw?.trim();
|
|
125
|
+
if (!token || seen.has(token)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
seen.add(token);
|
|
129
|
+
result.push(token);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function chunkTokens(tokens, batchSize = FCM_MULTICAST_BATCH_SIZE) {
|
|
134
|
+
const chunks = [];
|
|
135
|
+
for (let i = 0; i < tokens.length; i += batchSize) {
|
|
136
|
+
chunks.push(tokens.slice(i, i + batchSize));
|
|
137
|
+
}
|
|
138
|
+
return chunks;
|
|
139
|
+
}
|
|
140
|
+
function mergeMulticastResults(results) {
|
|
141
|
+
const merged = {
|
|
142
|
+
successCount: 0,
|
|
143
|
+
failureCount: 0,
|
|
144
|
+
invalidTokens: [],
|
|
145
|
+
transientFailures: [],
|
|
146
|
+
tokenFailures: []
|
|
147
|
+
};
|
|
148
|
+
const invalidSet = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const result of results) {
|
|
150
|
+
merged.successCount += result.successCount;
|
|
151
|
+
merged.failureCount += result.failureCount;
|
|
152
|
+
for (const token of result.invalidTokens) {
|
|
153
|
+
if (!invalidSet.has(token)) {
|
|
154
|
+
invalidSet.add(token);
|
|
155
|
+
merged.invalidTokens.push(token);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
merged.transientFailures.push(...result.transientFailures);
|
|
159
|
+
merged.tokenFailures.push(...result.tokenFailures);
|
|
160
|
+
}
|
|
161
|
+
return merged;
|
|
162
|
+
}
|
|
163
|
+
function emptyMulticastResult() {
|
|
164
|
+
return {
|
|
165
|
+
successCount: 0,
|
|
166
|
+
failureCount: 0,
|
|
167
|
+
invalidTokens: [],
|
|
168
|
+
transientFailures: [],
|
|
169
|
+
tokenFailures: []
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function appendTokenFailure(target, failure) {
|
|
173
|
+
target.tokenFailures.push(failure);
|
|
174
|
+
target.failureCount += 1;
|
|
175
|
+
if (failure.permanent) {
|
|
176
|
+
target.invalidTokens.push(failure.token);
|
|
177
|
+
} else {
|
|
178
|
+
target.transientFailures.push(failure);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/retry.ts
|
|
183
|
+
var PERMANENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
184
|
+
"invalid-registration-token",
|
|
185
|
+
"registration-token-not-registered",
|
|
186
|
+
"messaging/registration-token-not-registered",
|
|
187
|
+
"messaging/invalid-registration-token",
|
|
188
|
+
"not-registered",
|
|
189
|
+
"permission-denied",
|
|
190
|
+
"messaging/permission-denied",
|
|
191
|
+
"sender-id-mismatch",
|
|
192
|
+
"messaging/sender-id-mismatch",
|
|
193
|
+
"invalid-argument",
|
|
194
|
+
"messaging/invalid-argument"
|
|
195
|
+
]);
|
|
196
|
+
var TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
197
|
+
"unavailable",
|
|
198
|
+
"messaging/unavailable",
|
|
199
|
+
"internal",
|
|
200
|
+
"messaging/internal",
|
|
201
|
+
"resource-exhausted",
|
|
202
|
+
"messaging/resource-exhausted",
|
|
203
|
+
"deadline-exceeded",
|
|
204
|
+
"messaging/deadline-exceeded"
|
|
205
|
+
]);
|
|
206
|
+
function extractErrorCode(err) {
|
|
207
|
+
if (!err || typeof err !== "object") {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
const e = err;
|
|
211
|
+
return e.code || e.errorInfo?.code;
|
|
212
|
+
}
|
|
213
|
+
function extractMessage(err) {
|
|
214
|
+
if (err instanceof Error) {
|
|
215
|
+
return err.message;
|
|
216
|
+
}
|
|
217
|
+
return String(err);
|
|
218
|
+
}
|
|
219
|
+
function isPermanentTokenError(err) {
|
|
220
|
+
const code = (extractErrorCode(err) || "").toLowerCase();
|
|
221
|
+
if (PERMANENT_ERROR_CODES.has(code)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const msg = extractMessage(err).toLowerCase();
|
|
225
|
+
return msg.includes("not-registered") || msg.includes("invalid-registration-token") || msg.includes("registration-token-not-registered");
|
|
226
|
+
}
|
|
227
|
+
function isTransientError(err) {
|
|
228
|
+
const code = (extractErrorCode(err) || "").toLowerCase();
|
|
229
|
+
if (TRANSIENT_ERROR_CODES.has(code)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (!err || typeof err !== "object") {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const e = err;
|
|
236
|
+
if (typeof e.status === "number" && e.status >= 500) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
const msg = String(e.message || e.code || "").toLowerCase();
|
|
240
|
+
return msg.includes("timeout") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("network") || msg.includes("socket") || msg.includes("unavailable");
|
|
241
|
+
}
|
|
242
|
+
function shouldRetryError(err) {
|
|
243
|
+
if (isPermanentTokenError(err)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return isTransientError(err);
|
|
247
|
+
}
|
|
248
|
+
function classifyTokenError(err) {
|
|
249
|
+
const errorCode = extractErrorCode(err);
|
|
250
|
+
const message = extractMessage(err);
|
|
251
|
+
const permanent = isPermanentTokenError(err) || !isTransientError(err) && !shouldRetryError(err);
|
|
252
|
+
return { permanent, errorCode, message };
|
|
253
|
+
}
|
|
254
|
+
async function sleep(ms) {
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/client.ts
|
|
259
|
+
function buildAdminMessage(message, token) {
|
|
260
|
+
const payload = {};
|
|
261
|
+
if (message.notification) {
|
|
262
|
+
payload.notification = {
|
|
263
|
+
title: message.notification.title,
|
|
264
|
+
body: message.notification.body,
|
|
265
|
+
imageUrl: message.notification.imageUrl
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (message.data) {
|
|
269
|
+
payload.data = message.data;
|
|
270
|
+
}
|
|
271
|
+
if (message.webpush) {
|
|
272
|
+
payload.webpush = message.webpush;
|
|
273
|
+
}
|
|
274
|
+
if (message.android) {
|
|
275
|
+
payload.android = message.android;
|
|
276
|
+
}
|
|
277
|
+
if (message.apns) {
|
|
278
|
+
payload.apns = message.apns;
|
|
279
|
+
}
|
|
280
|
+
if (message.collapseKey) {
|
|
281
|
+
payload.collapseKey = message.collapseKey;
|
|
282
|
+
}
|
|
283
|
+
if (token) {
|
|
284
|
+
payload.token = token;
|
|
285
|
+
}
|
|
286
|
+
if (message.ttlSeconds !== void 0) {
|
|
287
|
+
payload.android = {
|
|
288
|
+
...payload.android || {},
|
|
289
|
+
ttl: message.ttlSeconds * 1e3
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return payload;
|
|
293
|
+
}
|
|
294
|
+
var FcmClient = class {
|
|
295
|
+
constructor(config, messagingClient, app) {
|
|
296
|
+
this.closed = false;
|
|
297
|
+
try {
|
|
298
|
+
this.resolved = resolveConfig(config);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
throw new FcmConfigError(err.message);
|
|
301
|
+
}
|
|
302
|
+
if (messagingClient) {
|
|
303
|
+
this.messaging = messagingClient;
|
|
304
|
+
this.app = app ?? null;
|
|
305
|
+
} else {
|
|
306
|
+
const existing = getApps().find((a) => a.name === "multivendor-notification");
|
|
307
|
+
this.app = existing ?? initializeApp({
|
|
308
|
+
credential: cert({
|
|
309
|
+
projectId: this.resolved.projectId,
|
|
310
|
+
clientEmail: this.resolved.clientEmail,
|
|
311
|
+
privateKey: this.resolved.privateKey
|
|
312
|
+
}),
|
|
313
|
+
projectId: this.resolved.projectId
|
|
314
|
+
}, "multivendor-notification");
|
|
315
|
+
this.messaging = getMessaging(this.app);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
getConfig() {
|
|
319
|
+
return this.resolved;
|
|
320
|
+
}
|
|
321
|
+
async validate() {
|
|
322
|
+
resolveConfig(this.resolved);
|
|
323
|
+
}
|
|
324
|
+
async close() {
|
|
325
|
+
this.closed = true;
|
|
326
|
+
if (this.app) {
|
|
327
|
+
await deleteApp(this.app).catch(() => void 0);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async send(message) {
|
|
331
|
+
return this.sendToToken({ token: message.token, message });
|
|
332
|
+
}
|
|
333
|
+
async sendToToken(params) {
|
|
334
|
+
this.assertOpen();
|
|
335
|
+
const token = params.token?.trim();
|
|
336
|
+
if (!token) {
|
|
337
|
+
throw new FcmConfigError("FCM token is required");
|
|
338
|
+
}
|
|
339
|
+
validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });
|
|
340
|
+
let lastError;
|
|
341
|
+
const maxAttempts = 2;
|
|
342
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
343
|
+
try {
|
|
344
|
+
const messageId = await this.messaging.send(
|
|
345
|
+
buildAdminMessage(params.message, token)
|
|
346
|
+
);
|
|
347
|
+
return { messageId, attemptCount: attempt };
|
|
348
|
+
} catch (err) {
|
|
349
|
+
lastError = err;
|
|
350
|
+
if (isPermanentTokenFailure(err) || !shouldRetryError(err) || attempt >= maxAttempts) {
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
await sleep(this.resolved.retryBackoffMs);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const classified = classifyTokenError(lastError);
|
|
357
|
+
throw new FcmApiError(classified.message, {
|
|
358
|
+
errorCode: classified.errorCode,
|
|
359
|
+
attemptCount: maxAttempts,
|
|
360
|
+
details: lastError
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
async sendMulticast(params) {
|
|
364
|
+
this.assertOpen();
|
|
365
|
+
validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });
|
|
366
|
+
const uniqueTokens = dedupeTokens(params.tokens);
|
|
367
|
+
if (uniqueTokens.length === 0) {
|
|
368
|
+
return emptyMulticastResult();
|
|
369
|
+
}
|
|
370
|
+
const chunks = chunkTokens(uniqueTokens);
|
|
371
|
+
const batchResults = [];
|
|
372
|
+
for (const batch of chunks) {
|
|
373
|
+
batchResults.push(await this.sendMulticastBatch(batch, params.message));
|
|
374
|
+
}
|
|
375
|
+
return mergeMulticastResults(batchResults);
|
|
376
|
+
}
|
|
377
|
+
async sendMulticastBatch(tokens, message) {
|
|
378
|
+
const result = emptyMulticastResult();
|
|
379
|
+
let lastBatchError;
|
|
380
|
+
const maxAttempts = 2;
|
|
381
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
382
|
+
try {
|
|
383
|
+
const response = await this.messaging.sendEachForMulticast({
|
|
384
|
+
tokens,
|
|
385
|
+
notification: message.notification ? {
|
|
386
|
+
title: message.notification.title,
|
|
387
|
+
body: message.notification.body,
|
|
388
|
+
imageUrl: message.notification.imageUrl
|
|
389
|
+
} : void 0,
|
|
390
|
+
data: message.data,
|
|
391
|
+
webpush: message.webpush,
|
|
392
|
+
android: message.android,
|
|
393
|
+
apns: message.apns
|
|
394
|
+
});
|
|
395
|
+
result.successCount += response.successCount;
|
|
396
|
+
result.failureCount += response.failureCount;
|
|
397
|
+
response.responses.forEach((res, index) => {
|
|
398
|
+
if (res.success) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const token = tokens[index];
|
|
402
|
+
const classified2 = classifyTokenError(res.error);
|
|
403
|
+
const failure = {
|
|
404
|
+
token,
|
|
405
|
+
errorCode: classified2.errorCode,
|
|
406
|
+
message: classified2.message,
|
|
407
|
+
permanent: classified2.permanent
|
|
408
|
+
};
|
|
409
|
+
appendTokenFailure(result, failure);
|
|
410
|
+
});
|
|
411
|
+
return result;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
lastBatchError = err;
|
|
414
|
+
if (!shouldRetryError(err) || attempt >= maxAttempts) {
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
await sleep(this.resolved.retryBackoffMs);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const classified = classifyTokenError(lastBatchError);
|
|
421
|
+
if (!classified.permanent && shouldRetryError(lastBatchError)) {
|
|
422
|
+
for (const token of tokens) {
|
|
423
|
+
appendTokenFailure(result, {
|
|
424
|
+
token,
|
|
425
|
+
errorCode: classified.errorCode,
|
|
426
|
+
message: classified.message,
|
|
427
|
+
permanent: false
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
throw new FcmApiError(classified.message, {
|
|
433
|
+
errorCode: classified.errorCode,
|
|
434
|
+
attemptCount: maxAttempts,
|
|
435
|
+
details: lastBatchError
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
assertOpen() {
|
|
439
|
+
if (this.closed) {
|
|
440
|
+
throw new FcmConfigError("FcmClient is closed");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
function isPermanentTokenFailure(err) {
|
|
445
|
+
return classifyTokenError(err).permanent;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export { FCM_MULTICAST_BATCH_SIZE, FcmApiError, FcmClient, FcmConfigError, FcmPayloadError, chunkTokens, classifyTokenError, dedupeTokens, estimatePayloadBytes, isPermanentTokenError, isTransientError, isValidPemPrivateKey, mergeMulticastResults, resolveConfig, shouldRetryError, validatePayload };
|
|
449
|
+
//# sourceMappingURL=index.js.map
|
|
450
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/config.ts","../src/validate.ts","../src/batch.ts","../src/retry.ts","../src/client.ts"],"names":["classified"],"mappings":"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","file":"index.js","sourcesContent":["export class FcmConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'FcmConfigError';\n }\n}\n\nexport class FcmPayloadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'FcmPayloadError';\n }\n}\n\nexport class FcmApiError extends Error {\n public readonly statusCode?: number;\n public readonly errorCode?: string;\n public readonly attemptCount: number;\n public readonly details?: unknown;\n\n constructor(\n message: string,\n options: { statusCode?: number; errorCode?: string; attemptCount?: number; details?: unknown } = {},\n ) {\n super(message);\n this.name = 'FcmApiError';\n this.statusCode = options.statusCode;\n this.errorCode = options.errorCode;\n this.attemptCount = options.attemptCount ?? 1;\n this.details = options.details;\n }\n}\n","import { FcmConfigError } from './errors';\n\nexport interface FcmConfig {\n projectId?: string;\n clientEmail?: string;\n privateKey?: string;\n retryBackoffMs?: number;\n maxPayloadBytes?: number;\n}\n\nexport interface ResolvedFcmConfig {\n projectId: string;\n clientEmail: string;\n privateKey: string;\n retryBackoffMs: number;\n maxPayloadBytes: number;\n}\n\nconst PEM_HEADER = '-----BEGIN PRIVATE KEY-----';\nconst PEM_FOOTER = '-----END PRIVATE KEY-----';\n\nfunction normalizePrivateKey(raw: string): string {\n const trimmed = raw.trim();\n if (trimmed.includes('\\\\n')) {\n return trimmed.replace(/\\\\n/g, '\\n');\n }\n return trimmed;\n}\n\nexport function isValidPemPrivateKey(key: string): boolean {\n const normalized = normalizePrivateKey(key);\n return normalized.includes(PEM_HEADER) && normalized.includes(PEM_FOOTER);\n}\n\nexport function resolveConfig(config: FcmConfig): ResolvedFcmConfig {\n const projectId = config.projectId?.trim() || '';\n const clientEmail = config.clientEmail?.trim() || '';\n const privateKeyRaw = config.privateKey?.trim() || '';\n\n if (!projectId) {\n throw new FcmConfigError('FCM projectId is required');\n }\n if (!clientEmail || !clientEmail.includes('@')) {\n throw new FcmConfigError('FCM clientEmail must be a valid service account email');\n }\n if (!privateKeyRaw) {\n throw new FcmConfigError('FCM privateKey is required');\n }\n\n const privateKey = normalizePrivateKey(privateKeyRaw);\n if (!isValidPemPrivateKey(privateKey)) {\n throw new FcmConfigError('FCM privateKey must be a valid PEM private key');\n }\n\n return {\n projectId,\n clientEmail,\n privateKey,\n retryBackoffMs: config.retryBackoffMs ?? 250,\n maxPayloadBytes: config.maxPayloadBytes ?? 4096,\n };\n}\n","import { FcmPayloadError } from './errors';\nimport type { PushMessage } from './types';\n\nconst DEFAULT_TITLE_MAX = 200;\nconst DEFAULT_BODY_MAX = 1000;\n\nexport function estimatePayloadBytes(message: PushMessage): number {\n let bytes = 0;\n const notification = message.notification;\n if (notification?.title) {\n bytes += Buffer.byteLength(notification.title, 'utf8');\n }\n if (notification?.body) {\n bytes += Buffer.byteLength(notification.body, 'utf8');\n }\n if (notification?.imageUrl) {\n bytes += Buffer.byteLength(notification.imageUrl, 'utf8');\n }\n if (message.data) {\n for (const [key, value] of Object.entries(message.data)) {\n bytes += Buffer.byteLength(key, 'utf8') + Buffer.byteLength(value, 'utf8');\n }\n }\n if (message.collapseKey) {\n bytes += Buffer.byteLength(message.collapseKey, 'utf8');\n }\n if (message.messageId) {\n bytes += Buffer.byteLength(message.messageId, 'utf8');\n }\n return bytes;\n}\n\nexport function validatePayload(\n message: PushMessage,\n options: { maxPayloadBytes?: number; titleMax?: number; bodyMax?: number } = {},\n): void {\n const maxPayloadBytes = options.maxPayloadBytes ?? 4096;\n const titleMax = options.titleMax ?? DEFAULT_TITLE_MAX;\n const bodyMax = options.bodyMax ?? DEFAULT_BODY_MAX;\n\n const title = message.notification?.title;\n const body = message.notification?.body;\n\n if (title && title.length > titleMax) {\n throw new FcmPayloadError(`Notification title exceeds ${titleMax} characters`);\n }\n if (body && body.length > bodyMax) {\n throw new FcmPayloadError(`Notification body exceeds ${bodyMax} characters`);\n }\n\n const estimated = estimatePayloadBytes(message);\n if (estimated > maxPayloadBytes) {\n throw new FcmPayloadError(\n `Push payload estimated at ${estimated} bytes exceeds limit of ${maxPayloadBytes} bytes`,\n );\n }\n}\n","import type { MulticastResult, TokenSendFailure } from './types';\n\nexport const FCM_MULTICAST_BATCH_SIZE = 500;\n\nexport function dedupeTokens(tokens: string[]): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const raw of tokens) {\n const token = raw?.trim();\n if (!token || seen.has(token)) {\n continue;\n }\n seen.add(token);\n result.push(token);\n }\n return result;\n}\n\nexport function chunkTokens(tokens: string[], batchSize: number = FCM_MULTICAST_BATCH_SIZE): string[][] {\n const chunks: string[][] = [];\n for (let i = 0; i < tokens.length; i += batchSize) {\n chunks.push(tokens.slice(i, i + batchSize));\n }\n return chunks;\n}\n\nexport function mergeMulticastResults(results: MulticastResult[]): MulticastResult {\n const merged: MulticastResult = {\n successCount: 0,\n failureCount: 0,\n invalidTokens: [],\n transientFailures: [],\n tokenFailures: [],\n };\n\n const invalidSet = new Set<string>();\n\n for (const result of results) {\n merged.successCount += result.successCount;\n merged.failureCount += result.failureCount;\n for (const token of result.invalidTokens) {\n if (!invalidSet.has(token)) {\n invalidSet.add(token);\n merged.invalidTokens.push(token);\n }\n }\n merged.transientFailures.push(...result.transientFailures);\n merged.tokenFailures.push(...result.tokenFailures);\n }\n\n return merged;\n}\n\nexport function emptyMulticastResult(): MulticastResult {\n return {\n successCount: 0,\n failureCount: 0,\n invalidTokens: [],\n transientFailures: [],\n tokenFailures: [],\n };\n}\n\nexport function appendTokenFailure(\n target: MulticastResult,\n failure: TokenSendFailure,\n): void {\n target.tokenFailures.push(failure);\n target.failureCount += 1;\n if (failure.permanent) {\n target.invalidTokens.push(failure.token);\n } else {\n target.transientFailures.push(failure);\n }\n}\n","const PERMANENT_ERROR_CODES = new Set([\n 'invalid-registration-token',\n 'registration-token-not-registered',\n 'messaging/registration-token-not-registered',\n 'messaging/invalid-registration-token',\n 'not-registered',\n 'permission-denied',\n 'messaging/permission-denied',\n 'sender-id-mismatch',\n 'messaging/sender-id-mismatch',\n 'invalid-argument',\n 'messaging/invalid-argument',\n]);\n\nconst TRANSIENT_ERROR_CODES = new Set([\n 'unavailable',\n 'messaging/unavailable',\n 'internal',\n 'messaging/internal',\n 'resource-exhausted',\n 'messaging/resource-exhausted',\n 'deadline-exceeded',\n 'messaging/deadline-exceeded',\n]);\n\nfunction extractErrorCode(err: unknown): string | undefined {\n if (!err || typeof err !== 'object') {\n return undefined;\n }\n const e = err as { code?: string; errorInfo?: { code?: string } };\n return e.code || e.errorInfo?.code;\n}\n\nfunction extractMessage(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\nexport function isPermanentTokenError(err: unknown): boolean {\n const code = (extractErrorCode(err) || '').toLowerCase();\n if (PERMANENT_ERROR_CODES.has(code)) {\n return true;\n }\n const msg = extractMessage(err).toLowerCase();\n return msg.includes('not-registered')\n || msg.includes('invalid-registration-token')\n || msg.includes('registration-token-not-registered');\n}\n\nexport function isTransientError(err: unknown): boolean {\n const code = (extractErrorCode(err) || '').toLowerCase();\n if (TRANSIENT_ERROR_CODES.has(code)) {\n return true;\n }\n if (!err || typeof err !== 'object') {\n return false;\n }\n const e = err as { status?: number; message?: string; code?: string };\n if (typeof e.status === 'number' && e.status >= 500) {\n return true;\n }\n const msg = String(e.message || e.code || '').toLowerCase();\n return msg.includes('timeout')\n || msg.includes('econnreset')\n || msg.includes('enotfound')\n || msg.includes('network')\n || msg.includes('socket')\n || msg.includes('unavailable');\n}\n\nexport function shouldRetryError(err: unknown): boolean {\n if (isPermanentTokenError(err)) {\n return false;\n }\n return isTransientError(err);\n}\n\nexport function classifyTokenError(err: unknown): { permanent: boolean; errorCode?: string; message: string } {\n const errorCode = extractErrorCode(err);\n const message = extractMessage(err);\n const permanent = isPermanentTokenError(err) || (!isTransientError(err) && !shouldRetryError(err));\n return { permanent, errorCode, message };\n}\n\nexport async function sleep(ms: number): Promise<void> {\n await new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n","import type { Message, Messaging } from 'firebase-admin/messaging';\nimport { initializeApp, cert, getApps, deleteApp, type App } from 'firebase-admin/app';\nimport { getMessaging } from 'firebase-admin/messaging';\nimport { resolveConfig, type FcmConfig, type ResolvedFcmConfig } from './config';\nimport { FcmApiError, FcmConfigError } from './errors';\nimport { validatePayload } from './validate';\nimport {\n appendTokenFailure,\n chunkTokens,\n dedupeTokens,\n emptyMulticastResult,\n mergeMulticastResults,\n} from './batch';\nimport {\n classifyTokenError,\n shouldRetryError,\n sleep,\n} from './retry';\nimport type {\n MulticastResult,\n PushMessage,\n SendMulticastParams,\n SendResult,\n SendToTokenParams,\n TokenSendFailure,\n} from './types';\n\nexport type FcmMessagingClient = Pick<Messaging, 'send' | 'sendEachForMulticast'>;\n\nfunction buildAdminMessage(message: PushMessage, token?: string): Record<string, unknown> {\n const payload: Record<string, unknown> = {};\n if (message.notification) {\n payload.notification = {\n title: message.notification.title,\n body: message.notification.body,\n imageUrl: message.notification.imageUrl,\n };\n }\n if (message.data) {\n payload.data = message.data;\n }\n if (message.webpush) {\n payload.webpush = message.webpush;\n }\n if (message.android) {\n payload.android = message.android;\n }\n if (message.apns) {\n payload.apns = message.apns;\n }\n if (message.collapseKey) {\n payload.collapseKey = message.collapseKey;\n }\n if (token) {\n payload.token = token;\n }\n if (message.ttlSeconds !== undefined) {\n payload.android = {\n ...(payload.android as Record<string, unknown> || {}),\n ttl: message.ttlSeconds * 1000,\n };\n }\n return payload;\n}\n\nexport class FcmClient {\n private readonly resolved: ResolvedFcmConfig;\n private readonly messaging: FcmMessagingClient;\n private readonly app: App | null;\n private closed = false;\n\n constructor(config: FcmConfig, messagingClient?: FcmMessagingClient, app?: App) {\n try {\n this.resolved = resolveConfig(config);\n } catch (err) {\n throw new FcmConfigError((err as Error).message);\n }\n\n if (messagingClient) {\n this.messaging = messagingClient;\n this.app = app ?? null;\n } else {\n const existing = getApps().find((a) => a.name === 'multivendor-notification');\n this.app = existing ?? initializeApp({\n credential: cert({\n projectId: this.resolved.projectId,\n clientEmail: this.resolved.clientEmail,\n privateKey: this.resolved.privateKey,\n }),\n projectId: this.resolved.projectId,\n }, 'multivendor-notification');\n this.messaging = getMessaging(this.app);\n }\n }\n\n getConfig(): ResolvedFcmConfig {\n return this.resolved;\n }\n\n async validate(): Promise<void> {\n resolveConfig(this.resolved);\n }\n\n async close(): Promise<void> {\n this.closed = true;\n if (this.app) {\n await deleteApp(this.app).catch(() => undefined);\n }\n }\n\n async send(message: PushMessage & { token: string }): Promise<SendResult> {\n return this.sendToToken({ token: message.token, message });\n }\n\n async sendToToken(params: SendToTokenParams): Promise<SendResult> {\n this.assertOpen();\n const token = params.token?.trim();\n if (!token) {\n throw new FcmConfigError('FCM token is required');\n }\n\n validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });\n\n let lastError: unknown;\n const maxAttempts = 2;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const messageId = await this.messaging.send(\n buildAdminMessage(params.message, token) as unknown as Message,\n );\n return { messageId, attemptCount: attempt };\n } catch (err) {\n lastError = err;\n if (isPermanentTokenFailure(err) || !shouldRetryError(err) || attempt >= maxAttempts) {\n break;\n }\n await sleep(this.resolved.retryBackoffMs);\n }\n }\n\n const classified = classifyTokenError(lastError);\n throw new FcmApiError(classified.message, {\n errorCode: classified.errorCode,\n attemptCount: maxAttempts,\n details: lastError,\n });\n }\n\n async sendMulticast(params: SendMulticastParams): Promise<MulticastResult> {\n this.assertOpen();\n validatePayload(params.message, { maxPayloadBytes: this.resolved.maxPayloadBytes });\n\n const uniqueTokens = dedupeTokens(params.tokens);\n if (uniqueTokens.length === 0) {\n return emptyMulticastResult();\n }\n\n const chunks = chunkTokens(uniqueTokens);\n const batchResults: MulticastResult[] = [];\n\n for (const batch of chunks) {\n batchResults.push(await this.sendMulticastBatch(batch, params.message));\n }\n\n return mergeMulticastResults(batchResults);\n }\n\n private async sendMulticastBatch(tokens: string[], message: PushMessage): Promise<MulticastResult> {\n const result = emptyMulticastResult();\n\n let lastBatchError: unknown;\n const maxAttempts = 2;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const response = await this.messaging.sendEachForMulticast({\n tokens,\n notification: message.notification\n ? {\n title: message.notification.title,\n body: message.notification.body,\n imageUrl: message.notification.imageUrl,\n }\n : undefined,\n data: message.data,\n webpush: message.webpush as never,\n android: message.android as never,\n apns: message.apns as never,\n });\n\n result.successCount += response.successCount;\n result.failureCount += response.failureCount;\n\n response.responses.forEach((res, index) => {\n if (res.success) {\n return;\n }\n const token = tokens[index];\n const classified = classifyTokenError(res.error);\n const failure: TokenSendFailure = {\n token,\n errorCode: classified.errorCode,\n message: classified.message,\n permanent: classified.permanent,\n };\n appendTokenFailure(result, failure);\n });\n\n return result;\n } catch (err) {\n lastBatchError = err;\n if (!shouldRetryError(err) || attempt >= maxAttempts) {\n break;\n }\n await sleep(this.resolved.retryBackoffMs);\n }\n }\n\n const classified = classifyTokenError(lastBatchError);\n if (!classified.permanent && shouldRetryError(lastBatchError)) {\n for (const token of tokens) {\n appendTokenFailure(result, {\n token,\n errorCode: classified.errorCode,\n message: classified.message,\n permanent: false,\n });\n }\n return result;\n }\n\n throw new FcmApiError(classified.message, {\n errorCode: classified.errorCode,\n attemptCount: maxAttempts,\n details: lastBatchError,\n });\n }\n\n private assertOpen(): void {\n if (this.closed) {\n throw new FcmConfigError('FcmClient is closed');\n }\n }\n}\n\nfunction isPermanentTokenFailure(err: unknown): boolean {\n return classifyTokenError(err).permanent;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "multivendor-notification",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Framework-agnostic FCM push notification client with batching, retry, and payload validation for multivendor SaaS apps",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "SmartByteLabs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/SmartByteLabs/multivendor-saas-components.git",
|
|
10
|
+
"directory": "multivendor-notification"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"import": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"require": {
|
|
25
|
+
"types": "./dist/index.d.cts",
|
|
26
|
+
"default": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"firebase-admin": "^13.0.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.17.19",
|
|
38
|
+
"tsup": "^8.4.0",
|
|
39
|
+
"typescript": "^5.7.3",
|
|
40
|
+
"vitest": "^3.0.5"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"fcm",
|
|
44
|
+
"firebase",
|
|
45
|
+
"push",
|
|
46
|
+
"notification",
|
|
47
|
+
"multivendor"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest"
|
|
53
|
+
}
|
|
54
|
+
}
|