@arkstack/notifications 0.16.9 → 0.16.11
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.d.ts +44 -12
- package/dist/index.js +66 -26
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ type FirebaseTransportConfig = {
|
|
|
67
67
|
project_id?: string;
|
|
68
68
|
client_email?: string;
|
|
69
69
|
private_key?: string;
|
|
70
|
+
app_name?: string;
|
|
71
|
+
admin_sdk_path?: string;
|
|
70
72
|
};
|
|
71
73
|
type RealtimeDriverOptions = {
|
|
72
74
|
transport?: RealtimeDriverName; /** Channel/topic to broadcast on. Defaults to `${channel_prefix}${user.id}`. */
|
|
@@ -90,7 +92,7 @@ type RealtimeNotificationPayload = {
|
|
|
90
92
|
};
|
|
91
93
|
/** The result of a realtime broadcast (plus the stored record when `store` is on). */
|
|
92
94
|
type RealtimeBroadcastResult = {
|
|
93
|
-
channel: string;
|
|
95
|
+
channel: string | string[];
|
|
94
96
|
event: string;
|
|
95
97
|
payload: RealtimeNotificationPayload;
|
|
96
98
|
stored?: UserNotification;
|
|
@@ -186,6 +188,10 @@ interface NotificationConfig {
|
|
|
186
188
|
project_id: string;
|
|
187
189
|
client_email: string;
|
|
188
190
|
private_key: string;
|
|
191
|
+
app_name?: string;
|
|
192
|
+
} | {
|
|
193
|
+
app_name?: string;
|
|
194
|
+
admin_sdk_path: string;
|
|
189
195
|
};
|
|
190
196
|
};
|
|
191
197
|
}
|
|
@@ -290,9 +296,13 @@ declare class MailNotification extends NotificationContract {
|
|
|
290
296
|
/**
|
|
291
297
|
* A realtime transport (Pusher, Firebase, …) broadcasts a notification payload
|
|
292
298
|
* to a channel/topic that connected clients subscribe to.
|
|
299
|
+
*
|
|
300
|
+
* `channel` may be an array: for Pusher it fans out to multiple channels, and
|
|
301
|
+
* for Firebase it is treated as a list of device registration tokens delivered
|
|
302
|
+
* via a multicast send.
|
|
293
303
|
*/
|
|
294
304
|
interface RealtimeDriver {
|
|
295
|
-
broadcast(channel: string, event: string, payload: RealtimeNotificationPayload): Promise<unknown>;
|
|
305
|
+
broadcast(channel: string | string[], event: string, payload: RealtimeNotificationPayload): Promise<unknown>;
|
|
296
306
|
}
|
|
297
307
|
//#endregion
|
|
298
308
|
//#region src/drivers/RealtimeNotification.d.ts
|
|
@@ -316,7 +326,8 @@ declare class RealtimeNotification extends NotificationContract<RealtimeBroadcas
|
|
|
316
326
|
from(_from: string): this;
|
|
317
327
|
subject(subject: string): this;
|
|
318
328
|
/**
|
|
319
|
-
* Set the recipient: a `User` (derives the channel)
|
|
329
|
+
* Set the recipient: a `User` (derives the channel), an explicit channel
|
|
330
|
+
* string, or an array of channels (Pusher) / device tokens (Firebase).
|
|
320
331
|
*
|
|
321
332
|
* @param recipient
|
|
322
333
|
* @returns
|
|
@@ -324,11 +335,14 @@ declare class RealtimeNotification extends NotificationContract<RealtimeBroadcas
|
|
|
324
335
|
recipient(recipient: NotificationRecipient | User): this;
|
|
325
336
|
/**
|
|
326
337
|
* Broadcast on an explicit channel/topic instead of the per-user default.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
338
|
+
* An array broadcasts to multiple Pusher channels, or — for Firebase — to a
|
|
339
|
+
* list of device registration tokens (multicast).
|
|
340
|
+
*
|
|
341
|
+
* @param channel
|
|
342
|
+
* @returns
|
|
343
|
+
*/
|
|
331
344
|
channel(channel: string): this;
|
|
345
|
+
channel(channel: string[]): this;
|
|
332
346
|
/**
|
|
333
347
|
* The event name clients subscribe to (default `notification`).
|
|
334
348
|
*
|
|
@@ -430,13 +444,26 @@ declare class PusherRealtimeDriver implements RealtimeDriver {
|
|
|
430
444
|
private clientPromise?;
|
|
431
445
|
constructor(options?: PusherTransportConfig);
|
|
432
446
|
private client;
|
|
433
|
-
broadcast(channel: string, event: string, payload: RealtimeNotificationPayload): Promise<unknown>;
|
|
447
|
+
broadcast(channel: string | string[], event: string, payload: RealtimeNotificationPayload): Promise<unknown>;
|
|
434
448
|
}
|
|
435
449
|
//#endregion
|
|
436
450
|
//#region src/drivers/realtime/FirebaseRealtimeDriver.d.ts
|
|
437
451
|
/**
|
|
438
|
-
*
|
|
439
|
-
|
|
452
|
+
* The outcome of a token multicast: totals plus the tokens FCM rejected as dead.
|
|
453
|
+
*/
|
|
454
|
+
interface FirebaseMulticastResult {
|
|
455
|
+
successCount: number;
|
|
456
|
+
failureCount: number;
|
|
457
|
+
/**
|
|
458
|
+
* Tokens FCM reported as unregistered/invalid — delete these from your store.
|
|
459
|
+
*/
|
|
460
|
+
invalidTokens: string[];
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Broadcasts notifications over [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging).
|
|
464
|
+
* A single channel maps to an FCM topic; an array of channels is treated as
|
|
465
|
+
* device registration tokens and delivered via a multicast send (chunked to
|
|
466
|
+
* FCM's 500-token limit), returning the tokens that should be pruned.
|
|
440
467
|
*
|
|
441
468
|
* `firebase-admin` is an optional peer dependency, imported lazily so the
|
|
442
469
|
* package installs without it; it is only required when this transport is used.
|
|
@@ -447,10 +474,15 @@ declare class FirebaseRealtimeDriver implements RealtimeDriver {
|
|
|
447
474
|
private messagingPromise?;
|
|
448
475
|
constructor(options?: FirebaseTransportConfig);
|
|
449
476
|
private messaging;
|
|
450
|
-
broadcast(channel: string, event: string, payload: RealtimeNotificationPayload): Promise<string>;
|
|
477
|
+
broadcast(channel: string | string[], event: string, payload: RealtimeNotificationPayload): Promise<string | FirebaseMulticastResult>;
|
|
478
|
+
/**
|
|
479
|
+
* Send to many device tokens at once, chunked to FCM's 500-token limit, and
|
|
480
|
+
* collect the tokens FCM rejects as dead so the caller can prune them.
|
|
481
|
+
*/
|
|
482
|
+
private multicast;
|
|
451
483
|
}
|
|
452
484
|
//#endregion
|
|
453
485
|
//#region src/utils/template.d.ts
|
|
454
486
|
declare const interpolate: (value: string, data?: NotificationData) => string;
|
|
455
487
|
//#endregion
|
|
456
|
-
export { AfricasTalkingSmsDriver, DbNotification, DbNotificationPayload, DbNotificationType, DriverResult, FirebaseRealtimeDriver, FirebaseTransportConfig, MailDriverOptions, MailNotification, MailRecipient, MailRecipientAddress, MergedTransportConfig, Notification, NotificationChannel, NotificationConfig, NotificationContract, NotificationData, NotificationDriverMap, NotificationRecipient, PusherRealtimeDriver, PusherTransportConfig, RealtimeBroadcastResult, RealtimeDriver, RealtimeDriverName, RealtimeDriverOptions, RealtimeNotification, RealtimeNotificationPayload, SmsDriverName, SmsDriverOptions, SmsNotification, TwilioSmsDriver, UserNotification, UserNotificationCenter, configure, interpolate };
|
|
488
|
+
export { AfricasTalkingSmsDriver, DbNotification, DbNotificationPayload, DbNotificationType, DriverResult, FirebaseMulticastResult, FirebaseRealtimeDriver, FirebaseTransportConfig, MailDriverOptions, MailNotification, MailRecipient, MailRecipientAddress, MergedTransportConfig, Notification, NotificationChannel, NotificationConfig, NotificationContract, NotificationData, NotificationDriverMap, NotificationRecipient, PusherRealtimeDriver, PusherTransportConfig, RealtimeBroadcastResult, RealtimeDriver, RealtimeDriverName, RealtimeDriverOptions, RealtimeNotification, RealtimeNotificationPayload, SmsDriverName, SmsDriverOptions, SmsNotification, TwilioSmsDriver, UserNotification, UserNotificationCenter, configure, interpolate };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { config, env, getModel } from "@arkstack/common";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import path, { join } from "node:path";
|
|
4
4
|
import nodemailer from "nodemailer";
|
|
5
5
|
import { Arkstack } from "@arkstack/contract";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
8
|
import africastalking from "africastalking";
|
|
8
9
|
import twilio from "twilio";
|
|
@@ -323,9 +324,19 @@ var MailNotification = class extends NotificationContract {
|
|
|
323
324
|
};
|
|
324
325
|
//#endregion
|
|
325
326
|
//#region src/drivers/realtime/FirebaseRealtimeDriver.ts
|
|
327
|
+
/** FCM caps a multicast at 500 tokens per call. */
|
|
328
|
+
const MULTICAST_LIMIT = 500;
|
|
329
|
+
/** Error codes that mean a token is dead and should be pruned by the app. */
|
|
330
|
+
const DEAD_TOKEN_CODES = /* @__PURE__ */ new Set([
|
|
331
|
+
"messaging/registration-token-not-registered",
|
|
332
|
+
"messaging/invalid-registration-token",
|
|
333
|
+
"messaging/invalid-argument"
|
|
334
|
+
]);
|
|
326
335
|
/**
|
|
327
|
-
* Broadcasts notifications over [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging)
|
|
328
|
-
*
|
|
336
|
+
* Broadcasts notifications over [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging).
|
|
337
|
+
* A single channel maps to an FCM topic; an array of channels is treated as
|
|
338
|
+
* device registration tokens and delivered via a multicast send (chunked to
|
|
339
|
+
* FCM's 500-token limit), returning the tokens that should be pruned.
|
|
329
340
|
*
|
|
330
341
|
* `firebase-admin` is an optional peer dependency, imported lazily so the
|
|
331
342
|
* package installs without it; it is only required when this transport is used.
|
|
@@ -338,16 +349,23 @@ var FirebaseRealtimeDriver = class {
|
|
|
338
349
|
this.options = options;
|
|
339
350
|
}
|
|
340
351
|
messaging() {
|
|
352
|
+
const appSpecifier = "firebase-admin/app";
|
|
353
|
+
const messagingSpecifier = "firebase-admin/messaging";
|
|
341
354
|
this.messagingPromise ??= (async () => {
|
|
342
|
-
const [app, messaging] = await Promise.all([import(
|
|
355
|
+
const [app, messaging] = await Promise.all([import(appSpecifier), import(messagingSpecifier)]).catch(() => {
|
|
343
356
|
throw new Error("The \"firebase-admin\" package is required for the Firebase realtime transport. Install it with `npm i firebase-admin`.");
|
|
344
357
|
});
|
|
345
|
-
const
|
|
358
|
+
const adminsdk = path.join(process.cwd(), this.options.admin_sdk_path ?? env("FIREBASE_ADMINSDK", "firebase-adminsdk.json"));
|
|
359
|
+
let serviceAccount;
|
|
360
|
+
try {
|
|
361
|
+
if (existsSync(adminsdk)) serviceAccount = JSON.parse(readFileSync(adminsdk, { encoding: "utf-8" }));
|
|
362
|
+
} catch {}
|
|
363
|
+
const credential = app.cert(serviceAccount ?? {
|
|
346
364
|
projectId: this.options.project_id ?? env("FIREBASE_PROJECT_ID", ""),
|
|
347
365
|
clientEmail: this.options.client_email ?? env("FIREBASE_CLIENT_EMAIL", ""),
|
|
348
366
|
privateKey: (this.options.private_key ?? env("FIREBASE_PRIVATE_KEY", ""))?.replace(/\\n/g, "\n")
|
|
349
367
|
});
|
|
350
|
-
const name = "arkstack-realtime";
|
|
368
|
+
const name = String(this.options.app_name ?? env("FIREBASE_APP_NAME", "arkstack-realtime")).replaceAll(" ", "-").toLowerCase();
|
|
351
369
|
const instance = app.getApps().find((a) => a.name === name) ?? app.initializeApp({ credential }, name);
|
|
352
370
|
return messaging.getMessaging(instance);
|
|
353
371
|
})();
|
|
@@ -355,15 +373,41 @@ var FirebaseRealtimeDriver = class {
|
|
|
355
373
|
}
|
|
356
374
|
async broadcast(channel, event, payload) {
|
|
357
375
|
const messaging = await this.messaging();
|
|
376
|
+
const data = {
|
|
377
|
+
event,
|
|
378
|
+
payload: JSON.stringify(payload)
|
|
379
|
+
};
|
|
380
|
+
if (Array.isArray(channel)) return await this.multicast(messaging, channel, data);
|
|
358
381
|
const topic = channel.replace(/[^a-zA-Z0-9-_.~%]/g, "_");
|
|
359
382
|
return await messaging.send({
|
|
360
383
|
topic,
|
|
361
|
-
data
|
|
362
|
-
event,
|
|
363
|
-
payload: JSON.stringify(payload)
|
|
364
|
-
}
|
|
384
|
+
data
|
|
365
385
|
});
|
|
366
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Send to many device tokens at once, chunked to FCM's 500-token limit, and
|
|
389
|
+
* collect the tokens FCM rejects as dead so the caller can prune them.
|
|
390
|
+
*/
|
|
391
|
+
async multicast(messaging, tokens, data) {
|
|
392
|
+
const result = {
|
|
393
|
+
successCount: 0,
|
|
394
|
+
failureCount: 0,
|
|
395
|
+
invalidTokens: []
|
|
396
|
+
};
|
|
397
|
+
for (let i = 0; i < tokens.length; i += MULTICAST_LIMIT) {
|
|
398
|
+
const batch = tokens.slice(i, i + MULTICAST_LIMIT);
|
|
399
|
+
const response = await messaging.sendEachForMulticast({
|
|
400
|
+
tokens: batch,
|
|
401
|
+
data
|
|
402
|
+
});
|
|
403
|
+
result.successCount += response.successCount;
|
|
404
|
+
result.failureCount += response.failureCount;
|
|
405
|
+
response.responses.forEach((res, index) => {
|
|
406
|
+
if (!res.success && res.error?.code && DEAD_TOKEN_CODES.has(res.error.code)) result.invalidTokens.push(batch[index]);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
367
411
|
};
|
|
368
412
|
//#endregion
|
|
369
413
|
//#region src/drivers/realtime/PusherRealtimeDriver.ts
|
|
@@ -380,8 +424,9 @@ var PusherRealtimeDriver = class {
|
|
|
380
424
|
this.options = options;
|
|
381
425
|
}
|
|
382
426
|
client() {
|
|
427
|
+
const specifier = "pusher";
|
|
383
428
|
this.clientPromise ??= (async () => {
|
|
384
|
-
const mod = await import(
|
|
429
|
+
const mod = await import(specifier).catch(() => {
|
|
385
430
|
throw new Error("The \"pusher\" package is required for the Pusher realtime transport. Install it with `npm i pusher`.");
|
|
386
431
|
});
|
|
387
432
|
return new (mod.default ?? mod)({
|
|
@@ -440,29 +485,24 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
440
485
|
this.payload.title = subject;
|
|
441
486
|
return this;
|
|
442
487
|
}
|
|
443
|
-
/**
|
|
444
|
-
* Set the recipient: a `User` (derives the channel)
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* @
|
|
488
|
+
/**
|
|
489
|
+
* Set the recipient: a `User` (derives the channel), an explicit channel
|
|
490
|
+
* string, or an array of channels (Pusher) / device tokens (Firebase).
|
|
491
|
+
*
|
|
492
|
+
* @param recipient
|
|
493
|
+
* @returns
|
|
448
494
|
*/
|
|
449
495
|
recipient(recipient) {
|
|
450
|
-
if (typeof recipient === "object" && !Array.isArray(recipient) &&
|
|
496
|
+
if (typeof recipient === "object" && !Array.isArray(recipient) && typeof recipient.id !== "undefined") {
|
|
451
497
|
this.user = recipient;
|
|
452
498
|
return this;
|
|
453
499
|
}
|
|
454
|
-
if (typeof recipient === "string") {
|
|
500
|
+
if (typeof recipient === "string" || Array.isArray(recipient)) {
|
|
455
501
|
this.channelName = recipient;
|
|
456
502
|
return this;
|
|
457
503
|
}
|
|
458
504
|
throw new Error("Realtime notifications require a user recipient or a channel name");
|
|
459
505
|
}
|
|
460
|
-
/**
|
|
461
|
-
* Broadcast on an explicit channel/topic instead of the per-user default.
|
|
462
|
-
*
|
|
463
|
-
* @param channel
|
|
464
|
-
* @returns
|
|
465
|
-
*/
|
|
466
506
|
channel(channel) {
|
|
467
507
|
this.channelName = channel;
|
|
468
508
|
return this;
|
|
@@ -501,7 +541,7 @@ var RealtimeNotification = class extends NotificationContract {
|
|
|
501
541
|
return this;
|
|
502
542
|
}
|
|
503
543
|
resolveChannel() {
|
|
504
|
-
if (this.channelName) return this.channelName;
|
|
544
|
+
if (this.channelName !== void 0) return this.channelName;
|
|
505
545
|
if (this.user) return `${this.channelPrefix}${this.user.id}`;
|
|
506
546
|
throw new Error("No channel resolved for realtime notification (provide a user or channel)");
|
|
507
547
|
}
|
|
@@ -662,7 +702,7 @@ var Notification = class Notification {
|
|
|
662
702
|
}
|
|
663
703
|
prepare(recipient, data = {}) {
|
|
664
704
|
this.driver.data(data);
|
|
665
|
-
if (recipient && typeof recipient === "object" && !Array.isArray(recipient) &&
|
|
705
|
+
if (recipient && typeof recipient === "object" && !Array.isArray(recipient) && typeof recipient.id !== "undefined") {
|
|
666
706
|
if (this.driver instanceof MailNotification) {
|
|
667
707
|
if (recipient.email) this.driver.recipient(recipient.email);
|
|
668
708
|
} else if (this.driver instanceof SmsNotification) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkstack/notifications",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Framework-agnostic notification module for Arkstack and Nodejs, providing support for multi-channel notification delivery.",
|
|
6
6
|
"homepage": "https://arkstack.toneflix.net/guide/notifications",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"africastalking": "^0.8.0",
|
|
35
35
|
"nodemailer": "^8.0.7",
|
|
36
36
|
"twilio": "^6.0.0",
|
|
37
|
-
"@arkstack/common": "^0.16.
|
|
37
|
+
"@arkstack/common": "^0.16.11"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@kanun-hq/plugin-phone": "^0.1.8",
|
|
41
41
|
"firebase-admin": "^13.0.0",
|
|
42
42
|
"pusher": "^5.2.0",
|
|
43
|
-
"@arkstack/contract": "^0.16.
|
|
44
|
-
"@arkstack/database": "^0.16.
|
|
43
|
+
"@arkstack/contract": "^0.16.11",
|
|
44
|
+
"@arkstack/database": "^0.16.11"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"pusher": {
|