@feathers-baas/plugin-notifications 0.2.1 → 0.3.0
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/CHANGELOG.md +9 -0
- package/dist/index.cjs +48 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -16
- package/dist/index.d.ts +40 -16
- package/dist/index.js +44 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +14 -3
- package/src/notifier.ts +79 -26
- package/src/queue/producer.ts +4 -1
- package/src/queue/worker.ts +3 -1
- package/test/notifier.test.ts +75 -0
- package/test/queue-name.test.ts +19 -0
- package/test/queue.test.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @feathers-baas/plugin-notifications
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4d99238: Fix BullMQ startup crash and make auth email links configurable (#6, #7).
|
|
8
|
+
- The queue is now named `feathers-baas-notifications` with BullMQ `prefix: 'feathers-baas'` (exported as `NOTIFICATION_QUEUE_PREFIX`). bullmq ≥ 5 rejects `:` in queue names, so the old name crashed the app at startup whenever `REDIS_URL` and an email driver were both set. Queue and worker use the same name and prefix.
|
|
9
|
+
- Notifier options accept `paths` (`verify`, `resetPassword`, `verifySetPassword`), `verifyDelay` / `resetDelay`, and a `buildNotification` override. `buildAuthNotification` and `formatDuration` are exported so apps can wrap the default emails.
|
|
10
|
+
- The "link expires in…" copy is derived from the configured token lifetimes (the verification email used to say 24 hours while tokens lived 5 days).
|
|
11
|
+
|
|
3
12
|
## 0.2.1
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -33,15 +33,18 @@ __export(index_exports, {
|
|
|
33
33
|
BrevoDriver: () => BrevoDriver,
|
|
34
34
|
DriverRegistry: () => DriverRegistry,
|
|
35
35
|
NOTIFICATION_QUEUE_NAME: () => NOTIFICATION_QUEUE_NAME,
|
|
36
|
+
NOTIFICATION_QUEUE_PREFIX: () => NOTIFICATION_QUEUE_PREFIX,
|
|
36
37
|
ResendDriver: () => ResendDriver,
|
|
37
38
|
SMTPDriver: () => SMTPDriver,
|
|
38
39
|
SendGridDriver: () => SendGridDriver,
|
|
40
|
+
buildAuthNotification: () => buildAuthNotification,
|
|
39
41
|
createBullMQNotifier: () => createBullMQNotifier,
|
|
40
42
|
createDirectNotifier: () => createDirectNotifier,
|
|
41
43
|
createNotificationQueue: () => createNotificationQueue,
|
|
42
44
|
createNotificationWorker: () => createNotificationWorker,
|
|
43
45
|
default: () => index_default,
|
|
44
|
-
enqueueNotification: () => enqueueNotification
|
|
46
|
+
enqueueNotification: () => enqueueNotification,
|
|
47
|
+
formatDuration: () => formatDuration
|
|
45
48
|
});
|
|
46
49
|
module.exports = __toCommonJS(index_exports);
|
|
47
50
|
var import_node_module = require("module");
|
|
@@ -78,10 +81,12 @@ var DriverRegistry = class {
|
|
|
78
81
|
|
|
79
82
|
// src/queue/producer.ts
|
|
80
83
|
var import_bullmq = require("bullmq");
|
|
81
|
-
var NOTIFICATION_QUEUE_NAME = "feathers-baas
|
|
84
|
+
var NOTIFICATION_QUEUE_NAME = "feathers-baas-notifications";
|
|
85
|
+
var NOTIFICATION_QUEUE_PREFIX = "feathers-baas";
|
|
82
86
|
function createNotificationQueue(redis) {
|
|
83
87
|
return new import_bullmq.Queue(NOTIFICATION_QUEUE_NAME, {
|
|
84
88
|
connection: redis,
|
|
89
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
85
90
|
defaultJobOptions: {
|
|
86
91
|
attempts: 5,
|
|
87
92
|
backoff: {
|
|
@@ -108,14 +113,39 @@ var AUTH_MANAGEMENT_EVENT_MAP = {
|
|
|
108
113
|
passwordChange: "auth.password-change",
|
|
109
114
|
identityChange: "auth.identity-change"
|
|
110
115
|
};
|
|
111
|
-
|
|
116
|
+
var DEFAULT_VERIFY_DELAY_MS = 1e3 * 60 * 60 * 24 * 5;
|
|
117
|
+
var DEFAULT_RESET_DELAY_MS = 1e3 * 60 * 60 * 2;
|
|
118
|
+
function formatDuration(ms) {
|
|
119
|
+
const units = [
|
|
120
|
+
["day", 864e5],
|
|
121
|
+
["hour", 36e5],
|
|
122
|
+
["minute", 6e4]
|
|
123
|
+
];
|
|
124
|
+
for (const [name, size] of units) {
|
|
125
|
+
if (ms >= size && ms % size === 0) {
|
|
126
|
+
const n = ms / size;
|
|
127
|
+
return `${n} ${name}${n === 1 ? "" : "s"}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const minutes = Math.max(1, Math.round(ms / 6e4));
|
|
131
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
132
|
+
}
|
|
133
|
+
function buildLink(appUrl, path, token) {
|
|
134
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
135
|
+
const route = path.startsWith("/") ? path : `/${path}`;
|
|
136
|
+
return `${base}${route}?token=${encodeURIComponent(token ?? "")}`;
|
|
137
|
+
}
|
|
138
|
+
function buildAuthNotification(type, user, opts) {
|
|
139
|
+
const { appUrl, paths = {} } = opts;
|
|
140
|
+
const verifyExpiry = formatDuration(opts.verifyDelay ?? DEFAULT_VERIFY_DELAY_MS);
|
|
141
|
+
const resetExpiry = formatDuration(opts.resetDelay ?? DEFAULT_RESET_DELAY_MS);
|
|
112
142
|
const email = user["email"];
|
|
113
143
|
if (!email) return null;
|
|
114
144
|
switch (type) {
|
|
115
145
|
case "auth.verify-signup":
|
|
116
146
|
case "auth.resend-verify-signup": {
|
|
117
147
|
const token = user["verifyToken"];
|
|
118
|
-
const verifyUrl =
|
|
148
|
+
const verifyUrl = buildLink(appUrl, paths.verify ?? "/verify", token);
|
|
119
149
|
return {
|
|
120
150
|
to: email,
|
|
121
151
|
channel: "email",
|
|
@@ -124,15 +154,15 @@ function buildAuthNotification(type, user, appUrl) {
|
|
|
124
154
|
<h2>Welcome!</h2>
|
|
125
155
|
<p>Click the link below to verify your email address:</p>
|
|
126
156
|
<p><a href="<%= verifyUrl %>">Verify email</a></p>
|
|
127
|
-
<p>This link expires in
|
|
157
|
+
<p>This link expires in <%= expiresIn %>.</p>
|
|
128
158
|
`,
|
|
129
159
|
textTemplate: `Verify your email: <%= verifyUrl %>`,
|
|
130
|
-
data: { verifyUrl, user }
|
|
160
|
+
data: { verifyUrl, expiresIn: verifyExpiry, user }
|
|
131
161
|
};
|
|
132
162
|
}
|
|
133
163
|
case "auth.send-reset-pwd": {
|
|
134
164
|
const token = user["resetToken"];
|
|
135
|
-
const resetUrl =
|
|
165
|
+
const resetUrl = buildLink(appUrl, paths.resetPassword ?? "/reset-password", token);
|
|
136
166
|
return {
|
|
137
167
|
to: email,
|
|
138
168
|
channel: "email",
|
|
@@ -141,10 +171,10 @@ function buildAuthNotification(type, user, appUrl) {
|
|
|
141
171
|
<h2>Password reset requested</h2>
|
|
142
172
|
<p>Click the link below to reset your password:</p>
|
|
143
173
|
<p><a href="<%= resetUrl %>">Reset password</a></p>
|
|
144
|
-
<p>This link expires in
|
|
174
|
+
<p>This link expires in <%= expiresIn %>. If you did not request this, ignore this email.</p>
|
|
145
175
|
`,
|
|
146
176
|
textTemplate: `Reset your password: <%= resetUrl %>`,
|
|
147
|
-
data: { resetUrl, user }
|
|
177
|
+
data: { resetUrl, expiresIn: resetExpiry, user }
|
|
148
178
|
};
|
|
149
179
|
}
|
|
150
180
|
case "auth.reset-pwd": {
|
|
@@ -179,7 +209,7 @@ function buildAuthNotification(type, user, appUrl) {
|
|
|
179
209
|
}
|
|
180
210
|
case "auth.verify-signup-set-password": {
|
|
181
211
|
const token = user["verifyToken"];
|
|
182
|
-
const verifyUrl =
|
|
212
|
+
const verifyUrl = buildLink(appUrl, paths.verifySetPassword ?? "/verify-set-password", token);
|
|
183
213
|
return {
|
|
184
214
|
to: email,
|
|
185
215
|
channel: "email",
|
|
@@ -198,7 +228,7 @@ function createDirectNotifier(opts) {
|
|
|
198
228
|
return async (type, user, _notifierOptions) => {
|
|
199
229
|
const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type];
|
|
200
230
|
if (!eventKey) return;
|
|
201
|
-
const notification = buildAuthNotification(eventKey, user, opts
|
|
231
|
+
const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts);
|
|
202
232
|
if (!notification) return;
|
|
203
233
|
const driverNames = opts.registry.getDriversForEvent(eventKey);
|
|
204
234
|
if (driverNames.length === 0) return;
|
|
@@ -215,7 +245,7 @@ function createBullMQNotifier(opts) {
|
|
|
215
245
|
return async (type, user, _notifierOptions) => {
|
|
216
246
|
const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type];
|
|
217
247
|
if (!eventKey) return;
|
|
218
|
-
const notification = buildAuthNotification(eventKey, user, opts
|
|
248
|
+
const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts);
|
|
219
249
|
if (!notification) return;
|
|
220
250
|
const driverNames = opts.registry.getDriversForEvent(eventKey);
|
|
221
251
|
if (driverNames.length === 0) return;
|
|
@@ -243,6 +273,8 @@ function createNotificationWorker(registry, redis, opts = {}) {
|
|
|
243
273
|
},
|
|
244
274
|
{
|
|
245
275
|
connection: redis,
|
|
276
|
+
// Must match the producer, or jobs are enqueued where the worker isn't listening
|
|
277
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
246
278
|
concurrency,
|
|
247
279
|
// Don't create a new redis connection per job — reuse the pool
|
|
248
280
|
autorun: true
|
|
@@ -382,13 +414,16 @@ var index_default = plugin;
|
|
|
382
414
|
BrevoDriver,
|
|
383
415
|
DriverRegistry,
|
|
384
416
|
NOTIFICATION_QUEUE_NAME,
|
|
417
|
+
NOTIFICATION_QUEUE_PREFIX,
|
|
385
418
|
ResendDriver,
|
|
386
419
|
SMTPDriver,
|
|
387
420
|
SendGridDriver,
|
|
421
|
+
buildAuthNotification,
|
|
388
422
|
createBullMQNotifier,
|
|
389
423
|
createDirectNotifier,
|
|
390
424
|
createNotificationQueue,
|
|
391
425
|
createNotificationWorker,
|
|
392
|
-
enqueueNotification
|
|
426
|
+
enqueueNotification,
|
|
427
|
+
formatDuration
|
|
393
428
|
});
|
|
394
429
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/registry.ts","../src/queue/producer.ts","../src/notifier.ts","../src/queue/worker.ts","../src/drivers/smtp.driver.ts","../src/drivers/resend.driver.ts","../src/drivers/sendgrid.driver.ts","../src/drivers/brevo.driver.ts"],"sourcesContent":["// Generated by feathers-baas. Safe to edit.\nimport { createRequire } from 'node:module'\nimport type { FeathersBaasPlugin } from './plugin.js'\n\nconst pkg = createRequire(import.meta.url)('../package.json') as { version: string }\n\nconst plugin: FeathersBaasPlugin = {\n name: '@feathers-baas/plugin-notifications',\n version: pkg.version,\n async install(_app, _options) {\n // Notification drivers are configured via core's configureNotifications()\n // based on env vars. Auto-install is a no-op; discovery confirms the plugin is present.\n },\n}\n\nexport default plugin\n\nexport { DriverRegistry } from './registry.js'\nexport { createBullMQNotifier, createDirectNotifier } from './notifier.js'\nexport type { NotifierFn, BullMQNotifierOptions, DirectNotifierOptions } from './notifier.js'\n\n// Queue\nexport { createNotificationQueue, enqueueNotification, NOTIFICATION_QUEUE_NAME } from './queue/producer.js'\nexport { createNotificationWorker } from './queue/worker.js'\nexport type { WorkerOptions } from './queue/worker.js'\n\n// Drivers\nexport { SMTPDriver } from './drivers/smtp.driver.js'\nexport type { SMTPDriverOptions } from './drivers/smtp.driver.js'\nexport { ResendDriver } from './drivers/resend.driver.js'\nexport type { ResendDriverOptions } from './drivers/resend.driver.js'\nexport { SendGridDriver } from './drivers/sendgrid.driver.js'\nexport type { SendGridDriverOptions } from './drivers/sendgrid.driver.js'\nexport { BrevoDriver } from './drivers/brevo.driver.js'\nexport type { BrevoDriverOptions } from './drivers/brevo.driver.js'\n\n// Types\nexport type {\n Notification,\n NotifierDriver,\n NotificationJob,\n NotificationEvent,\n AuthNotificationEvent,\n} from './types.js'\n","// Generated by feathers-baas. Safe to edit.\nimport type { NotifierDriver, NotificationEvent } from './types.js'\n\n/**\n * DriverRegistry maps event keys to the list of drivers that should handle\n * that event.\n *\n * A single driver instance can handle multiple events (e.g. the SMTP driver\n * handles all auth.* email events).\n *\n * @example\n * ```ts\n * const registry = new DriverRegistry()\n * registry.registerDriver(smtpDriver)\n * registry.mapEvent('auth.verify-signup', 'smtp')\n * registry.mapEvent('auth.send-reset-pwd', 'smtp')\n * ```\n */\nexport class DriverRegistry {\n private readonly drivers = new Map<string, NotifierDriver>()\n private readonly eventMap = new Map<NotificationEvent, string[]>()\n\n /** Register a driver under its name. Overwrites any existing driver with the same name. */\n registerDriver(driver: NotifierDriver): void {\n this.drivers.set(driver.name, driver)\n }\n\n /** Map a notification event to a driver name. Multiple calls for the same event add drivers. */\n mapEvent(event: NotificationEvent, driverName: string): void {\n const existing = this.eventMap.get(event) ?? []\n if (!existing.includes(driverName)) {\n existing.push(driverName)\n }\n this.eventMap.set(event, existing)\n }\n\n /** Look up a driver by name. Returns undefined if not registered. */\n getDriver(name: string): NotifierDriver | undefined {\n return this.drivers.get(name)\n }\n\n /** Get all driver names mapped to the given event. */\n getDriversForEvent(event: NotificationEvent): string[] {\n return this.eventMap.get(event) ?? []\n }\n\n /** All registered driver names. */\n listDriverNames(): string[] {\n return [...this.drivers.keys()]\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Queue } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { NotificationJob } from '../types.js'\n\nexport const NOTIFICATION_QUEUE_NAME = 'feathers-baas:notifications'\n\n/**\n * Create a BullMQ Queue instance for notification jobs.\n *\n * @example\n * ```ts\n * const redis = new Redis(process.env.REDIS_URL)\n * const queue = createNotificationQueue(redis)\n * await enqueueNotification(queue, { driverName: 'smtp', notification: { ... } })\n * ```\n */\nexport function createNotificationQueue(redis: Redis): Queue<NotificationJob> {\n return new Queue<NotificationJob>(NOTIFICATION_QUEUE_NAME, {\n connection: redis,\n defaultJobOptions: {\n attempts: 5,\n backoff: {\n type: 'exponential',\n delay: 1_000, // initial delay: 1s → 2s → 4s → 8s → 16s\n },\n removeOnComplete: { count: 100 },\n removeOnFail: { count: 500 },\n },\n })\n}\n\n/**\n * Enqueue a notification job.\n *\n * @param queue BullMQ Queue instance from createNotificationQueue()\n * @param job Job payload { driverName, notification }\n */\nexport async function enqueueNotification(\n queue: Queue<NotificationJob>,\n job: NotificationJob,\n): Promise<void> {\n await queue.add('send', job)\n}\n","// Generated by feathers-baas. Safe to edit.\nimport type { Queue } from 'bullmq'\nimport type { NotificationJob, Notification, AuthNotificationEvent } from './types.js'\nimport { enqueueNotification } from './queue/producer.js'\nimport type { DriverRegistry } from './registry.js'\n\n/**\n * Auth-management notification types mapped to feathers-baas event keys.\n * feathers-authentication-management passes these type strings to the notifier.\n */\nconst AUTH_MANAGEMENT_EVENT_MAP: Record<string, AuthNotificationEvent> = {\n resendVerifySignup: 'auth.resend-verify-signup',\n verifySignup: 'auth.verify-signup',\n verifySignupSetPassword: 'auth.verify-signup-set-password',\n sendResetPwd: 'auth.send-reset-pwd',\n resetPwd: 'auth.reset-pwd',\n passwordChange: 'auth.password-change',\n identityChange: 'auth.identity-change',\n}\n\n/**\n * Build a default Notification payload for each auth event type.\n * Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.\n */\nfunction buildAuthNotification(\n type: AuthNotificationEvent,\n user: Record<string, unknown>,\n appUrl: string,\n): Notification | null {\n const email = user['email'] as string\n if (!email) return null\n\n switch (type) {\n case 'auth.verify-signup':\n case 'auth.resend-verify-signup': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = `${appUrl}/verify?token=${token ?? ''}`\n return {\n to: email,\n channel: 'email',\n subject: 'Verify your email address',\n bodyTemplate: `\n <h2>Welcome!</h2>\n <p>Click the link below to verify your email address:</p>\n <p><a href=\"<%= verifyUrl %>\">Verify email</a></p>\n <p>This link expires in 24 hours.</p>\n `,\n textTemplate: `Verify your email: <%= verifyUrl %>`,\n data: { verifyUrl, user },\n }\n }\n\n case 'auth.send-reset-pwd': {\n const token = user['resetToken'] as string | undefined\n const resetUrl = `${appUrl}/reset-password?token=${token ?? ''}`\n return {\n to: email,\n channel: 'email',\n subject: 'Reset your password',\n bodyTemplate: `\n <h2>Password reset requested</h2>\n <p>Click the link below to reset your password:</p>\n <p><a href=\"<%= resetUrl %>\">Reset password</a></p>\n <p>This link expires in 2 hours. If you did not request this, ignore this email.</p>\n `,\n textTemplate: `Reset your password: <%= resetUrl %>`,\n data: { resetUrl, user },\n }\n }\n\n case 'auth.reset-pwd': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been reset',\n bodyTemplate: `<p>Your password has been successfully reset. If you did not do this, please contact support immediately.</p>`,\n textTemplate: `Your password has been reset. If you did not do this, please contact support.`,\n data: { user },\n }\n }\n\n case 'auth.password-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been changed',\n bodyTemplate: `<p>Your account password was just changed. If this was not you, please contact support immediately.</p>`,\n textTemplate: `Your password was changed. Contact support if this wasn't you.`,\n data: { user },\n }\n }\n\n case 'auth.identity-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your email address is being changed',\n bodyTemplate: `<p>A request was made to change your email address. Click the link to confirm.</p>`,\n textTemplate: `Confirm your email change by clicking the link in the HTML email.`,\n data: { user },\n }\n }\n\n case 'auth.verify-signup-set-password': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = `${appUrl}/verify-set-password?token=${token ?? ''}`\n return {\n to: email,\n channel: 'email',\n subject: 'Set your password',\n bodyTemplate: `\n <h2>Set your password</h2>\n <p><a href=\"<%= verifyUrl %>\">Set password</a></p>\n `,\n textTemplate: `Set your password: <%= verifyUrl %>`,\n data: { verifyUrl, user },\n }\n }\n }\n}\n\nexport type NotifierFn = (\n type: string,\n user: Record<string, unknown>,\n notifierOptions: Record<string, unknown>,\n) => Promise<void>\n\nexport interface BullMQNotifierOptions {\n /** BullMQ Queue instance — from createNotificationQueue(). */\n queue: Queue<NotificationJob>\n /** Driver registry — used to determine which drivers handle each event. */\n registry: DriverRegistry\n /**\n * Public base URL of the app, e.g. https://api.example.com\n * Used to construct verification / reset URLs in email templates.\n */\n appUrl: string\n}\n\n/**\n * Create a notifier function compatible with feathers-authentication-management.\n *\n * When an auth event fires (verifySignup, sendResetPwd, etc.), this notifier:\n * 1. Maps the event type to a feathers-baas notification event key\n * 2. Looks up all registered drivers for that event\n * 3. Enqueues one BullMQ job per driver\n * 4. Returns immediately — delivery is asynchronous\n *\n * Wire into the app:\n * ```ts\n * app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))\n * ```\n */\nexport interface DirectNotifierOptions {\n /** Driver registry — used to determine which drivers handle each event. */\n registry: DriverRegistry\n /** Public base URL of the app. */\n appUrl: string\n}\n\n/**\n * Create a notifier that sends notifications directly via the driver (no queue).\n * Use this when Redis/BullMQ is not available — suitable for API-based drivers\n * (Brevo, Resend, SendGrid) in development or low-traffic environments.\n */\nexport function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return\n\n const notification = buildAuthNotification(eventKey, user, opts.appUrl)\n if (!notification) return\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return\n\n await Promise.all(\n driverNames.map(async (driverName) => {\n const driver = opts.registry.getDriver(driverName)\n if (!driver) return\n await driver.send(notification)\n }),\n )\n }\n}\n\nexport function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return // Unknown event type — no-op\n\n const notification = buildAuthNotification(eventKey, user, opts.appUrl)\n if (!notification) return // Missing required user fields — no-op\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return // No drivers configured for this event\n\n await Promise.all(\n driverNames.map((driverName) =>\n enqueueNotification(opts.queue, { driverName, notification }),\n ),\n )\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Worker } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { DriverRegistry } from '../registry.js'\nimport type { NotificationJob } from '../types.js'\nimport { NOTIFICATION_QUEUE_NAME } from './producer.js'\n\nexport interface WorkerOptions {\n /** BullMQ concurrency — number of jobs processed in parallel. */\n concurrency?: number\n}\n\n/**\n * Create and start the BullMQ worker that processes notification jobs.\n *\n * The worker looks up the driver from the registry by name, renders the\n * EJS template, and calls driver.send(). BullMQ handles retries with\n * exponential back-off automatically.\n *\n * Call worker.close() during graceful shutdown to drain in-flight jobs.\n *\n * @example\n * ```ts\n * const worker = createNotificationWorker(registry, redis, { concurrency: 5 })\n *\n * process.on('SIGTERM', async () => {\n * await worker.close()\n * })\n * ```\n */\nexport function createNotificationWorker(\n registry: DriverRegistry,\n redis: Redis,\n opts: WorkerOptions = {},\n): Worker<NotificationJob> {\n const concurrency = opts.concurrency ?? 5\n\n const worker = new Worker<NotificationJob>(\n NOTIFICATION_QUEUE_NAME,\n async (job) => {\n const { driverName, notification } = job.data\n\n const driver = registry.getDriver(driverName)\n if (!driver) {\n throw new Error(`Notification driver \"${driverName}\" is not registered`)\n }\n\n await driver.send(notification)\n },\n {\n connection: redis,\n concurrency,\n // Don't create a new redis connection per job — reuse the pool\n autorun: true,\n },\n )\n\n worker.on('failed', (job, err) => {\n // BullMQ handles retries; this is just for observability\n const jobId = job?.id ?? 'unknown'\n const message = err instanceof Error ? err.message : String(err)\n console.error(`[notifications] Job ${jobId} failed: ${message}`)\n })\n\n return worker\n}\n","// Generated by feathers-baas. Safe to edit.\nimport nodemailer from 'nodemailer'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SMTPDriverOptions {\n /** SMTP connection URL, e.g. smtp://user:pass@mail.example.com:587 */\n url: string\n /** Display name shown in the From field. */\n fromName: string\n /** Email address shown in the From field. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by nodemailer over SMTP.\n * Templates are rendered with EJS at send time.\n *\n * Use this driver for self-hosted SMTP servers (Postfix, Mailtrap, etc.).\n * For transactional email services, prefer ResendDriver.\n */\nexport class SMTPDriver implements NotifierDriver {\n readonly name = 'smtp'\n\n private readonly transporter: nodemailer.Transporter\n\n constructor(private readonly opts: SMTPDriverOptions) {\n this.transporter = nodemailer.createTransport(opts.url)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await this.transporter.sendMail({\n from: `\"${this.opts.fromName}\" <${this.opts.fromAddress}>`,\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n }\n\n /** Verify SMTP connectivity. Throws if the server is unreachable. */\n async verify(): Promise<void> {\n await this.transporter.verify()\n }\n\n /** Close the underlying connection pool. */\n close(): void {\n this.transporter.close()\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Resend } from 'resend'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface ResendDriverOptions {\n /** Resend API key (from https://resend.com/api-keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be a verified domain in your Resend account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Resend transactional email API.\n * Templates are rendered with EJS at send time.\n *\n * Resend offers generous free tier and excellent deliverability,\n * making it the recommended choice for new projects.\n */\nexport class ResendDriver implements NotifierDriver {\n readonly name = 'resend'\n\n private readonly client: Resend\n\n constructor(private readonly opts: ResendDriverOptions) {\n this.client = new Resend(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const result = await this.client.emails.send({\n from: `${this.opts.fromName} <${this.opts.fromAddress}>`,\n to: [notification.to],\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n\n if ('error' in result && result.error) {\n throw new Error(`Resend error: ${result.error.message}`)\n }\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport sgMail from '@sendgrid/mail'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SendGridDriverOptions {\n /** SendGrid API key (from https://app.sendgrid.com/settings/api_keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your SendGrid account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the SendGrid API.\n * Templates are rendered with EJS at send time.\n */\nexport class SendGridDriver implements NotifierDriver {\n readonly name = 'sendgrid'\n\n constructor(private readonly opts: SendGridDriverOptions) {\n sgMail.setApiKey(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await sgMail.send({\n from: { name: this.opts.fromName, email: this.opts.fromAddress },\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n ...(text ? { text } : {}),\n })\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { TransactionalEmailsApi, TransactionalEmailsApiApiKeys, SendSmtpEmail } from '@getbrevo/brevo'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface BrevoDriverOptions {\n /** Brevo API key (from https://app.brevo.com/settings/keys/api). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your Brevo account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Brevo (formerly Sendinblue) Transactional Email API.\n * Templates are rendered with EJS at send time.\n */\nexport class BrevoDriver implements NotifierDriver {\n readonly name = 'brevo'\n\n private readonly apiInstance: TransactionalEmailsApi\n\n constructor(private readonly opts: BrevoDriverOptions) {\n this.apiInstance = new TransactionalEmailsApi()\n this.apiInstance.setApiKey(TransactionalEmailsApiApiKeys.apiKey, opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const email = new SendSmtpEmail()\n email.sender = { name: this.opts.fromName, email: this.opts.fromAddress }\n email.to = [{ email: notification.to }]\n email.subject = notification.subject ?? '(no subject)'\n email.htmlContent = html\n if (text) email.textContent = text\n\n await this.apiInstance.sendTransacEmail(email)\n }\n}\n"],"mappings":"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","names":["import_bullmq","nodemailer","ejs","ejsRender","ejs","ejsRender","sgMail","ejs","ejsRender"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/registry.ts","../src/queue/producer.ts","../src/notifier.ts","../src/queue/worker.ts","../src/drivers/smtp.driver.ts","../src/drivers/resend.driver.ts","../src/drivers/sendgrid.driver.ts","../src/drivers/brevo.driver.ts"],"sourcesContent":["// Generated by feathers-baas. Safe to edit.\nimport { createRequire } from 'node:module'\nimport type { FeathersBaasPlugin } from './plugin.js'\n\nconst pkg = createRequire(import.meta.url)('../package.json') as { version: string }\n\nconst plugin: FeathersBaasPlugin = {\n name: '@feathers-baas/plugin-notifications',\n version: pkg.version,\n async install(_app, _options) {\n // Notification drivers are configured via core's configureNotifications()\n // based on env vars. Auto-install is a no-op; discovery confirms the plugin is present.\n },\n}\n\nexport default plugin\n\nexport { DriverRegistry } from './registry.js'\nexport { createBullMQNotifier, createDirectNotifier, buildAuthNotification, formatDuration } from './notifier.js'\nexport type {\n NotifierFn,\n BullMQNotifierOptions,\n DirectNotifierOptions,\n AuthNotificationOptions,\n AuthNotificationBuilder,\n} from './notifier.js'\n\n// Queue\nexport {\n createNotificationQueue,\n enqueueNotification,\n NOTIFICATION_QUEUE_NAME,\n NOTIFICATION_QUEUE_PREFIX,\n} from './queue/producer.js'\nexport { createNotificationWorker } from './queue/worker.js'\nexport type { WorkerOptions } from './queue/worker.js'\n\n// Drivers\nexport { SMTPDriver } from './drivers/smtp.driver.js'\nexport type { SMTPDriverOptions } from './drivers/smtp.driver.js'\nexport { ResendDriver } from './drivers/resend.driver.js'\nexport type { ResendDriverOptions } from './drivers/resend.driver.js'\nexport { SendGridDriver } from './drivers/sendgrid.driver.js'\nexport type { SendGridDriverOptions } from './drivers/sendgrid.driver.js'\nexport { BrevoDriver } from './drivers/brevo.driver.js'\nexport type { BrevoDriverOptions } from './drivers/brevo.driver.js'\n\n// Types\nexport type {\n Notification,\n NotifierDriver,\n NotificationJob,\n NotificationEvent,\n AuthNotificationEvent,\n} from './types.js'\n","// Generated by feathers-baas. Safe to edit.\nimport type { NotifierDriver, NotificationEvent } from './types.js'\n\n/**\n * DriverRegistry maps event keys to the list of drivers that should handle\n * that event.\n *\n * A single driver instance can handle multiple events (e.g. the SMTP driver\n * handles all auth.* email events).\n *\n * @example\n * ```ts\n * const registry = new DriverRegistry()\n * registry.registerDriver(smtpDriver)\n * registry.mapEvent('auth.verify-signup', 'smtp')\n * registry.mapEvent('auth.send-reset-pwd', 'smtp')\n * ```\n */\nexport class DriverRegistry {\n private readonly drivers = new Map<string, NotifierDriver>()\n private readonly eventMap = new Map<NotificationEvent, string[]>()\n\n /** Register a driver under its name. Overwrites any existing driver with the same name. */\n registerDriver(driver: NotifierDriver): void {\n this.drivers.set(driver.name, driver)\n }\n\n /** Map a notification event to a driver name. Multiple calls for the same event add drivers. */\n mapEvent(event: NotificationEvent, driverName: string): void {\n const existing = this.eventMap.get(event) ?? []\n if (!existing.includes(driverName)) {\n existing.push(driverName)\n }\n this.eventMap.set(event, existing)\n }\n\n /** Look up a driver by name. Returns undefined if not registered. */\n getDriver(name: string): NotifierDriver | undefined {\n return this.drivers.get(name)\n }\n\n /** Get all driver names mapped to the given event. */\n getDriversForEvent(event: NotificationEvent): string[] {\n return this.eventMap.get(event) ?? []\n }\n\n /** All registered driver names. */\n listDriverNames(): string[] {\n return [...this.drivers.keys()]\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Queue } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { NotificationJob } from '../types.js'\n\n// BullMQ rejects ':' in queue names — namespace with its `prefix` option instead.\nexport const NOTIFICATION_QUEUE_NAME = 'feathers-baas-notifications'\nexport const NOTIFICATION_QUEUE_PREFIX = 'feathers-baas'\n\n/**\n * Create a BullMQ Queue instance for notification jobs.\n *\n * @example\n * ```ts\n * const redis = new Redis(process.env.REDIS_URL)\n * const queue = createNotificationQueue(redis)\n * await enqueueNotification(queue, { driverName: 'smtp', notification: { ... } })\n * ```\n */\nexport function createNotificationQueue(redis: Redis): Queue<NotificationJob> {\n return new Queue<NotificationJob>(NOTIFICATION_QUEUE_NAME, {\n connection: redis,\n prefix: NOTIFICATION_QUEUE_PREFIX,\n defaultJobOptions: {\n attempts: 5,\n backoff: {\n type: 'exponential',\n delay: 1_000, // initial delay: 1s → 2s → 4s → 8s → 16s\n },\n removeOnComplete: { count: 100 },\n removeOnFail: { count: 500 },\n },\n })\n}\n\n/**\n * Enqueue a notification job.\n *\n * @param queue BullMQ Queue instance from createNotificationQueue()\n * @param job Job payload { driverName, notification }\n */\nexport async function enqueueNotification(\n queue: Queue<NotificationJob>,\n job: NotificationJob,\n): Promise<void> {\n await queue.add('send', job)\n}\n","// Generated by feathers-baas. Safe to edit.\nimport type { Queue } from 'bullmq'\nimport type { NotificationJob, Notification, AuthNotificationEvent } from './types.js'\nimport { enqueueNotification } from './queue/producer.js'\nimport type { DriverRegistry } from './registry.js'\n\n/**\n * Auth-management notification types mapped to feathers-baas event keys.\n * feathers-authentication-management passes these type strings to the notifier.\n */\nconst AUTH_MANAGEMENT_EVENT_MAP: Record<string, AuthNotificationEvent> = {\n resendVerifySignup: 'auth.resend-verify-signup',\n verifySignup: 'auth.verify-signup',\n verifySignupSetPassword: 'auth.verify-signup-set-password',\n sendResetPwd: 'auth.send-reset-pwd',\n resetPwd: 'auth.reset-pwd',\n passwordChange: 'auth.password-change',\n identityChange: 'auth.identity-change',\n}\n\n/** Defaults match feathers-authentication-management's own `delay` / `resetDelay`. */\nconst DEFAULT_VERIFY_DELAY_MS = 1000 * 60 * 60 * 24 * 5\nconst DEFAULT_RESET_DELAY_MS = 1000 * 60 * 60 * 2\n\nexport interface AuthNotificationOptions {\n /**\n * Public base URL of the **web app** that redeems the tokens (not this API),\n * e.g. https://app.example.com\n */\n appUrl: string\n /** Frontend routes the email links point to. */\n paths?: {\n /** Default: `/verify` */\n verify?: string\n /** Default: `/reset-password` */\n resetPassword?: string\n /** Default: `/verify-set-password` */\n verifySetPassword?: string\n }\n /** Verification token lifetime in ms — must match auth-management's `delay`. Default: 5 days. */\n verifyDelay?: number\n /** Reset token lifetime in ms — must match auth-management's `resetDelay`. Default: 2 hours. */\n resetDelay?: number\n}\n\nexport type AuthNotificationBuilder = (\n type: AuthNotificationEvent,\n user: Record<string, unknown>,\n opts: AuthNotificationOptions,\n) => Notification | null\n\n/** Human-readable duration for email copy, e.g. 432000000 → \"5 days\". */\nexport function formatDuration(ms: number): string {\n const units: [string, number][] = [\n ['day', 86_400_000],\n ['hour', 3_600_000],\n ['minute', 60_000],\n ]\n for (const [name, size] of units) {\n if (ms >= size && ms % size === 0) {\n const n = ms / size\n return `${n} ${name}${n === 1 ? '' : 's'}`\n }\n }\n const minutes = Math.max(1, Math.round(ms / 60_000))\n return `${minutes} minute${minutes === 1 ? '' : 's'}`\n}\n\nfunction buildLink(appUrl: string, path: string, token: string | undefined): string {\n const base = appUrl.replace(/\\/+$/, '')\n const route = path.startsWith('/') ? path : `/${path}`\n return `${base}${route}?token=${encodeURIComponent(token ?? '')}`\n}\n\n/**\n * Build the default Notification payload for each auth event type.\n * Exported so apps can wrap it via the notifier's `buildNotification` option.\n * Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.\n */\nexport function buildAuthNotification(\n type: AuthNotificationEvent,\n user: Record<string, unknown>,\n opts: AuthNotificationOptions,\n): Notification | null {\n const { appUrl, paths = {} } = opts\n const verifyExpiry = formatDuration(opts.verifyDelay ?? DEFAULT_VERIFY_DELAY_MS)\n const resetExpiry = formatDuration(opts.resetDelay ?? DEFAULT_RESET_DELAY_MS)\n const email = user['email'] as string\n if (!email) return null\n\n switch (type) {\n case 'auth.verify-signup':\n case 'auth.resend-verify-signup': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = buildLink(appUrl, paths.verify ?? '/verify', token)\n return {\n to: email,\n channel: 'email',\n subject: 'Verify your email address',\n bodyTemplate: `\n <h2>Welcome!</h2>\n <p>Click the link below to verify your email address:</p>\n <p><a href=\"<%= verifyUrl %>\">Verify email</a></p>\n <p>This link expires in <%= expiresIn %>.</p>\n `,\n textTemplate: `Verify your email: <%= verifyUrl %>`,\n data: { verifyUrl, expiresIn: verifyExpiry, user },\n }\n }\n\n case 'auth.send-reset-pwd': {\n const token = user['resetToken'] as string | undefined\n const resetUrl = buildLink(appUrl, paths.resetPassword ?? '/reset-password', token)\n return {\n to: email,\n channel: 'email',\n subject: 'Reset your password',\n bodyTemplate: `\n <h2>Password reset requested</h2>\n <p>Click the link below to reset your password:</p>\n <p><a href=\"<%= resetUrl %>\">Reset password</a></p>\n <p>This link expires in <%= expiresIn %>. If you did not request this, ignore this email.</p>\n `,\n textTemplate: `Reset your password: <%= resetUrl %>`,\n data: { resetUrl, expiresIn: resetExpiry, user },\n }\n }\n\n case 'auth.reset-pwd': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been reset',\n bodyTemplate: `<p>Your password has been successfully reset. If you did not do this, please contact support immediately.</p>`,\n textTemplate: `Your password has been reset. If you did not do this, please contact support.`,\n data: { user },\n }\n }\n\n case 'auth.password-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been changed',\n bodyTemplate: `<p>Your account password was just changed. If this was not you, please contact support immediately.</p>`,\n textTemplate: `Your password was changed. Contact support if this wasn't you.`,\n data: { user },\n }\n }\n\n case 'auth.identity-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your email address is being changed',\n bodyTemplate: `<p>A request was made to change your email address. Click the link to confirm.</p>`,\n textTemplate: `Confirm your email change by clicking the link in the HTML email.`,\n data: { user },\n }\n }\n\n case 'auth.verify-signup-set-password': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = buildLink(appUrl, paths.verifySetPassword ?? '/verify-set-password', token)\n return {\n to: email,\n channel: 'email',\n subject: 'Set your password',\n bodyTemplate: `\n <h2>Set your password</h2>\n <p><a href=\"<%= verifyUrl %>\">Set password</a></p>\n `,\n textTemplate: `Set your password: <%= verifyUrl %>`,\n data: { verifyUrl, user },\n }\n }\n }\n}\n\nexport type NotifierFn = (\n type: string,\n user: Record<string, unknown>,\n notifierOptions: Record<string, unknown>,\n) => Promise<void>\n\ninterface NotifierBaseOptions extends AuthNotificationOptions {\n /** Driver registry — used to determine which drivers handle each event. */\n registry: DriverRegistry\n /** Replace the default email composition. Wrap `buildAuthNotification` to tweak it. */\n buildNotification?: AuthNotificationBuilder\n}\n\nexport interface BullMQNotifierOptions extends NotifierBaseOptions {\n /** BullMQ Queue instance — from createNotificationQueue(). */\n queue: Queue<NotificationJob>\n}\n\n/**\n * Create a notifier function compatible with feathers-authentication-management.\n *\n * When an auth event fires (verifySignup, sendResetPwd, etc.), this notifier:\n * 1. Maps the event type to a feathers-baas notification event key\n * 2. Looks up all registered drivers for that event\n * 3. Enqueues one BullMQ job per driver\n * 4. Returns immediately — delivery is asynchronous\n *\n * Wire into the app:\n * ```ts\n * app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))\n * ```\n */\nexport type DirectNotifierOptions = NotifierBaseOptions\n\n/**\n * Create a notifier that sends notifications directly via the driver (no queue).\n * Use this when Redis/BullMQ is not available — suitable for API-based drivers\n * (Brevo, Resend, SendGrid) in development or low-traffic environments.\n */\nexport function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return\n\n const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts)\n if (!notification) return\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return\n\n await Promise.all(\n driverNames.map(async (driverName) => {\n const driver = opts.registry.getDriver(driverName)\n if (!driver) return\n await driver.send(notification)\n }),\n )\n }\n}\n\nexport function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return // Unknown event type — no-op\n\n const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts)\n if (!notification) return // Missing required user fields — no-op\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return // No drivers configured for this event\n\n await Promise.all(\n driverNames.map((driverName) =>\n enqueueNotification(opts.queue, { driverName, notification }),\n ),\n )\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Worker } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { DriverRegistry } from '../registry.js'\nimport type { NotificationJob } from '../types.js'\nimport { NOTIFICATION_QUEUE_NAME, NOTIFICATION_QUEUE_PREFIX } from './producer.js'\n\nexport interface WorkerOptions {\n /** BullMQ concurrency — number of jobs processed in parallel. */\n concurrency?: number\n}\n\n/**\n * Create and start the BullMQ worker that processes notification jobs.\n *\n * The worker looks up the driver from the registry by name, renders the\n * EJS template, and calls driver.send(). BullMQ handles retries with\n * exponential back-off automatically.\n *\n * Call worker.close() during graceful shutdown to drain in-flight jobs.\n *\n * @example\n * ```ts\n * const worker = createNotificationWorker(registry, redis, { concurrency: 5 })\n *\n * process.on('SIGTERM', async () => {\n * await worker.close()\n * })\n * ```\n */\nexport function createNotificationWorker(\n registry: DriverRegistry,\n redis: Redis,\n opts: WorkerOptions = {},\n): Worker<NotificationJob> {\n const concurrency = opts.concurrency ?? 5\n\n const worker = new Worker<NotificationJob>(\n NOTIFICATION_QUEUE_NAME,\n async (job) => {\n const { driverName, notification } = job.data\n\n const driver = registry.getDriver(driverName)\n if (!driver) {\n throw new Error(`Notification driver \"${driverName}\" is not registered`)\n }\n\n await driver.send(notification)\n },\n {\n connection: redis,\n // Must match the producer, or jobs are enqueued where the worker isn't listening\n prefix: NOTIFICATION_QUEUE_PREFIX,\n concurrency,\n // Don't create a new redis connection per job — reuse the pool\n autorun: true,\n },\n )\n\n worker.on('failed', (job, err) => {\n // BullMQ handles retries; this is just for observability\n const jobId = job?.id ?? 'unknown'\n const message = err instanceof Error ? err.message : String(err)\n console.error(`[notifications] Job ${jobId} failed: ${message}`)\n })\n\n return worker\n}\n","// Generated by feathers-baas. Safe to edit.\nimport nodemailer from 'nodemailer'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SMTPDriverOptions {\n /** SMTP connection URL, e.g. smtp://user:pass@mail.example.com:587 */\n url: string\n /** Display name shown in the From field. */\n fromName: string\n /** Email address shown in the From field. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by nodemailer over SMTP.\n * Templates are rendered with EJS at send time.\n *\n * Use this driver for self-hosted SMTP servers (Postfix, Mailtrap, etc.).\n * For transactional email services, prefer ResendDriver.\n */\nexport class SMTPDriver implements NotifierDriver {\n readonly name = 'smtp'\n\n private readonly transporter: nodemailer.Transporter\n\n constructor(private readonly opts: SMTPDriverOptions) {\n this.transporter = nodemailer.createTransport(opts.url)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await this.transporter.sendMail({\n from: `\"${this.opts.fromName}\" <${this.opts.fromAddress}>`,\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n }\n\n /** Verify SMTP connectivity. Throws if the server is unreachable. */\n async verify(): Promise<void> {\n await this.transporter.verify()\n }\n\n /** Close the underlying connection pool. */\n close(): void {\n this.transporter.close()\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Resend } from 'resend'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface ResendDriverOptions {\n /** Resend API key (from https://resend.com/api-keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be a verified domain in your Resend account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Resend transactional email API.\n * Templates are rendered with EJS at send time.\n *\n * Resend offers generous free tier and excellent deliverability,\n * making it the recommended choice for new projects.\n */\nexport class ResendDriver implements NotifierDriver {\n readonly name = 'resend'\n\n private readonly client: Resend\n\n constructor(private readonly opts: ResendDriverOptions) {\n this.client = new Resend(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const result = await this.client.emails.send({\n from: `${this.opts.fromName} <${this.opts.fromAddress}>`,\n to: [notification.to],\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n\n if ('error' in result && result.error) {\n throw new Error(`Resend error: ${result.error.message}`)\n }\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport sgMail from '@sendgrid/mail'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SendGridDriverOptions {\n /** SendGrid API key (from https://app.sendgrid.com/settings/api_keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your SendGrid account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the SendGrid API.\n * Templates are rendered with EJS at send time.\n */\nexport class SendGridDriver implements NotifierDriver {\n readonly name = 'sendgrid'\n\n constructor(private readonly opts: SendGridDriverOptions) {\n sgMail.setApiKey(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await sgMail.send({\n from: { name: this.opts.fromName, email: this.opts.fromAddress },\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n ...(text ? { text } : {}),\n })\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { TransactionalEmailsApi, TransactionalEmailsApiApiKeys, SendSmtpEmail } from '@getbrevo/brevo'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface BrevoDriverOptions {\n /** Brevo API key (from https://app.brevo.com/settings/keys/api). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your Brevo account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Brevo (formerly Sendinblue) Transactional Email API.\n * Templates are rendered with EJS at send time.\n */\nexport class BrevoDriver implements NotifierDriver {\n readonly name = 'brevo'\n\n private readonly apiInstance: TransactionalEmailsApi\n\n constructor(private readonly opts: BrevoDriverOptions) {\n this.apiInstance = new TransactionalEmailsApi()\n this.apiInstance.setApiKey(TransactionalEmailsApiApiKeys.apiKey, opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const email = new SendSmtpEmail()\n email.sender = { name: this.opts.fromName, email: this.opts.fromAddress }\n email.to = [{ email: notification.to }]\n email.subject = notification.subject ?? '(no subject)'\n email.htmlContent = html\n if (text) email.textContent = text\n\n await this.apiInstance.sendTransacEmail(email)\n }\n}\n"],"mappings":"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","names":["import_bullmq","nodemailer","ejs","ejsRender","ejs","ejsRender","sgMail","ejs","ejsRender"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -82,17 +82,45 @@ declare class DriverRegistry {
|
|
|
82
82
|
listDriverNames(): string[];
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
interface BullMQNotifierOptions {
|
|
87
|
-
/** BullMQ Queue instance — from createNotificationQueue(). */
|
|
88
|
-
queue: Queue<NotificationJob>;
|
|
89
|
-
/** Driver registry — used to determine which drivers handle each event. */
|
|
90
|
-
registry: DriverRegistry;
|
|
85
|
+
interface AuthNotificationOptions {
|
|
91
86
|
/**
|
|
92
|
-
* Public base URL of the app
|
|
93
|
-
*
|
|
87
|
+
* Public base URL of the **web app** that redeems the tokens (not this API),
|
|
88
|
+
* e.g. https://app.example.com
|
|
94
89
|
*/
|
|
95
90
|
appUrl: string;
|
|
91
|
+
/** Frontend routes the email links point to. */
|
|
92
|
+
paths?: {
|
|
93
|
+
/** Default: `/verify` */
|
|
94
|
+
verify?: string;
|
|
95
|
+
/** Default: `/reset-password` */
|
|
96
|
+
resetPassword?: string;
|
|
97
|
+
/** Default: `/verify-set-password` */
|
|
98
|
+
verifySetPassword?: string;
|
|
99
|
+
};
|
|
100
|
+
/** Verification token lifetime in ms — must match auth-management's `delay`. Default: 5 days. */
|
|
101
|
+
verifyDelay?: number;
|
|
102
|
+
/** Reset token lifetime in ms — must match auth-management's `resetDelay`. Default: 2 hours. */
|
|
103
|
+
resetDelay?: number;
|
|
104
|
+
}
|
|
105
|
+
type AuthNotificationBuilder = (type: AuthNotificationEvent, user: Record<string, unknown>, opts: AuthNotificationOptions) => Notification | null;
|
|
106
|
+
/** Human-readable duration for email copy, e.g. 432000000 → "5 days". */
|
|
107
|
+
declare function formatDuration(ms: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* Build the default Notification payload for each auth event type.
|
|
110
|
+
* Exported so apps can wrap it via the notifier's `buildNotification` option.
|
|
111
|
+
* Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.
|
|
112
|
+
*/
|
|
113
|
+
declare function buildAuthNotification(type: AuthNotificationEvent, user: Record<string, unknown>, opts: AuthNotificationOptions): Notification | null;
|
|
114
|
+
type NotifierFn = (type: string, user: Record<string, unknown>, notifierOptions: Record<string, unknown>) => Promise<void>;
|
|
115
|
+
interface NotifierBaseOptions extends AuthNotificationOptions {
|
|
116
|
+
/** Driver registry — used to determine which drivers handle each event. */
|
|
117
|
+
registry: DriverRegistry;
|
|
118
|
+
/** Replace the default email composition. Wrap `buildAuthNotification` to tweak it. */
|
|
119
|
+
buildNotification?: AuthNotificationBuilder;
|
|
120
|
+
}
|
|
121
|
+
interface BullMQNotifierOptions extends NotifierBaseOptions {
|
|
122
|
+
/** BullMQ Queue instance — from createNotificationQueue(). */
|
|
123
|
+
queue: Queue<NotificationJob>;
|
|
96
124
|
}
|
|
97
125
|
/**
|
|
98
126
|
* Create a notifier function compatible with feathers-authentication-management.
|
|
@@ -108,12 +136,7 @@ interface BullMQNotifierOptions {
|
|
|
108
136
|
* app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))
|
|
109
137
|
* ```
|
|
110
138
|
*/
|
|
111
|
-
|
|
112
|
-
/** Driver registry — used to determine which drivers handle each event. */
|
|
113
|
-
registry: DriverRegistry;
|
|
114
|
-
/** Public base URL of the app. */
|
|
115
|
-
appUrl: string;
|
|
116
|
-
}
|
|
139
|
+
type DirectNotifierOptions = NotifierBaseOptions;
|
|
117
140
|
/**
|
|
118
141
|
* Create a notifier that sends notifications directly via the driver (no queue).
|
|
119
142
|
* Use this when Redis/BullMQ is not available — suitable for API-based drivers
|
|
@@ -122,7 +145,8 @@ interface DirectNotifierOptions {
|
|
|
122
145
|
declare function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn;
|
|
123
146
|
declare function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn;
|
|
124
147
|
|
|
125
|
-
declare const NOTIFICATION_QUEUE_NAME = "feathers-baas
|
|
148
|
+
declare const NOTIFICATION_QUEUE_NAME = "feathers-baas-notifications";
|
|
149
|
+
declare const NOTIFICATION_QUEUE_PREFIX = "feathers-baas";
|
|
126
150
|
/**
|
|
127
151
|
* Create a BullMQ Queue instance for notification jobs.
|
|
128
152
|
*
|
|
@@ -257,4 +281,4 @@ declare class BrevoDriver implements NotifierDriver {
|
|
|
257
281
|
|
|
258
282
|
declare const plugin: FeathersBaasPlugin;
|
|
259
283
|
|
|
260
|
-
export { type AuthNotificationEvent, BrevoDriver, type BrevoDriverOptions, type BullMQNotifierOptions, type DirectNotifierOptions, DriverRegistry, NOTIFICATION_QUEUE_NAME, type Notification, type NotificationEvent, type NotificationJob, type NotifierDriver, type NotifierFn, ResendDriver, type ResendDriverOptions, SMTPDriver, type SMTPDriverOptions, SendGridDriver, type SendGridDriverOptions, type WorkerOptions, createBullMQNotifier, createDirectNotifier, createNotificationQueue, createNotificationWorker, plugin as default, enqueueNotification };
|
|
284
|
+
export { type AuthNotificationBuilder, type AuthNotificationEvent, type AuthNotificationOptions, BrevoDriver, type BrevoDriverOptions, type BullMQNotifierOptions, type DirectNotifierOptions, DriverRegistry, NOTIFICATION_QUEUE_NAME, NOTIFICATION_QUEUE_PREFIX, type Notification, type NotificationEvent, type NotificationJob, type NotifierDriver, type NotifierFn, ResendDriver, type ResendDriverOptions, SMTPDriver, type SMTPDriverOptions, SendGridDriver, type SendGridDriverOptions, type WorkerOptions, buildAuthNotification, createBullMQNotifier, createDirectNotifier, createNotificationQueue, createNotificationWorker, plugin as default, enqueueNotification, formatDuration };
|
package/dist/index.d.ts
CHANGED
|
@@ -82,17 +82,45 @@ declare class DriverRegistry {
|
|
|
82
82
|
listDriverNames(): string[];
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
interface BullMQNotifierOptions {
|
|
87
|
-
/** BullMQ Queue instance — from createNotificationQueue(). */
|
|
88
|
-
queue: Queue<NotificationJob>;
|
|
89
|
-
/** Driver registry — used to determine which drivers handle each event. */
|
|
90
|
-
registry: DriverRegistry;
|
|
85
|
+
interface AuthNotificationOptions {
|
|
91
86
|
/**
|
|
92
|
-
* Public base URL of the app
|
|
93
|
-
*
|
|
87
|
+
* Public base URL of the **web app** that redeems the tokens (not this API),
|
|
88
|
+
* e.g. https://app.example.com
|
|
94
89
|
*/
|
|
95
90
|
appUrl: string;
|
|
91
|
+
/** Frontend routes the email links point to. */
|
|
92
|
+
paths?: {
|
|
93
|
+
/** Default: `/verify` */
|
|
94
|
+
verify?: string;
|
|
95
|
+
/** Default: `/reset-password` */
|
|
96
|
+
resetPassword?: string;
|
|
97
|
+
/** Default: `/verify-set-password` */
|
|
98
|
+
verifySetPassword?: string;
|
|
99
|
+
};
|
|
100
|
+
/** Verification token lifetime in ms — must match auth-management's `delay`. Default: 5 days. */
|
|
101
|
+
verifyDelay?: number;
|
|
102
|
+
/** Reset token lifetime in ms — must match auth-management's `resetDelay`. Default: 2 hours. */
|
|
103
|
+
resetDelay?: number;
|
|
104
|
+
}
|
|
105
|
+
type AuthNotificationBuilder = (type: AuthNotificationEvent, user: Record<string, unknown>, opts: AuthNotificationOptions) => Notification | null;
|
|
106
|
+
/** Human-readable duration for email copy, e.g. 432000000 → "5 days". */
|
|
107
|
+
declare function formatDuration(ms: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* Build the default Notification payload for each auth event type.
|
|
110
|
+
* Exported so apps can wrap it via the notifier's `buildNotification` option.
|
|
111
|
+
* Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.
|
|
112
|
+
*/
|
|
113
|
+
declare function buildAuthNotification(type: AuthNotificationEvent, user: Record<string, unknown>, opts: AuthNotificationOptions): Notification | null;
|
|
114
|
+
type NotifierFn = (type: string, user: Record<string, unknown>, notifierOptions: Record<string, unknown>) => Promise<void>;
|
|
115
|
+
interface NotifierBaseOptions extends AuthNotificationOptions {
|
|
116
|
+
/** Driver registry — used to determine which drivers handle each event. */
|
|
117
|
+
registry: DriverRegistry;
|
|
118
|
+
/** Replace the default email composition. Wrap `buildAuthNotification` to tweak it. */
|
|
119
|
+
buildNotification?: AuthNotificationBuilder;
|
|
120
|
+
}
|
|
121
|
+
interface BullMQNotifierOptions extends NotifierBaseOptions {
|
|
122
|
+
/** BullMQ Queue instance — from createNotificationQueue(). */
|
|
123
|
+
queue: Queue<NotificationJob>;
|
|
96
124
|
}
|
|
97
125
|
/**
|
|
98
126
|
* Create a notifier function compatible with feathers-authentication-management.
|
|
@@ -108,12 +136,7 @@ interface BullMQNotifierOptions {
|
|
|
108
136
|
* app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))
|
|
109
137
|
* ```
|
|
110
138
|
*/
|
|
111
|
-
|
|
112
|
-
/** Driver registry — used to determine which drivers handle each event. */
|
|
113
|
-
registry: DriverRegistry;
|
|
114
|
-
/** Public base URL of the app. */
|
|
115
|
-
appUrl: string;
|
|
116
|
-
}
|
|
139
|
+
type DirectNotifierOptions = NotifierBaseOptions;
|
|
117
140
|
/**
|
|
118
141
|
* Create a notifier that sends notifications directly via the driver (no queue).
|
|
119
142
|
* Use this when Redis/BullMQ is not available — suitable for API-based drivers
|
|
@@ -122,7 +145,8 @@ interface DirectNotifierOptions {
|
|
|
122
145
|
declare function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn;
|
|
123
146
|
declare function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn;
|
|
124
147
|
|
|
125
|
-
declare const NOTIFICATION_QUEUE_NAME = "feathers-baas
|
|
148
|
+
declare const NOTIFICATION_QUEUE_NAME = "feathers-baas-notifications";
|
|
149
|
+
declare const NOTIFICATION_QUEUE_PREFIX = "feathers-baas";
|
|
126
150
|
/**
|
|
127
151
|
* Create a BullMQ Queue instance for notification jobs.
|
|
128
152
|
*
|
|
@@ -257,4 +281,4 @@ declare class BrevoDriver implements NotifierDriver {
|
|
|
257
281
|
|
|
258
282
|
declare const plugin: FeathersBaasPlugin;
|
|
259
283
|
|
|
260
|
-
export { type AuthNotificationEvent, BrevoDriver, type BrevoDriverOptions, type BullMQNotifierOptions, type DirectNotifierOptions, DriverRegistry, NOTIFICATION_QUEUE_NAME, type Notification, type NotificationEvent, type NotificationJob, type NotifierDriver, type NotifierFn, ResendDriver, type ResendDriverOptions, SMTPDriver, type SMTPDriverOptions, SendGridDriver, type SendGridDriverOptions, type WorkerOptions, createBullMQNotifier, createDirectNotifier, createNotificationQueue, createNotificationWorker, plugin as default, enqueueNotification };
|
|
284
|
+
export { type AuthNotificationBuilder, type AuthNotificationEvent, type AuthNotificationOptions, BrevoDriver, type BrevoDriverOptions, type BullMQNotifierOptions, type DirectNotifierOptions, DriverRegistry, NOTIFICATION_QUEUE_NAME, NOTIFICATION_QUEUE_PREFIX, type Notification, type NotificationEvent, type NotificationJob, type NotifierDriver, type NotifierFn, ResendDriver, type ResendDriverOptions, SMTPDriver, type SMTPDriverOptions, SendGridDriver, type SendGridDriverOptions, type WorkerOptions, buildAuthNotification, createBullMQNotifier, createDirectNotifier, createNotificationQueue, createNotificationWorker, plugin as default, enqueueNotification, formatDuration };
|
package/dist/index.js
CHANGED
|
@@ -33,10 +33,12 @@ var DriverRegistry = class {
|
|
|
33
33
|
|
|
34
34
|
// src/queue/producer.ts
|
|
35
35
|
import { Queue } from "bullmq";
|
|
36
|
-
var NOTIFICATION_QUEUE_NAME = "feathers-baas
|
|
36
|
+
var NOTIFICATION_QUEUE_NAME = "feathers-baas-notifications";
|
|
37
|
+
var NOTIFICATION_QUEUE_PREFIX = "feathers-baas";
|
|
37
38
|
function createNotificationQueue(redis) {
|
|
38
39
|
return new Queue(NOTIFICATION_QUEUE_NAME, {
|
|
39
40
|
connection: redis,
|
|
41
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
40
42
|
defaultJobOptions: {
|
|
41
43
|
attempts: 5,
|
|
42
44
|
backoff: {
|
|
@@ -63,14 +65,39 @@ var AUTH_MANAGEMENT_EVENT_MAP = {
|
|
|
63
65
|
passwordChange: "auth.password-change",
|
|
64
66
|
identityChange: "auth.identity-change"
|
|
65
67
|
};
|
|
66
|
-
|
|
68
|
+
var DEFAULT_VERIFY_DELAY_MS = 1e3 * 60 * 60 * 24 * 5;
|
|
69
|
+
var DEFAULT_RESET_DELAY_MS = 1e3 * 60 * 60 * 2;
|
|
70
|
+
function formatDuration(ms) {
|
|
71
|
+
const units = [
|
|
72
|
+
["day", 864e5],
|
|
73
|
+
["hour", 36e5],
|
|
74
|
+
["minute", 6e4]
|
|
75
|
+
];
|
|
76
|
+
for (const [name, size] of units) {
|
|
77
|
+
if (ms >= size && ms % size === 0) {
|
|
78
|
+
const n = ms / size;
|
|
79
|
+
return `${n} ${name}${n === 1 ? "" : "s"}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const minutes = Math.max(1, Math.round(ms / 6e4));
|
|
83
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
84
|
+
}
|
|
85
|
+
function buildLink(appUrl, path, token) {
|
|
86
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
87
|
+
const route = path.startsWith("/") ? path : `/${path}`;
|
|
88
|
+
return `${base}${route}?token=${encodeURIComponent(token ?? "")}`;
|
|
89
|
+
}
|
|
90
|
+
function buildAuthNotification(type, user, opts) {
|
|
91
|
+
const { appUrl, paths = {} } = opts;
|
|
92
|
+
const verifyExpiry = formatDuration(opts.verifyDelay ?? DEFAULT_VERIFY_DELAY_MS);
|
|
93
|
+
const resetExpiry = formatDuration(opts.resetDelay ?? DEFAULT_RESET_DELAY_MS);
|
|
67
94
|
const email = user["email"];
|
|
68
95
|
if (!email) return null;
|
|
69
96
|
switch (type) {
|
|
70
97
|
case "auth.verify-signup":
|
|
71
98
|
case "auth.resend-verify-signup": {
|
|
72
99
|
const token = user["verifyToken"];
|
|
73
|
-
const verifyUrl =
|
|
100
|
+
const verifyUrl = buildLink(appUrl, paths.verify ?? "/verify", token);
|
|
74
101
|
return {
|
|
75
102
|
to: email,
|
|
76
103
|
channel: "email",
|
|
@@ -79,15 +106,15 @@ function buildAuthNotification(type, user, appUrl) {
|
|
|
79
106
|
<h2>Welcome!</h2>
|
|
80
107
|
<p>Click the link below to verify your email address:</p>
|
|
81
108
|
<p><a href="<%= verifyUrl %>">Verify email</a></p>
|
|
82
|
-
<p>This link expires in
|
|
109
|
+
<p>This link expires in <%= expiresIn %>.</p>
|
|
83
110
|
`,
|
|
84
111
|
textTemplate: `Verify your email: <%= verifyUrl %>`,
|
|
85
|
-
data: { verifyUrl, user }
|
|
112
|
+
data: { verifyUrl, expiresIn: verifyExpiry, user }
|
|
86
113
|
};
|
|
87
114
|
}
|
|
88
115
|
case "auth.send-reset-pwd": {
|
|
89
116
|
const token = user["resetToken"];
|
|
90
|
-
const resetUrl =
|
|
117
|
+
const resetUrl = buildLink(appUrl, paths.resetPassword ?? "/reset-password", token);
|
|
91
118
|
return {
|
|
92
119
|
to: email,
|
|
93
120
|
channel: "email",
|
|
@@ -96,10 +123,10 @@ function buildAuthNotification(type, user, appUrl) {
|
|
|
96
123
|
<h2>Password reset requested</h2>
|
|
97
124
|
<p>Click the link below to reset your password:</p>
|
|
98
125
|
<p><a href="<%= resetUrl %>">Reset password</a></p>
|
|
99
|
-
<p>This link expires in
|
|
126
|
+
<p>This link expires in <%= expiresIn %>. If you did not request this, ignore this email.</p>
|
|
100
127
|
`,
|
|
101
128
|
textTemplate: `Reset your password: <%= resetUrl %>`,
|
|
102
|
-
data: { resetUrl, user }
|
|
129
|
+
data: { resetUrl, expiresIn: resetExpiry, user }
|
|
103
130
|
};
|
|
104
131
|
}
|
|
105
132
|
case "auth.reset-pwd": {
|
|
@@ -134,7 +161,7 @@ function buildAuthNotification(type, user, appUrl) {
|
|
|
134
161
|
}
|
|
135
162
|
case "auth.verify-signup-set-password": {
|
|
136
163
|
const token = user["verifyToken"];
|
|
137
|
-
const verifyUrl =
|
|
164
|
+
const verifyUrl = buildLink(appUrl, paths.verifySetPassword ?? "/verify-set-password", token);
|
|
138
165
|
return {
|
|
139
166
|
to: email,
|
|
140
167
|
channel: "email",
|
|
@@ -153,7 +180,7 @@ function createDirectNotifier(opts) {
|
|
|
153
180
|
return async (type, user, _notifierOptions) => {
|
|
154
181
|
const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type];
|
|
155
182
|
if (!eventKey) return;
|
|
156
|
-
const notification = buildAuthNotification(eventKey, user, opts
|
|
183
|
+
const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts);
|
|
157
184
|
if (!notification) return;
|
|
158
185
|
const driverNames = opts.registry.getDriversForEvent(eventKey);
|
|
159
186
|
if (driverNames.length === 0) return;
|
|
@@ -170,7 +197,7 @@ function createBullMQNotifier(opts) {
|
|
|
170
197
|
return async (type, user, _notifierOptions) => {
|
|
171
198
|
const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type];
|
|
172
199
|
if (!eventKey) return;
|
|
173
|
-
const notification = buildAuthNotification(eventKey, user, opts
|
|
200
|
+
const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts);
|
|
174
201
|
if (!notification) return;
|
|
175
202
|
const driverNames = opts.registry.getDriversForEvent(eventKey);
|
|
176
203
|
if (driverNames.length === 0) return;
|
|
@@ -198,6 +225,8 @@ function createNotificationWorker(registry, redis, opts = {}) {
|
|
|
198
225
|
},
|
|
199
226
|
{
|
|
200
227
|
connection: redis,
|
|
228
|
+
// Must match the producer, or jobs are enqueued where the worker isn't listening
|
|
229
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
201
230
|
concurrency,
|
|
202
231
|
// Don't create a new redis connection per job — reuse the pool
|
|
203
232
|
autorun: true
|
|
@@ -335,14 +364,17 @@ export {
|
|
|
335
364
|
BrevoDriver,
|
|
336
365
|
DriverRegistry,
|
|
337
366
|
NOTIFICATION_QUEUE_NAME,
|
|
367
|
+
NOTIFICATION_QUEUE_PREFIX,
|
|
338
368
|
ResendDriver,
|
|
339
369
|
SMTPDriver,
|
|
340
370
|
SendGridDriver,
|
|
371
|
+
buildAuthNotification,
|
|
341
372
|
createBullMQNotifier,
|
|
342
373
|
createDirectNotifier,
|
|
343
374
|
createNotificationQueue,
|
|
344
375
|
createNotificationWorker,
|
|
345
376
|
index_default as default,
|
|
346
|
-
enqueueNotification
|
|
377
|
+
enqueueNotification,
|
|
378
|
+
formatDuration
|
|
347
379
|
};
|
|
348
380
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/registry.ts","../src/queue/producer.ts","../src/notifier.ts","../src/queue/worker.ts","../src/drivers/smtp.driver.ts","../src/drivers/resend.driver.ts","../src/drivers/sendgrid.driver.ts","../src/drivers/brevo.driver.ts"],"sourcesContent":["// Generated by feathers-baas. Safe to edit.\nimport { createRequire } from 'node:module'\nimport type { FeathersBaasPlugin } from './plugin.js'\n\nconst pkg = createRequire(import.meta.url)('../package.json') as { version: string }\n\nconst plugin: FeathersBaasPlugin = {\n name: '@feathers-baas/plugin-notifications',\n version: pkg.version,\n async install(_app, _options) {\n // Notification drivers are configured via core's configureNotifications()\n // based on env vars. Auto-install is a no-op; discovery confirms the plugin is present.\n },\n}\n\nexport default plugin\n\nexport { DriverRegistry } from './registry.js'\nexport { createBullMQNotifier, createDirectNotifier } from './notifier.js'\nexport type { NotifierFn, BullMQNotifierOptions, DirectNotifierOptions } from './notifier.js'\n\n// Queue\nexport { createNotificationQueue, enqueueNotification, NOTIFICATION_QUEUE_NAME } from './queue/producer.js'\nexport { createNotificationWorker } from './queue/worker.js'\nexport type { WorkerOptions } from './queue/worker.js'\n\n// Drivers\nexport { SMTPDriver } from './drivers/smtp.driver.js'\nexport type { SMTPDriverOptions } from './drivers/smtp.driver.js'\nexport { ResendDriver } from './drivers/resend.driver.js'\nexport type { ResendDriverOptions } from './drivers/resend.driver.js'\nexport { SendGridDriver } from './drivers/sendgrid.driver.js'\nexport type { SendGridDriverOptions } from './drivers/sendgrid.driver.js'\nexport { BrevoDriver } from './drivers/brevo.driver.js'\nexport type { BrevoDriverOptions } from './drivers/brevo.driver.js'\n\n// Types\nexport type {\n Notification,\n NotifierDriver,\n NotificationJob,\n NotificationEvent,\n AuthNotificationEvent,\n} from './types.js'\n","// Generated by feathers-baas. Safe to edit.\nimport type { NotifierDriver, NotificationEvent } from './types.js'\n\n/**\n * DriverRegistry maps event keys to the list of drivers that should handle\n * that event.\n *\n * A single driver instance can handle multiple events (e.g. the SMTP driver\n * handles all auth.* email events).\n *\n * @example\n * ```ts\n * const registry = new DriverRegistry()\n * registry.registerDriver(smtpDriver)\n * registry.mapEvent('auth.verify-signup', 'smtp')\n * registry.mapEvent('auth.send-reset-pwd', 'smtp')\n * ```\n */\nexport class DriverRegistry {\n private readonly drivers = new Map<string, NotifierDriver>()\n private readonly eventMap = new Map<NotificationEvent, string[]>()\n\n /** Register a driver under its name. Overwrites any existing driver with the same name. */\n registerDriver(driver: NotifierDriver): void {\n this.drivers.set(driver.name, driver)\n }\n\n /** Map a notification event to a driver name. Multiple calls for the same event add drivers. */\n mapEvent(event: NotificationEvent, driverName: string): void {\n const existing = this.eventMap.get(event) ?? []\n if (!existing.includes(driverName)) {\n existing.push(driverName)\n }\n this.eventMap.set(event, existing)\n }\n\n /** Look up a driver by name. Returns undefined if not registered. */\n getDriver(name: string): NotifierDriver | undefined {\n return this.drivers.get(name)\n }\n\n /** Get all driver names mapped to the given event. */\n getDriversForEvent(event: NotificationEvent): string[] {\n return this.eventMap.get(event) ?? []\n }\n\n /** All registered driver names. */\n listDriverNames(): string[] {\n return [...this.drivers.keys()]\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Queue } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { NotificationJob } from '../types.js'\n\nexport const NOTIFICATION_QUEUE_NAME = 'feathers-baas:notifications'\n\n/**\n * Create a BullMQ Queue instance for notification jobs.\n *\n * @example\n * ```ts\n * const redis = new Redis(process.env.REDIS_URL)\n * const queue = createNotificationQueue(redis)\n * await enqueueNotification(queue, { driverName: 'smtp', notification: { ... } })\n * ```\n */\nexport function createNotificationQueue(redis: Redis): Queue<NotificationJob> {\n return new Queue<NotificationJob>(NOTIFICATION_QUEUE_NAME, {\n connection: redis,\n defaultJobOptions: {\n attempts: 5,\n backoff: {\n type: 'exponential',\n delay: 1_000, // initial delay: 1s → 2s → 4s → 8s → 16s\n },\n removeOnComplete: { count: 100 },\n removeOnFail: { count: 500 },\n },\n })\n}\n\n/**\n * Enqueue a notification job.\n *\n * @param queue BullMQ Queue instance from createNotificationQueue()\n * @param job Job payload { driverName, notification }\n */\nexport async function enqueueNotification(\n queue: Queue<NotificationJob>,\n job: NotificationJob,\n): Promise<void> {\n await queue.add('send', job)\n}\n","// Generated by feathers-baas. Safe to edit.\nimport type { Queue } from 'bullmq'\nimport type { NotificationJob, Notification, AuthNotificationEvent } from './types.js'\nimport { enqueueNotification } from './queue/producer.js'\nimport type { DriverRegistry } from './registry.js'\n\n/**\n * Auth-management notification types mapped to feathers-baas event keys.\n * feathers-authentication-management passes these type strings to the notifier.\n */\nconst AUTH_MANAGEMENT_EVENT_MAP: Record<string, AuthNotificationEvent> = {\n resendVerifySignup: 'auth.resend-verify-signup',\n verifySignup: 'auth.verify-signup',\n verifySignupSetPassword: 'auth.verify-signup-set-password',\n sendResetPwd: 'auth.send-reset-pwd',\n resetPwd: 'auth.reset-pwd',\n passwordChange: 'auth.password-change',\n identityChange: 'auth.identity-change',\n}\n\n/**\n * Build a default Notification payload for each auth event type.\n * Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.\n */\nfunction buildAuthNotification(\n type: AuthNotificationEvent,\n user: Record<string, unknown>,\n appUrl: string,\n): Notification | null {\n const email = user['email'] as string\n if (!email) return null\n\n switch (type) {\n case 'auth.verify-signup':\n case 'auth.resend-verify-signup': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = `${appUrl}/verify?token=${token ?? ''}`\n return {\n to: email,\n channel: 'email',\n subject: 'Verify your email address',\n bodyTemplate: `\n <h2>Welcome!</h2>\n <p>Click the link below to verify your email address:</p>\n <p><a href=\"<%= verifyUrl %>\">Verify email</a></p>\n <p>This link expires in 24 hours.</p>\n `,\n textTemplate: `Verify your email: <%= verifyUrl %>`,\n data: { verifyUrl, user },\n }\n }\n\n case 'auth.send-reset-pwd': {\n const token = user['resetToken'] as string | undefined\n const resetUrl = `${appUrl}/reset-password?token=${token ?? ''}`\n return {\n to: email,\n channel: 'email',\n subject: 'Reset your password',\n bodyTemplate: `\n <h2>Password reset requested</h2>\n <p>Click the link below to reset your password:</p>\n <p><a href=\"<%= resetUrl %>\">Reset password</a></p>\n <p>This link expires in 2 hours. If you did not request this, ignore this email.</p>\n `,\n textTemplate: `Reset your password: <%= resetUrl %>`,\n data: { resetUrl, user },\n }\n }\n\n case 'auth.reset-pwd': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been reset',\n bodyTemplate: `<p>Your password has been successfully reset. If you did not do this, please contact support immediately.</p>`,\n textTemplate: `Your password has been reset. If you did not do this, please contact support.`,\n data: { user },\n }\n }\n\n case 'auth.password-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been changed',\n bodyTemplate: `<p>Your account password was just changed. If this was not you, please contact support immediately.</p>`,\n textTemplate: `Your password was changed. Contact support if this wasn't you.`,\n data: { user },\n }\n }\n\n case 'auth.identity-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your email address is being changed',\n bodyTemplate: `<p>A request was made to change your email address. Click the link to confirm.</p>`,\n textTemplate: `Confirm your email change by clicking the link in the HTML email.`,\n data: { user },\n }\n }\n\n case 'auth.verify-signup-set-password': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = `${appUrl}/verify-set-password?token=${token ?? ''}`\n return {\n to: email,\n channel: 'email',\n subject: 'Set your password',\n bodyTemplate: `\n <h2>Set your password</h2>\n <p><a href=\"<%= verifyUrl %>\">Set password</a></p>\n `,\n textTemplate: `Set your password: <%= verifyUrl %>`,\n data: { verifyUrl, user },\n }\n }\n }\n}\n\nexport type NotifierFn = (\n type: string,\n user: Record<string, unknown>,\n notifierOptions: Record<string, unknown>,\n) => Promise<void>\n\nexport interface BullMQNotifierOptions {\n /** BullMQ Queue instance — from createNotificationQueue(). */\n queue: Queue<NotificationJob>\n /** Driver registry — used to determine which drivers handle each event. */\n registry: DriverRegistry\n /**\n * Public base URL of the app, e.g. https://api.example.com\n * Used to construct verification / reset URLs in email templates.\n */\n appUrl: string\n}\n\n/**\n * Create a notifier function compatible with feathers-authentication-management.\n *\n * When an auth event fires (verifySignup, sendResetPwd, etc.), this notifier:\n * 1. Maps the event type to a feathers-baas notification event key\n * 2. Looks up all registered drivers for that event\n * 3. Enqueues one BullMQ job per driver\n * 4. Returns immediately — delivery is asynchronous\n *\n * Wire into the app:\n * ```ts\n * app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))\n * ```\n */\nexport interface DirectNotifierOptions {\n /** Driver registry — used to determine which drivers handle each event. */\n registry: DriverRegistry\n /** Public base URL of the app. */\n appUrl: string\n}\n\n/**\n * Create a notifier that sends notifications directly via the driver (no queue).\n * Use this when Redis/BullMQ is not available — suitable for API-based drivers\n * (Brevo, Resend, SendGrid) in development or low-traffic environments.\n */\nexport function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return\n\n const notification = buildAuthNotification(eventKey, user, opts.appUrl)\n if (!notification) return\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return\n\n await Promise.all(\n driverNames.map(async (driverName) => {\n const driver = opts.registry.getDriver(driverName)\n if (!driver) return\n await driver.send(notification)\n }),\n )\n }\n}\n\nexport function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return // Unknown event type — no-op\n\n const notification = buildAuthNotification(eventKey, user, opts.appUrl)\n if (!notification) return // Missing required user fields — no-op\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return // No drivers configured for this event\n\n await Promise.all(\n driverNames.map((driverName) =>\n enqueueNotification(opts.queue, { driverName, notification }),\n ),\n )\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Worker } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { DriverRegistry } from '../registry.js'\nimport type { NotificationJob } from '../types.js'\nimport { NOTIFICATION_QUEUE_NAME } from './producer.js'\n\nexport interface WorkerOptions {\n /** BullMQ concurrency — number of jobs processed in parallel. */\n concurrency?: number\n}\n\n/**\n * Create and start the BullMQ worker that processes notification jobs.\n *\n * The worker looks up the driver from the registry by name, renders the\n * EJS template, and calls driver.send(). BullMQ handles retries with\n * exponential back-off automatically.\n *\n * Call worker.close() during graceful shutdown to drain in-flight jobs.\n *\n * @example\n * ```ts\n * const worker = createNotificationWorker(registry, redis, { concurrency: 5 })\n *\n * process.on('SIGTERM', async () => {\n * await worker.close()\n * })\n * ```\n */\nexport function createNotificationWorker(\n registry: DriverRegistry,\n redis: Redis,\n opts: WorkerOptions = {},\n): Worker<NotificationJob> {\n const concurrency = opts.concurrency ?? 5\n\n const worker = new Worker<NotificationJob>(\n NOTIFICATION_QUEUE_NAME,\n async (job) => {\n const { driverName, notification } = job.data\n\n const driver = registry.getDriver(driverName)\n if (!driver) {\n throw new Error(`Notification driver \"${driverName}\" is not registered`)\n }\n\n await driver.send(notification)\n },\n {\n connection: redis,\n concurrency,\n // Don't create a new redis connection per job — reuse the pool\n autorun: true,\n },\n )\n\n worker.on('failed', (job, err) => {\n // BullMQ handles retries; this is just for observability\n const jobId = job?.id ?? 'unknown'\n const message = err instanceof Error ? err.message : String(err)\n console.error(`[notifications] Job ${jobId} failed: ${message}`)\n })\n\n return worker\n}\n","// Generated by feathers-baas. Safe to edit.\nimport nodemailer from 'nodemailer'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SMTPDriverOptions {\n /** SMTP connection URL, e.g. smtp://user:pass@mail.example.com:587 */\n url: string\n /** Display name shown in the From field. */\n fromName: string\n /** Email address shown in the From field. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by nodemailer over SMTP.\n * Templates are rendered with EJS at send time.\n *\n * Use this driver for self-hosted SMTP servers (Postfix, Mailtrap, etc.).\n * For transactional email services, prefer ResendDriver.\n */\nexport class SMTPDriver implements NotifierDriver {\n readonly name = 'smtp'\n\n private readonly transporter: nodemailer.Transporter\n\n constructor(private readonly opts: SMTPDriverOptions) {\n this.transporter = nodemailer.createTransport(opts.url)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await this.transporter.sendMail({\n from: `\"${this.opts.fromName}\" <${this.opts.fromAddress}>`,\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n }\n\n /** Verify SMTP connectivity. Throws if the server is unreachable. */\n async verify(): Promise<void> {\n await this.transporter.verify()\n }\n\n /** Close the underlying connection pool. */\n close(): void {\n this.transporter.close()\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Resend } from 'resend'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface ResendDriverOptions {\n /** Resend API key (from https://resend.com/api-keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be a verified domain in your Resend account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Resend transactional email API.\n * Templates are rendered with EJS at send time.\n *\n * Resend offers generous free tier and excellent deliverability,\n * making it the recommended choice for new projects.\n */\nexport class ResendDriver implements NotifierDriver {\n readonly name = 'resend'\n\n private readonly client: Resend\n\n constructor(private readonly opts: ResendDriverOptions) {\n this.client = new Resend(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const result = await this.client.emails.send({\n from: `${this.opts.fromName} <${this.opts.fromAddress}>`,\n to: [notification.to],\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n\n if ('error' in result && result.error) {\n throw new Error(`Resend error: ${result.error.message}`)\n }\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport sgMail from '@sendgrid/mail'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SendGridDriverOptions {\n /** SendGrid API key (from https://app.sendgrid.com/settings/api_keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your SendGrid account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the SendGrid API.\n * Templates are rendered with EJS at send time.\n */\nexport class SendGridDriver implements NotifierDriver {\n readonly name = 'sendgrid'\n\n constructor(private readonly opts: SendGridDriverOptions) {\n sgMail.setApiKey(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await sgMail.send({\n from: { name: this.opts.fromName, email: this.opts.fromAddress },\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n ...(text ? { text } : {}),\n })\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { TransactionalEmailsApi, TransactionalEmailsApiApiKeys, SendSmtpEmail } from '@getbrevo/brevo'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface BrevoDriverOptions {\n /** Brevo API key (from https://app.brevo.com/settings/keys/api). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your Brevo account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Brevo (formerly Sendinblue) Transactional Email API.\n * Templates are rendered with EJS at send time.\n */\nexport class BrevoDriver implements NotifierDriver {\n readonly name = 'brevo'\n\n private readonly apiInstance: TransactionalEmailsApi\n\n constructor(private readonly opts: BrevoDriverOptions) {\n this.apiInstance = new TransactionalEmailsApi()\n this.apiInstance.setApiKey(TransactionalEmailsApiApiKeys.apiKey, opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const email = new SendSmtpEmail()\n email.sender = { name: this.opts.fromName, email: this.opts.fromAddress }\n email.to = [{ email: notification.to }]\n email.subject = notification.subject ?? '(no subject)'\n email.htmlContent = html\n if (text) email.textContent = text\n\n await this.apiInstance.sendTransacEmail(email)\n }\n}\n"],"mappings":"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","names":["ejs","ejsRender","ejs","ejsRender","ejs","ejsRender"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/registry.ts","../src/queue/producer.ts","../src/notifier.ts","../src/queue/worker.ts","../src/drivers/smtp.driver.ts","../src/drivers/resend.driver.ts","../src/drivers/sendgrid.driver.ts","../src/drivers/brevo.driver.ts"],"sourcesContent":["// Generated by feathers-baas. Safe to edit.\nimport { createRequire } from 'node:module'\nimport type { FeathersBaasPlugin } from './plugin.js'\n\nconst pkg = createRequire(import.meta.url)('../package.json') as { version: string }\n\nconst plugin: FeathersBaasPlugin = {\n name: '@feathers-baas/plugin-notifications',\n version: pkg.version,\n async install(_app, _options) {\n // Notification drivers are configured via core's configureNotifications()\n // based on env vars. Auto-install is a no-op; discovery confirms the plugin is present.\n },\n}\n\nexport default plugin\n\nexport { DriverRegistry } from './registry.js'\nexport { createBullMQNotifier, createDirectNotifier, buildAuthNotification, formatDuration } from './notifier.js'\nexport type {\n NotifierFn,\n BullMQNotifierOptions,\n DirectNotifierOptions,\n AuthNotificationOptions,\n AuthNotificationBuilder,\n} from './notifier.js'\n\n// Queue\nexport {\n createNotificationQueue,\n enqueueNotification,\n NOTIFICATION_QUEUE_NAME,\n NOTIFICATION_QUEUE_PREFIX,\n} from './queue/producer.js'\nexport { createNotificationWorker } from './queue/worker.js'\nexport type { WorkerOptions } from './queue/worker.js'\n\n// Drivers\nexport { SMTPDriver } from './drivers/smtp.driver.js'\nexport type { SMTPDriverOptions } from './drivers/smtp.driver.js'\nexport { ResendDriver } from './drivers/resend.driver.js'\nexport type { ResendDriverOptions } from './drivers/resend.driver.js'\nexport { SendGridDriver } from './drivers/sendgrid.driver.js'\nexport type { SendGridDriverOptions } from './drivers/sendgrid.driver.js'\nexport { BrevoDriver } from './drivers/brevo.driver.js'\nexport type { BrevoDriverOptions } from './drivers/brevo.driver.js'\n\n// Types\nexport type {\n Notification,\n NotifierDriver,\n NotificationJob,\n NotificationEvent,\n AuthNotificationEvent,\n} from './types.js'\n","// Generated by feathers-baas. Safe to edit.\nimport type { NotifierDriver, NotificationEvent } from './types.js'\n\n/**\n * DriverRegistry maps event keys to the list of drivers that should handle\n * that event.\n *\n * A single driver instance can handle multiple events (e.g. the SMTP driver\n * handles all auth.* email events).\n *\n * @example\n * ```ts\n * const registry = new DriverRegistry()\n * registry.registerDriver(smtpDriver)\n * registry.mapEvent('auth.verify-signup', 'smtp')\n * registry.mapEvent('auth.send-reset-pwd', 'smtp')\n * ```\n */\nexport class DriverRegistry {\n private readonly drivers = new Map<string, NotifierDriver>()\n private readonly eventMap = new Map<NotificationEvent, string[]>()\n\n /** Register a driver under its name. Overwrites any existing driver with the same name. */\n registerDriver(driver: NotifierDriver): void {\n this.drivers.set(driver.name, driver)\n }\n\n /** Map a notification event to a driver name. Multiple calls for the same event add drivers. */\n mapEvent(event: NotificationEvent, driverName: string): void {\n const existing = this.eventMap.get(event) ?? []\n if (!existing.includes(driverName)) {\n existing.push(driverName)\n }\n this.eventMap.set(event, existing)\n }\n\n /** Look up a driver by name. Returns undefined if not registered. */\n getDriver(name: string): NotifierDriver | undefined {\n return this.drivers.get(name)\n }\n\n /** Get all driver names mapped to the given event. */\n getDriversForEvent(event: NotificationEvent): string[] {\n return this.eventMap.get(event) ?? []\n }\n\n /** All registered driver names. */\n listDriverNames(): string[] {\n return [...this.drivers.keys()]\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Queue } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { NotificationJob } from '../types.js'\n\n// BullMQ rejects ':' in queue names — namespace with its `prefix` option instead.\nexport const NOTIFICATION_QUEUE_NAME = 'feathers-baas-notifications'\nexport const NOTIFICATION_QUEUE_PREFIX = 'feathers-baas'\n\n/**\n * Create a BullMQ Queue instance for notification jobs.\n *\n * @example\n * ```ts\n * const redis = new Redis(process.env.REDIS_URL)\n * const queue = createNotificationQueue(redis)\n * await enqueueNotification(queue, { driverName: 'smtp', notification: { ... } })\n * ```\n */\nexport function createNotificationQueue(redis: Redis): Queue<NotificationJob> {\n return new Queue<NotificationJob>(NOTIFICATION_QUEUE_NAME, {\n connection: redis,\n prefix: NOTIFICATION_QUEUE_PREFIX,\n defaultJobOptions: {\n attempts: 5,\n backoff: {\n type: 'exponential',\n delay: 1_000, // initial delay: 1s → 2s → 4s → 8s → 16s\n },\n removeOnComplete: { count: 100 },\n removeOnFail: { count: 500 },\n },\n })\n}\n\n/**\n * Enqueue a notification job.\n *\n * @param queue BullMQ Queue instance from createNotificationQueue()\n * @param job Job payload { driverName, notification }\n */\nexport async function enqueueNotification(\n queue: Queue<NotificationJob>,\n job: NotificationJob,\n): Promise<void> {\n await queue.add('send', job)\n}\n","// Generated by feathers-baas. Safe to edit.\nimport type { Queue } from 'bullmq'\nimport type { NotificationJob, Notification, AuthNotificationEvent } from './types.js'\nimport { enqueueNotification } from './queue/producer.js'\nimport type { DriverRegistry } from './registry.js'\n\n/**\n * Auth-management notification types mapped to feathers-baas event keys.\n * feathers-authentication-management passes these type strings to the notifier.\n */\nconst AUTH_MANAGEMENT_EVENT_MAP: Record<string, AuthNotificationEvent> = {\n resendVerifySignup: 'auth.resend-verify-signup',\n verifySignup: 'auth.verify-signup',\n verifySignupSetPassword: 'auth.verify-signup-set-password',\n sendResetPwd: 'auth.send-reset-pwd',\n resetPwd: 'auth.reset-pwd',\n passwordChange: 'auth.password-change',\n identityChange: 'auth.identity-change',\n}\n\n/** Defaults match feathers-authentication-management's own `delay` / `resetDelay`. */\nconst DEFAULT_VERIFY_DELAY_MS = 1000 * 60 * 60 * 24 * 5\nconst DEFAULT_RESET_DELAY_MS = 1000 * 60 * 60 * 2\n\nexport interface AuthNotificationOptions {\n /**\n * Public base URL of the **web app** that redeems the tokens (not this API),\n * e.g. https://app.example.com\n */\n appUrl: string\n /** Frontend routes the email links point to. */\n paths?: {\n /** Default: `/verify` */\n verify?: string\n /** Default: `/reset-password` */\n resetPassword?: string\n /** Default: `/verify-set-password` */\n verifySetPassword?: string\n }\n /** Verification token lifetime in ms — must match auth-management's `delay`. Default: 5 days. */\n verifyDelay?: number\n /** Reset token lifetime in ms — must match auth-management's `resetDelay`. Default: 2 hours. */\n resetDelay?: number\n}\n\nexport type AuthNotificationBuilder = (\n type: AuthNotificationEvent,\n user: Record<string, unknown>,\n opts: AuthNotificationOptions,\n) => Notification | null\n\n/** Human-readable duration for email copy, e.g. 432000000 → \"5 days\". */\nexport function formatDuration(ms: number): string {\n const units: [string, number][] = [\n ['day', 86_400_000],\n ['hour', 3_600_000],\n ['minute', 60_000],\n ]\n for (const [name, size] of units) {\n if (ms >= size && ms % size === 0) {\n const n = ms / size\n return `${n} ${name}${n === 1 ? '' : 's'}`\n }\n }\n const minutes = Math.max(1, Math.round(ms / 60_000))\n return `${minutes} minute${minutes === 1 ? '' : 's'}`\n}\n\nfunction buildLink(appUrl: string, path: string, token: string | undefined): string {\n const base = appUrl.replace(/\\/+$/, '')\n const route = path.startsWith('/') ? path : `/${path}`\n return `${base}${route}?token=${encodeURIComponent(token ?? '')}`\n}\n\n/**\n * Build the default Notification payload for each auth event type.\n * Exported so apps can wrap it via the notifier's `buildNotification` option.\n * Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.\n */\nexport function buildAuthNotification(\n type: AuthNotificationEvent,\n user: Record<string, unknown>,\n opts: AuthNotificationOptions,\n): Notification | null {\n const { appUrl, paths = {} } = opts\n const verifyExpiry = formatDuration(opts.verifyDelay ?? DEFAULT_VERIFY_DELAY_MS)\n const resetExpiry = formatDuration(opts.resetDelay ?? DEFAULT_RESET_DELAY_MS)\n const email = user['email'] as string\n if (!email) return null\n\n switch (type) {\n case 'auth.verify-signup':\n case 'auth.resend-verify-signup': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = buildLink(appUrl, paths.verify ?? '/verify', token)\n return {\n to: email,\n channel: 'email',\n subject: 'Verify your email address',\n bodyTemplate: `\n <h2>Welcome!</h2>\n <p>Click the link below to verify your email address:</p>\n <p><a href=\"<%= verifyUrl %>\">Verify email</a></p>\n <p>This link expires in <%= expiresIn %>.</p>\n `,\n textTemplate: `Verify your email: <%= verifyUrl %>`,\n data: { verifyUrl, expiresIn: verifyExpiry, user },\n }\n }\n\n case 'auth.send-reset-pwd': {\n const token = user['resetToken'] as string | undefined\n const resetUrl = buildLink(appUrl, paths.resetPassword ?? '/reset-password', token)\n return {\n to: email,\n channel: 'email',\n subject: 'Reset your password',\n bodyTemplate: `\n <h2>Password reset requested</h2>\n <p>Click the link below to reset your password:</p>\n <p><a href=\"<%= resetUrl %>\">Reset password</a></p>\n <p>This link expires in <%= expiresIn %>. If you did not request this, ignore this email.</p>\n `,\n textTemplate: `Reset your password: <%= resetUrl %>`,\n data: { resetUrl, expiresIn: resetExpiry, user },\n }\n }\n\n case 'auth.reset-pwd': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been reset',\n bodyTemplate: `<p>Your password has been successfully reset. If you did not do this, please contact support immediately.</p>`,\n textTemplate: `Your password has been reset. If you did not do this, please contact support.`,\n data: { user },\n }\n }\n\n case 'auth.password-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your password has been changed',\n bodyTemplate: `<p>Your account password was just changed. If this was not you, please contact support immediately.</p>`,\n textTemplate: `Your password was changed. Contact support if this wasn't you.`,\n data: { user },\n }\n }\n\n case 'auth.identity-change': {\n return {\n to: email,\n channel: 'email',\n subject: 'Your email address is being changed',\n bodyTemplate: `<p>A request was made to change your email address. Click the link to confirm.</p>`,\n textTemplate: `Confirm your email change by clicking the link in the HTML email.`,\n data: { user },\n }\n }\n\n case 'auth.verify-signup-set-password': {\n const token = user['verifyToken'] as string | undefined\n const verifyUrl = buildLink(appUrl, paths.verifySetPassword ?? '/verify-set-password', token)\n return {\n to: email,\n channel: 'email',\n subject: 'Set your password',\n bodyTemplate: `\n <h2>Set your password</h2>\n <p><a href=\"<%= verifyUrl %>\">Set password</a></p>\n `,\n textTemplate: `Set your password: <%= verifyUrl %>`,\n data: { verifyUrl, user },\n }\n }\n }\n}\n\nexport type NotifierFn = (\n type: string,\n user: Record<string, unknown>,\n notifierOptions: Record<string, unknown>,\n) => Promise<void>\n\ninterface NotifierBaseOptions extends AuthNotificationOptions {\n /** Driver registry — used to determine which drivers handle each event. */\n registry: DriverRegistry\n /** Replace the default email composition. Wrap `buildAuthNotification` to tweak it. */\n buildNotification?: AuthNotificationBuilder\n}\n\nexport interface BullMQNotifierOptions extends NotifierBaseOptions {\n /** BullMQ Queue instance — from createNotificationQueue(). */\n queue: Queue<NotificationJob>\n}\n\n/**\n * Create a notifier function compatible with feathers-authentication-management.\n *\n * When an auth event fires (verifySignup, sendResetPwd, etc.), this notifier:\n * 1. Maps the event type to a feathers-baas notification event key\n * 2. Looks up all registered drivers for that event\n * 3. Enqueues one BullMQ job per driver\n * 4. Returns immediately — delivery is asynchronous\n *\n * Wire into the app:\n * ```ts\n * app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))\n * ```\n */\nexport type DirectNotifierOptions = NotifierBaseOptions\n\n/**\n * Create a notifier that sends notifications directly via the driver (no queue).\n * Use this when Redis/BullMQ is not available — suitable for API-based drivers\n * (Brevo, Resend, SendGrid) in development or low-traffic environments.\n */\nexport function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return\n\n const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts)\n if (!notification) return\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return\n\n await Promise.all(\n driverNames.map(async (driverName) => {\n const driver = opts.registry.getDriver(driverName)\n if (!driver) return\n await driver.send(notification)\n }),\n )\n }\n}\n\nexport function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn {\n return async (type, user, _notifierOptions) => {\n const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]\n if (!eventKey) return // Unknown event type — no-op\n\n const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts)\n if (!notification) return // Missing required user fields — no-op\n\n const driverNames = opts.registry.getDriversForEvent(eventKey)\n if (driverNames.length === 0) return // No drivers configured for this event\n\n await Promise.all(\n driverNames.map((driverName) =>\n enqueueNotification(opts.queue, { driverName, notification }),\n ),\n )\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Worker } from 'bullmq'\nimport type { Redis } from 'ioredis'\nimport type { DriverRegistry } from '../registry.js'\nimport type { NotificationJob } from '../types.js'\nimport { NOTIFICATION_QUEUE_NAME, NOTIFICATION_QUEUE_PREFIX } from './producer.js'\n\nexport interface WorkerOptions {\n /** BullMQ concurrency — number of jobs processed in parallel. */\n concurrency?: number\n}\n\n/**\n * Create and start the BullMQ worker that processes notification jobs.\n *\n * The worker looks up the driver from the registry by name, renders the\n * EJS template, and calls driver.send(). BullMQ handles retries with\n * exponential back-off automatically.\n *\n * Call worker.close() during graceful shutdown to drain in-flight jobs.\n *\n * @example\n * ```ts\n * const worker = createNotificationWorker(registry, redis, { concurrency: 5 })\n *\n * process.on('SIGTERM', async () => {\n * await worker.close()\n * })\n * ```\n */\nexport function createNotificationWorker(\n registry: DriverRegistry,\n redis: Redis,\n opts: WorkerOptions = {},\n): Worker<NotificationJob> {\n const concurrency = opts.concurrency ?? 5\n\n const worker = new Worker<NotificationJob>(\n NOTIFICATION_QUEUE_NAME,\n async (job) => {\n const { driverName, notification } = job.data\n\n const driver = registry.getDriver(driverName)\n if (!driver) {\n throw new Error(`Notification driver \"${driverName}\" is not registered`)\n }\n\n await driver.send(notification)\n },\n {\n connection: redis,\n // Must match the producer, or jobs are enqueued where the worker isn't listening\n prefix: NOTIFICATION_QUEUE_PREFIX,\n concurrency,\n // Don't create a new redis connection per job — reuse the pool\n autorun: true,\n },\n )\n\n worker.on('failed', (job, err) => {\n // BullMQ handles retries; this is just for observability\n const jobId = job?.id ?? 'unknown'\n const message = err instanceof Error ? err.message : String(err)\n console.error(`[notifications] Job ${jobId} failed: ${message}`)\n })\n\n return worker\n}\n","// Generated by feathers-baas. Safe to edit.\nimport nodemailer from 'nodemailer'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SMTPDriverOptions {\n /** SMTP connection URL, e.g. smtp://user:pass@mail.example.com:587 */\n url: string\n /** Display name shown in the From field. */\n fromName: string\n /** Email address shown in the From field. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by nodemailer over SMTP.\n * Templates are rendered with EJS at send time.\n *\n * Use this driver for self-hosted SMTP servers (Postfix, Mailtrap, etc.).\n * For transactional email services, prefer ResendDriver.\n */\nexport class SMTPDriver implements NotifierDriver {\n readonly name = 'smtp'\n\n private readonly transporter: nodemailer.Transporter\n\n constructor(private readonly opts: SMTPDriverOptions) {\n this.transporter = nodemailer.createTransport(opts.url)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await this.transporter.sendMail({\n from: `\"${this.opts.fromName}\" <${this.opts.fromAddress}>`,\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n }\n\n /** Verify SMTP connectivity. Throws if the server is unreachable. */\n async verify(): Promise<void> {\n await this.transporter.verify()\n }\n\n /** Close the underlying connection pool. */\n close(): void {\n this.transporter.close()\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { Resend } from 'resend'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface ResendDriverOptions {\n /** Resend API key (from https://resend.com/api-keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be a verified domain in your Resend account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Resend transactional email API.\n * Templates are rendered with EJS at send time.\n *\n * Resend offers generous free tier and excellent deliverability,\n * making it the recommended choice for new projects.\n */\nexport class ResendDriver implements NotifierDriver {\n readonly name = 'resend'\n\n private readonly client: Resend\n\n constructor(private readonly opts: ResendDriverOptions) {\n this.client = new Resend(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const result = await this.client.emails.send({\n from: `${this.opts.fromName} <${this.opts.fromAddress}>`,\n to: [notification.to],\n subject: notification.subject ?? '(no subject)',\n html,\n text,\n })\n\n if ('error' in result && result.error) {\n throw new Error(`Resend error: ${result.error.message}`)\n }\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport sgMail from '@sendgrid/mail'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface SendGridDriverOptions {\n /** SendGrid API key (from https://app.sendgrid.com/settings/api_keys). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your SendGrid account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the SendGrid API.\n * Templates are rendered with EJS at send time.\n */\nexport class SendGridDriver implements NotifierDriver {\n readonly name = 'sendgrid'\n\n constructor(private readonly opts: SendGridDriverOptions) {\n sgMail.setApiKey(opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n await sgMail.send({\n from: { name: this.opts.fromName, email: this.opts.fromAddress },\n to: notification.to,\n subject: notification.subject ?? '(no subject)',\n html,\n ...(text ? { text } : {}),\n })\n }\n}\n","// Generated by feathers-baas. Safe to edit.\nimport { TransactionalEmailsApi, TransactionalEmailsApiApiKeys, SendSmtpEmail } from '@getbrevo/brevo'\nimport * as ejs from 'ejs'\nimport type { NotifierDriver, Notification } from '../types.js'\n\n// ejs is a CommonJS module — alias the render function defensively\nconst ejsRender = (ejs as unknown as { render?: typeof ejs.render }).render ?? ejs.render\n\nexport interface BrevoDriverOptions {\n /** Brevo API key (from https://app.brevo.com/settings/keys/api). */\n apiKey: string\n /** Display name shown in the From field. */\n fromName: string\n /** Verified sender address. Must be verified in your Brevo account. */\n fromAddress: string\n}\n\n/**\n * Email driver backed by the Brevo (formerly Sendinblue) Transactional Email API.\n * Templates are rendered with EJS at send time.\n */\nexport class BrevoDriver implements NotifierDriver {\n readonly name = 'brevo'\n\n private readonly apiInstance: TransactionalEmailsApi\n\n constructor(private readonly opts: BrevoDriverOptions) {\n this.apiInstance = new TransactionalEmailsApi()\n this.apiInstance.setApiKey(TransactionalEmailsApiApiKeys.apiKey, opts.apiKey)\n }\n\n async send(notification: Notification): Promise<void> {\n const html = ejsRender(notification.bodyTemplate, notification.data, { async: false })\n const text = notification.textTemplate\n ? ejsRender(notification.textTemplate, notification.data, { async: false })\n : undefined\n\n const email = new SendSmtpEmail()\n email.sender = { name: this.opts.fromName, email: this.opts.fromAddress }\n email.to = [{ email: notification.to }]\n email.subject = notification.subject ?? '(no subject)'\n email.htmlContent = html\n if (text) email.textContent = text\n\n await this.apiInstance.sendTransacEmail(email)\n }\n}\n"],"mappings":"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","names":["ejs","ejsRender","ejs","ejsRender","ejs","ejsRender"]}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -16,11 +16,22 @@ const plugin: FeathersBaasPlugin = {
|
|
|
16
16
|
export default plugin
|
|
17
17
|
|
|
18
18
|
export { DriverRegistry } from './registry.js'
|
|
19
|
-
export { createBullMQNotifier, createDirectNotifier } from './notifier.js'
|
|
20
|
-
export type {
|
|
19
|
+
export { createBullMQNotifier, createDirectNotifier, buildAuthNotification, formatDuration } from './notifier.js'
|
|
20
|
+
export type {
|
|
21
|
+
NotifierFn,
|
|
22
|
+
BullMQNotifierOptions,
|
|
23
|
+
DirectNotifierOptions,
|
|
24
|
+
AuthNotificationOptions,
|
|
25
|
+
AuthNotificationBuilder,
|
|
26
|
+
} from './notifier.js'
|
|
21
27
|
|
|
22
28
|
// Queue
|
|
23
|
-
export {
|
|
29
|
+
export {
|
|
30
|
+
createNotificationQueue,
|
|
31
|
+
enqueueNotification,
|
|
32
|
+
NOTIFICATION_QUEUE_NAME,
|
|
33
|
+
NOTIFICATION_QUEUE_PREFIX,
|
|
34
|
+
} from './queue/producer.js'
|
|
24
35
|
export { createNotificationWorker } from './queue/worker.js'
|
|
25
36
|
export type { WorkerOptions } from './queue/worker.js'
|
|
26
37
|
|
package/src/notifier.ts
CHANGED
|
@@ -18,15 +18,73 @@ const AUTH_MANAGEMENT_EVENT_MAP: Record<string, AuthNotificationEvent> = {
|
|
|
18
18
|
identityChange: 'auth.identity-change',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Defaults match feathers-authentication-management's own `delay` / `resetDelay`. */
|
|
22
|
+
const DEFAULT_VERIFY_DELAY_MS = 1000 * 60 * 60 * 24 * 5
|
|
23
|
+
const DEFAULT_RESET_DELAY_MS = 1000 * 60 * 60 * 2
|
|
24
|
+
|
|
25
|
+
export interface AuthNotificationOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Public base URL of the **web app** that redeems the tokens (not this API),
|
|
28
|
+
* e.g. https://app.example.com
|
|
29
|
+
*/
|
|
30
|
+
appUrl: string
|
|
31
|
+
/** Frontend routes the email links point to. */
|
|
32
|
+
paths?: {
|
|
33
|
+
/** Default: `/verify` */
|
|
34
|
+
verify?: string
|
|
35
|
+
/** Default: `/reset-password` */
|
|
36
|
+
resetPassword?: string
|
|
37
|
+
/** Default: `/verify-set-password` */
|
|
38
|
+
verifySetPassword?: string
|
|
39
|
+
}
|
|
40
|
+
/** Verification token lifetime in ms — must match auth-management's `delay`. Default: 5 days. */
|
|
41
|
+
verifyDelay?: number
|
|
42
|
+
/** Reset token lifetime in ms — must match auth-management's `resetDelay`. Default: 2 hours. */
|
|
43
|
+
resetDelay?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type AuthNotificationBuilder = (
|
|
47
|
+
type: AuthNotificationEvent,
|
|
48
|
+
user: Record<string, unknown>,
|
|
49
|
+
opts: AuthNotificationOptions,
|
|
50
|
+
) => Notification | null
|
|
51
|
+
|
|
52
|
+
/** Human-readable duration for email copy, e.g. 432000000 → "5 days". */
|
|
53
|
+
export function formatDuration(ms: number): string {
|
|
54
|
+
const units: [string, number][] = [
|
|
55
|
+
['day', 86_400_000],
|
|
56
|
+
['hour', 3_600_000],
|
|
57
|
+
['minute', 60_000],
|
|
58
|
+
]
|
|
59
|
+
for (const [name, size] of units) {
|
|
60
|
+
if (ms >= size && ms % size === 0) {
|
|
61
|
+
const n = ms / size
|
|
62
|
+
return `${n} ${name}${n === 1 ? '' : 's'}`
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const minutes = Math.max(1, Math.round(ms / 60_000))
|
|
66
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildLink(appUrl: string, path: string, token: string | undefined): string {
|
|
70
|
+
const base = appUrl.replace(/\/+$/, '')
|
|
71
|
+
const route = path.startsWith('/') ? path : `/${path}`
|
|
72
|
+
return `${base}${route}?token=${encodeURIComponent(token ?? '')}`
|
|
73
|
+
}
|
|
74
|
+
|
|
21
75
|
/**
|
|
22
|
-
* Build
|
|
76
|
+
* Build the default Notification payload for each auth event type.
|
|
77
|
+
* Exported so apps can wrap it via the notifier's `buildNotification` option.
|
|
23
78
|
* Templates use EJS — `<%= variable %>` for output, `<% code %>` for logic.
|
|
24
79
|
*/
|
|
25
|
-
function buildAuthNotification(
|
|
80
|
+
export function buildAuthNotification(
|
|
26
81
|
type: AuthNotificationEvent,
|
|
27
82
|
user: Record<string, unknown>,
|
|
28
|
-
|
|
83
|
+
opts: AuthNotificationOptions,
|
|
29
84
|
): Notification | null {
|
|
85
|
+
const { appUrl, paths = {} } = opts
|
|
86
|
+
const verifyExpiry = formatDuration(opts.verifyDelay ?? DEFAULT_VERIFY_DELAY_MS)
|
|
87
|
+
const resetExpiry = formatDuration(opts.resetDelay ?? DEFAULT_RESET_DELAY_MS)
|
|
30
88
|
const email = user['email'] as string
|
|
31
89
|
if (!email) return null
|
|
32
90
|
|
|
@@ -34,7 +92,7 @@ function buildAuthNotification(
|
|
|
34
92
|
case 'auth.verify-signup':
|
|
35
93
|
case 'auth.resend-verify-signup': {
|
|
36
94
|
const token = user['verifyToken'] as string | undefined
|
|
37
|
-
const verifyUrl =
|
|
95
|
+
const verifyUrl = buildLink(appUrl, paths.verify ?? '/verify', token)
|
|
38
96
|
return {
|
|
39
97
|
to: email,
|
|
40
98
|
channel: 'email',
|
|
@@ -43,16 +101,16 @@ function buildAuthNotification(
|
|
|
43
101
|
<h2>Welcome!</h2>
|
|
44
102
|
<p>Click the link below to verify your email address:</p>
|
|
45
103
|
<p><a href="<%= verifyUrl %>">Verify email</a></p>
|
|
46
|
-
<p>This link expires in
|
|
104
|
+
<p>This link expires in <%= expiresIn %>.</p>
|
|
47
105
|
`,
|
|
48
106
|
textTemplate: `Verify your email: <%= verifyUrl %>`,
|
|
49
|
-
data: { verifyUrl, user },
|
|
107
|
+
data: { verifyUrl, expiresIn: verifyExpiry, user },
|
|
50
108
|
}
|
|
51
109
|
}
|
|
52
110
|
|
|
53
111
|
case 'auth.send-reset-pwd': {
|
|
54
112
|
const token = user['resetToken'] as string | undefined
|
|
55
|
-
const resetUrl =
|
|
113
|
+
const resetUrl = buildLink(appUrl, paths.resetPassword ?? '/reset-password', token)
|
|
56
114
|
return {
|
|
57
115
|
to: email,
|
|
58
116
|
channel: 'email',
|
|
@@ -61,10 +119,10 @@ function buildAuthNotification(
|
|
|
61
119
|
<h2>Password reset requested</h2>
|
|
62
120
|
<p>Click the link below to reset your password:</p>
|
|
63
121
|
<p><a href="<%= resetUrl %>">Reset password</a></p>
|
|
64
|
-
<p>This link expires in
|
|
122
|
+
<p>This link expires in <%= expiresIn %>. If you did not request this, ignore this email.</p>
|
|
65
123
|
`,
|
|
66
124
|
textTemplate: `Reset your password: <%= resetUrl %>`,
|
|
67
|
-
data: { resetUrl, user },
|
|
125
|
+
data: { resetUrl, expiresIn: resetExpiry, user },
|
|
68
126
|
}
|
|
69
127
|
}
|
|
70
128
|
|
|
@@ -103,7 +161,7 @@ function buildAuthNotification(
|
|
|
103
161
|
|
|
104
162
|
case 'auth.verify-signup-set-password': {
|
|
105
163
|
const token = user['verifyToken'] as string | undefined
|
|
106
|
-
const verifyUrl =
|
|
164
|
+
const verifyUrl = buildLink(appUrl, paths.verifySetPassword ?? '/verify-set-password', token)
|
|
107
165
|
return {
|
|
108
166
|
to: email,
|
|
109
167
|
channel: 'email',
|
|
@@ -125,16 +183,16 @@ export type NotifierFn = (
|
|
|
125
183
|
notifierOptions: Record<string, unknown>,
|
|
126
184
|
) => Promise<void>
|
|
127
185
|
|
|
128
|
-
|
|
129
|
-
/** BullMQ Queue instance — from createNotificationQueue(). */
|
|
130
|
-
queue: Queue<NotificationJob>
|
|
186
|
+
interface NotifierBaseOptions extends AuthNotificationOptions {
|
|
131
187
|
/** Driver registry — used to determine which drivers handle each event. */
|
|
132
188
|
registry: DriverRegistry
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
189
|
+
/** Replace the default email composition. Wrap `buildAuthNotification` to tweak it. */
|
|
190
|
+
buildNotification?: AuthNotificationBuilder
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface BullMQNotifierOptions extends NotifierBaseOptions {
|
|
194
|
+
/** BullMQ Queue instance — from createNotificationQueue(). */
|
|
195
|
+
queue: Queue<NotificationJob>
|
|
138
196
|
}
|
|
139
197
|
|
|
140
198
|
/**
|
|
@@ -151,12 +209,7 @@ export interface BullMQNotifierOptions {
|
|
|
151
209
|
* app.set('notifier', createBullMQNotifier({ queue, registry, appUrl }))
|
|
152
210
|
* ```
|
|
153
211
|
*/
|
|
154
|
-
export
|
|
155
|
-
/** Driver registry — used to determine which drivers handle each event. */
|
|
156
|
-
registry: DriverRegistry
|
|
157
|
-
/** Public base URL of the app. */
|
|
158
|
-
appUrl: string
|
|
159
|
-
}
|
|
212
|
+
export type DirectNotifierOptions = NotifierBaseOptions
|
|
160
213
|
|
|
161
214
|
/**
|
|
162
215
|
* Create a notifier that sends notifications directly via the driver (no queue).
|
|
@@ -168,7 +221,7 @@ export function createDirectNotifier(opts: DirectNotifierOptions): NotifierFn {
|
|
|
168
221
|
const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]
|
|
169
222
|
if (!eventKey) return
|
|
170
223
|
|
|
171
|
-
const notification = buildAuthNotification(eventKey, user, opts
|
|
224
|
+
const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts)
|
|
172
225
|
if (!notification) return
|
|
173
226
|
|
|
174
227
|
const driverNames = opts.registry.getDriversForEvent(eventKey)
|
|
@@ -189,7 +242,7 @@ export function createBullMQNotifier(opts: BullMQNotifierOptions): NotifierFn {
|
|
|
189
242
|
const eventKey = AUTH_MANAGEMENT_EVENT_MAP[type]
|
|
190
243
|
if (!eventKey) return // Unknown event type — no-op
|
|
191
244
|
|
|
192
|
-
const notification = buildAuthNotification(eventKey, user, opts
|
|
245
|
+
const notification = (opts.buildNotification ?? buildAuthNotification)(eventKey, user, opts)
|
|
193
246
|
if (!notification) return // Missing required user fields — no-op
|
|
194
247
|
|
|
195
248
|
const driverNames = opts.registry.getDriversForEvent(eventKey)
|
package/src/queue/producer.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { Queue } from 'bullmq'
|
|
|
3
3
|
import type { Redis } from 'ioredis'
|
|
4
4
|
import type { NotificationJob } from '../types.js'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// BullMQ rejects ':' in queue names — namespace with its `prefix` option instead.
|
|
7
|
+
export const NOTIFICATION_QUEUE_NAME = 'feathers-baas-notifications'
|
|
8
|
+
export const NOTIFICATION_QUEUE_PREFIX = 'feathers-baas'
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Create a BullMQ Queue instance for notification jobs.
|
|
@@ -18,6 +20,7 @@ export const NOTIFICATION_QUEUE_NAME = 'feathers-baas:notifications'
|
|
|
18
20
|
export function createNotificationQueue(redis: Redis): Queue<NotificationJob> {
|
|
19
21
|
return new Queue<NotificationJob>(NOTIFICATION_QUEUE_NAME, {
|
|
20
22
|
connection: redis,
|
|
23
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
21
24
|
defaultJobOptions: {
|
|
22
25
|
attempts: 5,
|
|
23
26
|
backoff: {
|
package/src/queue/worker.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Worker } from 'bullmq'
|
|
|
3
3
|
import type { Redis } from 'ioredis'
|
|
4
4
|
import type { DriverRegistry } from '../registry.js'
|
|
5
5
|
import type { NotificationJob } from '../types.js'
|
|
6
|
-
import { NOTIFICATION_QUEUE_NAME } from './producer.js'
|
|
6
|
+
import { NOTIFICATION_QUEUE_NAME, NOTIFICATION_QUEUE_PREFIX } from './producer.js'
|
|
7
7
|
|
|
8
8
|
export interface WorkerOptions {
|
|
9
9
|
/** BullMQ concurrency — number of jobs processed in parallel. */
|
|
@@ -49,6 +49,8 @@ export function createNotificationWorker(
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
connection: redis,
|
|
52
|
+
// Must match the producer, or jobs are enqueued where the worker isn't listening
|
|
53
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
52
54
|
concurrency,
|
|
53
55
|
// Don't create a new redis connection per job — reuse the pool
|
|
54
56
|
autorun: true,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { buildAuthNotification, createDirectNotifier, formatDuration } from '../src/notifier.js'
|
|
3
|
+
import { DriverRegistry } from '../src/registry.js'
|
|
4
|
+
import type { Notification, NotifierDriver } from '../src/types.js'
|
|
5
|
+
|
|
6
|
+
const user = { email: 'alice@example.com', verifyToken: 'v/tok', resetToken: 'r-tok' }
|
|
7
|
+
|
|
8
|
+
describe('formatDuration', () => {
|
|
9
|
+
it('formats whole units', () => {
|
|
10
|
+
expect(formatDuration(1000 * 60 * 60 * 24 * 5)).toBe('5 days')
|
|
11
|
+
expect(formatDuration(1000 * 60 * 60 * 2)).toBe('2 hours')
|
|
12
|
+
expect(formatDuration(1000 * 60 * 60)).toBe('1 hour')
|
|
13
|
+
expect(formatDuration(1000 * 60 * 90)).toBe('90 minutes')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('buildAuthNotification', () => {
|
|
18
|
+
it('uses the default paths and auth-management default lifetimes', () => {
|
|
19
|
+
const verify = buildAuthNotification('auth.resend-verify-signup', user, { appUrl: 'https://app.example.com/' })
|
|
20
|
+
expect(verify?.data?.['verifyUrl']).toBe('https://app.example.com/verify?token=v%2Ftok')
|
|
21
|
+
expect(verify?.data?.['expiresIn']).toBe('5 days')
|
|
22
|
+
|
|
23
|
+
const reset = buildAuthNotification('auth.send-reset-pwd', user, { appUrl: 'https://app.example.com' })
|
|
24
|
+
expect(reset?.data?.['resetUrl']).toBe('https://app.example.com/reset-password?token=r-tok')
|
|
25
|
+
expect(reset?.data?.['expiresIn']).toBe('2 hours')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('honours custom paths and delays', () => {
|
|
29
|
+
const opts = {
|
|
30
|
+
appUrl: 'https://app.example.com',
|
|
31
|
+
paths: { verify: '/auth/confirm', resetPassword: 'auth/reset' },
|
|
32
|
+
verifyDelay: 1000 * 60 * 60 * 24,
|
|
33
|
+
resetDelay: 1000 * 60 * 30,
|
|
34
|
+
}
|
|
35
|
+
const verify = buildAuthNotification('auth.verify-signup', user, opts)
|
|
36
|
+
expect(verify?.data?.['verifyUrl']).toBe('https://app.example.com/auth/confirm?token=v%2Ftok')
|
|
37
|
+
expect(verify?.data?.['expiresIn']).toBe('1 day')
|
|
38
|
+
|
|
39
|
+
const reset = buildAuthNotification('auth.send-reset-pwd', user, opts)
|
|
40
|
+
expect(reset?.data?.['resetUrl']).toBe('https://app.example.com/auth/reset?token=r-tok')
|
|
41
|
+
expect(reset?.data?.['expiresIn']).toBe('30 minutes')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('createDirectNotifier', () => {
|
|
46
|
+
function setup() {
|
|
47
|
+
const send = vi.fn<(n: Notification) => Promise<void>>().mockResolvedValue(undefined)
|
|
48
|
+
const driver: NotifierDriver = { name: 'fake', send }
|
|
49
|
+
const registry = new DriverRegistry()
|
|
50
|
+
registry.registerDriver(driver)
|
|
51
|
+
registry.mapEvent('auth.resend-verify-signup', 'fake')
|
|
52
|
+
return { registry, send }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('passes path options through to the email', async () => {
|
|
56
|
+
const { registry, send } = setup()
|
|
57
|
+
const notify = createDirectNotifier({ registry, appUrl: 'https://app.example.com', paths: { verify: '/confirm' } })
|
|
58
|
+
await notify('resendVerifySignup', user, {})
|
|
59
|
+
expect(send.mock.calls[0]![0].data?.['verifyUrl']).toBe('https://app.example.com/confirm?token=v%2Ftok')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('lets apps override composition with buildNotification', async () => {
|
|
63
|
+
const { registry, send } = setup()
|
|
64
|
+
const notify = createDirectNotifier({
|
|
65
|
+
registry,
|
|
66
|
+
appUrl: 'https://app.example.com',
|
|
67
|
+
buildNotification: (type, u, opts) => {
|
|
68
|
+
const base = buildAuthNotification(type, u, opts)
|
|
69
|
+
return base && { ...base, subject: 'Custom subject' }
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
await notify('resendVerifySignup', user, {})
|
|
73
|
+
expect(send.mock.calls[0]![0].subject).toBe('Custom subject')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Startup smoke test against the real bullmq — it rejects ':' in queue names at construction time.
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { Queue } from 'bullmq'
|
|
4
|
+
import { NOTIFICATION_QUEUE_NAME, NOTIFICATION_QUEUE_PREFIX } from '../src/queue/producer.js'
|
|
5
|
+
|
|
6
|
+
describe('notification queue name', () => {
|
|
7
|
+
it('is accepted by the bullmq Queue constructor', () => {
|
|
8
|
+
let queue: Queue | undefined
|
|
9
|
+
expect(() => {
|
|
10
|
+
// Port 0 + lazyConnect: never actually reaches for Redis
|
|
11
|
+
queue = new Queue(NOTIFICATION_QUEUE_NAME, {
|
|
12
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
13
|
+
connection: { host: '127.0.0.1', port: 0, lazyConnect: true, enableOfflineQueue: false },
|
|
14
|
+
})
|
|
15
|
+
}).not.toThrow()
|
|
16
|
+
queue?.on('error', () => {})
|
|
17
|
+
expect(queue?.name).toBe(NOTIFICATION_QUEUE_NAME)
|
|
18
|
+
})
|
|
19
|
+
})
|
package/test/queue.test.ts
CHANGED
|
@@ -28,7 +28,12 @@ vi.mock('bullmq', () => ({
|
|
|
28
28
|
}))
|
|
29
29
|
|
|
30
30
|
// ─── Imports after mocking ────────────────────────────────────────────────────
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createNotificationQueue,
|
|
33
|
+
enqueueNotification,
|
|
34
|
+
NOTIFICATION_QUEUE_NAME,
|
|
35
|
+
NOTIFICATION_QUEUE_PREFIX,
|
|
36
|
+
} from '../src/queue/producer.js'
|
|
32
37
|
import { createNotificationWorker } from '../src/queue/worker.js'
|
|
33
38
|
import { DriverRegistry } from '../src/registry.js'
|
|
34
39
|
import type { NotifierDriver } from '../src/types.js'
|
|
@@ -52,6 +57,7 @@ describe('createNotificationQueue', () => {
|
|
|
52
57
|
expect(vi.mocked(Queue)).toHaveBeenCalledWith(
|
|
53
58
|
NOTIFICATION_QUEUE_NAME,
|
|
54
59
|
expect.objectContaining({
|
|
60
|
+
prefix: NOTIFICATION_QUEUE_PREFIX,
|
|
55
61
|
defaultJobOptions: expect.objectContaining({
|
|
56
62
|
attempts: 5,
|
|
57
63
|
backoff: expect.objectContaining({ type: 'exponential' }),
|
|
@@ -97,7 +103,7 @@ describe('createNotificationWorker', () => {
|
|
|
97
103
|
expect(vi.mocked(Worker)).toHaveBeenCalledWith(
|
|
98
104
|
NOTIFICATION_QUEUE_NAME,
|
|
99
105
|
expect.any(Function),
|
|
100
|
-
expect.objectContaining({ concurrency: 3 }),
|
|
106
|
+
expect.objectContaining({ concurrency: 3, prefix: NOTIFICATION_QUEUE_PREFIX }),
|
|
101
107
|
)
|
|
102
108
|
})
|
|
103
109
|
|