@murumets-ee/notifications 0.12.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.
@@ -0,0 +1,500 @@
1
+ import { _ as RenderedEmail, a as NOTIFICATION_TOPICS, c as NotificationRealtimePayload, d as NotifyOptions, f as NotifyResult, g as RenderedContent, h as RenderArgs, i as DEFAULT_RECIPIENT_CAP, l as NotificationTopic, m as Recipients, n as ChannelDeliveryRecord, o as NotificationChannelId, p as RecipientContext, r as ChannelDeliveryState, s as NotificationPreferences, t as ChannelDefinition, u as NotificationType, v as RenderedInApp } from "./types-B8qKgKMj.mjs";
2
+ import { clearNotificationTypeRegistry, defineNotificationType, getAllNotificationTypes, getNotificationType, registerNotificationType } from "./define.mjs";
3
+ import { n as notificationPreferencesSettings, r as resolveEnabledChannels, t as notificationPreferencesSchema } from "./preferences-BCkY2j9L.mjs";
4
+ import { JobDefinition } from "@murumets-ee/queue/client";
5
+ import { z } from "zod";
6
+ import * as _$_murumets_ee_db0 from "@murumets-ee/db";
7
+ import * as _$drizzle_orm0 from "drizzle-orm";
8
+ import { Logger } from "@murumets-ee/core";
9
+ import * as _$drizzle_orm_postgres_js0 from "drizzle-orm/postgres-js";
10
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
11
+ import { MailMessage, MailProvider, SendResult } from "@murumets-ee/mail";
12
+ import * as _$drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
13
+
14
+ //#region src/channels/types.d.ts
15
+ interface ChannelSendContext {
16
+ /** The notification row id this delivery is for. */
17
+ notificationId: string;
18
+ /** Whether this is a fresh row (`created`) or a group-collapse update. */
19
+ kind: 'created' | 'updated';
20
+ /** Type id (e.g. `ticketing.message.created`). */
21
+ typeId: string;
22
+ /** Recipient context (id + locale + verified email if any). */
23
+ recipient: RecipientContext;
24
+ /** Rendered content for ALL channels — channel picks its own slice. */
25
+ rendered: RenderedContent;
26
+ /**
27
+ * Total unread count for the recipient AFTER this notification — used
28
+ * in realtime payloads so connected tabs can update the badge in one
29
+ * round-trip without an extra fetch.
30
+ */
31
+ unreadCount: number;
32
+ /**
33
+ * Optional Drizzle transaction handle — present when the publisher
34
+ * called `notify(..., { tx })`. Channels that perform side-effects
35
+ * which must roll back with the publisher's tx (canonically, the
36
+ * email channel's `queue.enqueue`) MUST forward it. Channels with
37
+ * purely ephemeral side-effects (in-app realtime publish) ignore it.
38
+ *
39
+ * Documented in PLAN-OUTBOX Phase 5 / PR E (a.k.a. PLAN-NOTIFICATIONS PR F).
40
+ *
41
+ * Spelled `T | undefined` (not bare optional) because the publisher's
42
+ * tx is passed in as a destructured variable that may be undefined,
43
+ * and the repo enables `exactOptionalPropertyTypes` — bare optional
44
+ * would refuse the assignment.
45
+ */
46
+ tx?: PostgresJsDatabase | undefined;
47
+ }
48
+ interface NotificationChannel {
49
+ id: NotificationChannelId;
50
+ /**
51
+ * Perform the channel's delivery. Returning the delivery record lets
52
+ * the notify pipeline merge per-channel state into the row's `channels`
53
+ * array under a single TX-free UPDATE per notify call.
54
+ *
55
+ * Implementations MUST NOT throw — return a `failed` record with the
56
+ * reason in `detail` instead. A throw would leak into the publisher's
57
+ * call stack and break user-facing routes that fan out notifications
58
+ * non-critically (e.g. ticket reply succeeded, but notify() crashed).
59
+ */
60
+ send(ctx: ChannelSendContext): Promise<ChannelDeliveryRecord>;
61
+ }
62
+ //#endregion
63
+ //#region src/channels/email.d.ts
64
+ /**
65
+ * Payload schema for the `notifications:send-email` job.
66
+ *
67
+ * Deliberately minimal — only the row id + the recipient. Render content
68
+ * is re-derived from the row's `payload` + the type registry at handler
69
+ * run time, so a group-collapse update between enqueue and run is reflected
70
+ * in the sent email.
71
+ *
72
+ * `recipientUserId` is repeated alongside `notificationId` so the handler's
73
+ * row read can scope to `(id, recipientUserId)` without needing a second
74
+ * lookup — defense-in-depth against a misbehaving plugin queueing a job
75
+ * for a row owned by a different user.
76
+ */
77
+ declare const sendEmailJobPayloadSchema: z.ZodObject<{
78
+ notificationId: z.ZodString;
79
+ recipientUserId: z.ZodString;
80
+ }, "strip", z.ZodTypeAny, {
81
+ notificationId: string;
82
+ recipientUserId: string;
83
+ }, {
84
+ notificationId: string;
85
+ recipientUserId: string;
86
+ }>;
87
+ type SendEmailJobPayload = z.infer<typeof sendEmailJobPayloadSchema>;
88
+ /**
89
+ * Queue job definition. Plugin wiring registers a handler against this
90
+ * definition at init time when the mail provider is available.
91
+ */
92
+ declare const notificationsSendEmailJob: JobDefinition<SendEmailJobPayload>;
93
+ /**
94
+ * Minimal subset of `QueueClient.enqueue` the email channel relies on.
95
+ * Defining it as a shape (instead of importing the whole class) lets tests
96
+ * inject a stub without spinning up the queue table.
97
+ *
98
+ * The third `options` argument is structurally-typed against the bits of
99
+ * `EnqueueOptions` this channel actually forwards — currently just `tx`
100
+ * for outbox-style atomic enqueue (PLAN-OUTBOX Phase 5 / PR E). The real
101
+ * `QueueClient.enqueue` accepts a wider `EnqueueOptions`, which is
102
+ * structurally assignable to this narrower shape.
103
+ */
104
+ interface QueueEnqueueShape {
105
+ enqueue(job: JobDefinition<SendEmailJobPayload>, payload: SendEmailJobPayload, options?: {
106
+ tx?: PostgresJsDatabase | undefined;
107
+ }): Promise<string>;
108
+ }
109
+ interface EmailChannelConfig {
110
+ /** Queue client used to enqueue the send job. */
111
+ queue: QueueEnqueueShape;
112
+ /** Optional logger; child-logger from the plugin is preferred. */
113
+ logger?: Logger | undefined;
114
+ }
115
+ /**
116
+ * Build the `email` channel. Returns a `NotificationChannel` whose `send`
117
+ * enqueues a queue job — never blocks on mail I/O.
118
+ */
119
+ declare function createEmailChannel(config: EmailChannelConfig): NotificationChannel;
120
+ interface SendEmailHandlerConfig {
121
+ db: PostgresJsDatabase;
122
+ /** Mail provider — usually `getMailConfig().provider`. */
123
+ mail: MailProvider;
124
+ /** From-address. Plugin resolves this from `mail().defaultFrom` or its own opt. */
125
+ from: string;
126
+ /** Default locale fallback when the recipient has no per-user locale. */
127
+ defaultLocale?: string;
128
+ logger?: Logger | undefined;
129
+ }
130
+ /**
131
+ * Subset of `JobContext` the send-email handler reads. Declared structurally
132
+ * so we don't depend on a `JobHandler` import that the queue package doesn't
133
+ * re-export from any subpath today. Function-param contravariance lets a
134
+ * `(SendEmailJobContext) => Promise<void>` be assigned to the broader
135
+ * `JobHandler<SendEmailJobPayload>` shape `registerJob` expects.
136
+ */
137
+ interface SendEmailJobContext {
138
+ payload: SendEmailJobPayload;
139
+ /** Number of attempts so far (incl. the current one). Surfaced in failure logs. */
140
+ attempts: number;
141
+ }
142
+ type SendEmailHandler = (job: SendEmailJobContext) => Promise<void>;
143
+ /**
144
+ * Build the queue-handler closure. Plugin wiring calls
145
+ * `registerJob(notificationsSendEmailJob, createSendEmailHandler({...}))`.
146
+ *
147
+ * The closure is a thin adapter: it builds DB-backed deps and forwards to
148
+ * `runSendEmailJob(deps, ...)`, which is the testable orchestration core.
149
+ */
150
+ declare function createSendEmailHandler(config: SendEmailHandlerConfig): SendEmailHandler;
151
+ /**
152
+ * Subset of `notificationsTable.makeClient(...).findOne` return shape that
153
+ * `runSendEmailJob` reads. Re-declared structurally so tests don't need a
154
+ * real DB to construct one.
155
+ */
156
+ interface NotificationRowSnapshot {
157
+ id: string;
158
+ type: string;
159
+ payload: Record<string, unknown>;
160
+ channels: ChannelDeliveryRecord[];
161
+ }
162
+ /**
163
+ * Recipient subset returned by `fetchRecipient`. Mirrors `RecipientContext`
164
+ * but spelled out so tests don't need to import the full type for stubs.
165
+ */
166
+ interface SendEmailRecipient {
167
+ id: string;
168
+ name?: string | undefined;
169
+ email?: string | undefined;
170
+ locale: string;
171
+ }
172
+ /**
173
+ * Lower-level deps used by `runSendEmailJob`. The shipping
174
+ * `createSendEmailHandler` builds these from a `SendEmailHandlerConfig`;
175
+ * unit tests inject their own stubs to drive each branch without touching
176
+ * the DB or mail provider.
177
+ */
178
+ interface SendEmailJobDeps {
179
+ fetchRow(id: string, recipientUserId: string): Promise<NotificationRowSnapshot | null>;
180
+ fetchRecipient(userId: string): Promise<SendEmailRecipient | null>;
181
+ lookupType(typeId: string): {
182
+ render: (args: {
183
+ payload: Record<string, unknown>;
184
+ recipient: SendEmailRecipient;
185
+ locale: string;
186
+ }) => {
187
+ email?: RenderedEmail | undefined;
188
+ };
189
+ } | undefined;
190
+ sendMail(message: MailMessage): Promise<SendResult>;
191
+ flipChannelState(id: string, recipientUserId: string, entry: ChannelDeliveryRecord): Promise<void>;
192
+ from: string;
193
+ logger?: Logger | undefined;
194
+ }
195
+ /**
196
+ * Run the send-email job logic against pluggable deps.
197
+ *
198
+ * Behaviour summary:
199
+ * - Row missing → log + return (archived/deleted between enqueue and run).
200
+ * - Type missing → log + return (deregistered between enqueue and run).
201
+ * - Recipient missing or email unverified at run time → flip channel
202
+ * state to `skipped_unverified`, return cleanly.
203
+ * - Render produced no `email` slice → flip to `failed`, log, return
204
+ * cleanly (no retry — render is deterministic).
205
+ * - mail.send threw → flip to `failed` with detail, RE-THROW so the
206
+ * queue retries until maxRetries.
207
+ * - mail.send returned → flip to `delivered`.
208
+ */
209
+ declare function runSendEmailJob(deps: SendEmailJobDeps, job: SendEmailJobContext): Promise<void>;
210
+ //#endregion
211
+ //#region src/channels/in-app.d.ts
212
+ declare const inAppChannel: NotificationChannel;
213
+ //#endregion
214
+ //#region src/throttle.d.ts
215
+ /**
216
+ * Per-channel in-memory throttle - sliding window per (userId, typeId, channel).
217
+ *
218
+ * v1 lives in process memory; multi-process slip is acceptable for the v1
219
+ * targets ("5 emails per 15 min per user"). PLAN-NOTIFICATIONS section 6 Q2
220
+ * calls out the trade-off and the upgrade path (Postgres-backed counter) if
221
+ * the slip ever shows up in production.
222
+ *
223
+ * Format `<count>/<window>` - window is `<n><s|m|h>` (e.g. `5/15m`).
224
+ *
225
+ * Over-cap returns `false` from `tryConsume`. The caller (notify pipeline)
226
+ * records a `throttled` ChannelDeliveryRecord on the row instead of
227
+ * enqueueing the channel work.
228
+ */
229
+ interface ParsedThrottle {
230
+ /** Max events allowed inside the window. */
231
+ count: number;
232
+ /** Window length in milliseconds. */
233
+ windowMs: number;
234
+ }
235
+ declare function parseThrottle(spec: string): ParsedThrottle;
236
+ /**
237
+ * Tracks per-(userId, typeId, channel) event timestamps in memory.
238
+ *
239
+ * Memory bound: each key keeps at most `count` timestamps; entries with
240
+ * no recent activity are evicted on the next consume call against that
241
+ * key. The unbounded growth concern is therefore the *number of distinct
242
+ * keys*, not per-key length. v2 may add a periodic sweeper if the key
243
+ * count ever shows up in heap profiles.
244
+ */
245
+ declare class ThrottleStore {
246
+ private readonly events;
247
+ private readonly nowFn;
248
+ constructor(nowFn?: () => number);
249
+ /**
250
+ * @returns `true` if the event fits in the window (and is recorded);
251
+ * `false` if it would exceed the cap (no record made - caller should
252
+ * mark the delivery as `throttled` and skip the channel work).
253
+ */
254
+ tryConsume(key: string, spec: string): boolean;
255
+ /**
256
+ * Build a stable throttle key from the per-recipient identity tuple.
257
+ * Uses `:` as a separator - none of the components (better-auth
258
+ * user ids, dot-namespaced type ids, camelCase channel ids) ever
259
+ * contain `:`, so collisions are impossible.
260
+ */
261
+ static buildKey(userId: string, typeId: string, channel: string): string;
262
+ /** @internal - for tests. */
263
+ reset(): void;
264
+ }
265
+ //#endregion
266
+ //#region src/client.d.ts
267
+ /**
268
+ * Resolver for per-user preferences. Allows the plugin to wire up a
269
+ * read-through cache without forcing the client to depend on a specific
270
+ * settings client implementation.
271
+ */
272
+ type PreferencesResolver = (userId: string) => Promise<NotificationPreferences>;
273
+ interface NotificationClientConfig {
274
+ db: PostgresJsDatabase;
275
+ logger?: Logger;
276
+ /** Default locale fallback when a recipient has no per-user locale. */
277
+ defaultLocale?: string;
278
+ /** Recipient cap for `resolveUsers()`. Defaults to 1000. */
279
+ recipientCap?: number;
280
+ /** Channel implementations. Keyed by channel id. Defaults to `inApp` only. */
281
+ channels?: Record<string, NotificationChannel>;
282
+ /** Resolver for per-user preferences. Defaults to "always empty" → type defaults apply. */
283
+ preferencesResolver?: PreferencesResolver;
284
+ /** In-memory throttle store. Tests inject a deterministic clock. */
285
+ throttleStore?: ThrottleStore;
286
+ }
287
+ declare class NotificationClient {
288
+ private readonly db;
289
+ private readonly logger?;
290
+ private readonly defaultLocale;
291
+ private readonly recipientCap;
292
+ private readonly channels;
293
+ private readonly preferencesResolver;
294
+ private readonly throttleStore;
295
+ private readonly notificationsClient;
296
+ constructor(config: NotificationClientConfig);
297
+ /**
298
+ * Resolve the table client to use for notification row writes. Mirrors
299
+ * `QueueClient.resolveJobsClient` (PR A of PLAN-OUTBOX). When `tx` is
300
+ * provided, every INSERT/UPDATE on `toolkit_notifications` participates
301
+ * in the caller's transaction; otherwise the package's owned client is
302
+ * used. Recipient lookup against the auth `user` table is a SELECT and
303
+ * does not need tx-binding.
304
+ */
305
+ private resolveNotificationsClient;
306
+ /**
307
+ * Publish a notification to one or more recipients. See PLAN-NOTIFICATIONS §3.3.
308
+ *
309
+ * When `options.tx` is set, every row write and every email-channel
310
+ * queue enqueue participates in the caller's transaction — see the
311
+ * file-level comment for the full contract.
312
+ */
313
+ notify<TPayload extends Record<string, unknown>>(options: NotifyOptions<TPayload>): Promise<NotifyResult>;
314
+ /**
315
+ * Per-recipient: collapse-or-insert the row, then dispatch each surviving
316
+ * channel under the throttle gate, then persist channel delivery state.
317
+ *
318
+ * `notificationsClient` is the tx-resolved table client from `notify()` —
319
+ * passed in instead of read from `this.notificationsClient` so every
320
+ * row write inside this method participates in the caller's tx when set.
321
+ * `tx` is forwarded to channels (the email channel needs it to attach
322
+ * `queue.enqueue` to the same tx).
323
+ */
324
+ private persistAndDispatch;
325
+ /**
326
+ * Count of unread (readAt IS NULL) notifications for a user. Drives the
327
+ * realtime payload's `unreadCount` and the bell badge.
328
+ */
329
+ unreadCount(userId: string): Promise<number>;
330
+ /**
331
+ * Result of `markRead` — `mutated` distinguishes "we updated readAt now"
332
+ * from "row was already read, no-op" so the route can decide whether to
333
+ * fire a realtime event.
334
+ */
335
+ markRead(userId: string, notificationId: string): Promise<{
336
+ id: string;
337
+ type: string;
338
+ readAt: Date;
339
+ mutated: boolean;
340
+ } | null>;
341
+ /** Mark all unread notifications for a user as read. Returns the count affected. */
342
+ markAllRead(userId: string): Promise<number>;
343
+ /**
344
+ * Soft-archive a notification. Returns the archived row or null if not
345
+ * addressable. `mutated` is false on a second call (already archived).
346
+ */
347
+ archive(userId: string, notificationId: string): Promise<{
348
+ id: string;
349
+ type: string;
350
+ mutated: boolean;
351
+ } | null>;
352
+ }
353
+ declare function setActiveNotificationClient(client: NotificationClient | null): void;
354
+ declare function getActiveNotificationClient(): NotificationClient | null;
355
+ /**
356
+ * Free-function publisher — the canonical caller-facing API. Resolves the
357
+ * active client wired up by the `notifications()` plugin's init hook.
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * import { notify } from '@murumets-ee/notifications'
362
+ *
363
+ * await notify({
364
+ * type: TicketingMessageCreated,
365
+ * recipients: { userIds: [agentId] },
366
+ * payload: { ticketId, messageId, ticketSubject, authorName },
367
+ * })
368
+ * ```
369
+ */
370
+ declare function notify<TPayload extends Record<string, unknown>>(options: NotifyOptions<TPayload>): Promise<NotifyResult>;
371
+ //#endregion
372
+ //#region src/errors.d.ts
373
+ /**
374
+ * Errors thrown by `@murumets-ee/notifications`.
375
+ *
376
+ * All extend `Error` so callers can `instanceof` discriminate. Names match
377
+ * the class so logger output reads cleanly.
378
+ */
379
+ declare class RecipientRequiredError extends Error {
380
+ constructor();
381
+ }
382
+ declare class NotificationFanoutTooLargeError extends Error {
383
+ readonly attemptedSize: number;
384
+ readonly cap: number;
385
+ constructor(attemptedSize: number, cap: number);
386
+ }
387
+ declare class UnknownNotificationTypeError extends Error {
388
+ readonly typeId: string;
389
+ constructor(typeId: string);
390
+ }
391
+ declare class UnsupportedChannelError extends Error {
392
+ readonly typeId: string;
393
+ readonly channelId: string;
394
+ constructor(typeId: string, channelId: string);
395
+ }
396
+ //#endregion
397
+ //#region src/notifications-table.d.ts
398
+ declare const notificationsTable: {
399
+ table: _$drizzle_orm_pg_core0.PgTableWithColumns<{
400
+ name: string;
401
+ schema: undefined;
402
+ columns: {
403
+ [x: string]: _$drizzle_orm_pg_core0.PgColumn<{
404
+ name: string;
405
+ tableName: string;
406
+ dataType: _$drizzle_orm0.ColumnDataType;
407
+ columnType: string;
408
+ data: unknown;
409
+ driverParam: unknown;
410
+ notNull: false;
411
+ hasDefault: false;
412
+ isPrimaryKey: false;
413
+ isAutoincrement: false;
414
+ hasRuntimeDefault: false;
415
+ enumValues: string[] | undefined;
416
+ baseColumn: never;
417
+ identity: undefined;
418
+ generated: undefined;
419
+ }, {}, {}>;
420
+ };
421
+ dialect: "pg";
422
+ }>;
423
+ schema: _$_murumets_ee_db0.TableDefinition<{
424
+ readonly id: _$_murumets_ee_db0.ColumnFactory<string, "uuid", true, true>;
425
+ readonly recipientUserId: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
426
+ readonly type: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
427
+ readonly payload: _$_murumets_ee_db0.ColumnFactory<Record<string, unknown>, "jsonb", true, true>;
428
+ readonly channels: _$_murumets_ee_db0.ColumnFactory<ChannelDeliveryRecord[], "jsonb", true, true>;
429
+ readonly groupKey: _$_murumets_ee_db0.ColumnFactory<string, "varchar", false, false>;
430
+ readonly readAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", false, false>;
431
+ readonly archivedAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", false, false>;
432
+ readonly createdAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", true, true>;
433
+ readonly updatedAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", true, true>;
434
+ }>;
435
+ columnKinds: Readonly<Record<string, _$_murumets_ee_db0.ColumnKind>>;
436
+ primaryKeyColumns: readonly string[];
437
+ makeClient: (db: _$drizzle_orm_postgres_js0.PostgresJsDatabase) => _$_murumets_ee_db0.TableClient<{
438
+ readonly id: _$_murumets_ee_db0.ColumnFactory<string, "uuid", true, true>;
439
+ readonly recipientUserId: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
440
+ readonly type: _$_murumets_ee_db0.ColumnFactory<string, "varchar", true, false>;
441
+ readonly payload: _$_murumets_ee_db0.ColumnFactory<Record<string, unknown>, "jsonb", true, true>;
442
+ readonly channels: _$_murumets_ee_db0.ColumnFactory<ChannelDeliveryRecord[], "jsonb", true, true>;
443
+ readonly groupKey: _$_murumets_ee_db0.ColumnFactory<string, "varchar", false, false>;
444
+ readonly readAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", false, false>;
445
+ readonly archivedAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", false, false>;
446
+ readonly createdAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", true, true>;
447
+ readonly updatedAt: _$_murumets_ee_db0.ColumnFactory<Date, "timestamp", true, true>;
448
+ }, _$drizzle_orm_pg_core0.PgTableWithColumns<{
449
+ name: string;
450
+ schema: undefined;
451
+ columns: {
452
+ [x: string]: _$drizzle_orm_pg_core0.PgColumn<{
453
+ name: string;
454
+ tableName: string;
455
+ dataType: _$drizzle_orm0.ColumnDataType;
456
+ columnType: string;
457
+ data: unknown;
458
+ driverParam: unknown;
459
+ notNull: false;
460
+ hasDefault: false;
461
+ isPrimaryKey: false;
462
+ isAutoincrement: false;
463
+ hasRuntimeDefault: false;
464
+ enumValues: string[] | undefined;
465
+ baseColumn: never;
466
+ identity: undefined;
467
+ generated: undefined;
468
+ }, {}, {}>;
469
+ };
470
+ dialect: "pg";
471
+ }>>;
472
+ };
473
+ /** Backward-compatible re-export following the queue/jobs convention. */
474
+ declare const toolkitNotifications: _$drizzle_orm_pg_core0.PgTableWithColumns<{
475
+ name: string;
476
+ schema: undefined;
477
+ columns: {
478
+ [x: string]: _$drizzle_orm_pg_core0.PgColumn<{
479
+ name: string;
480
+ tableName: string;
481
+ dataType: _$drizzle_orm0.ColumnDataType;
482
+ columnType: string;
483
+ data: unknown;
484
+ driverParam: unknown;
485
+ notNull: false;
486
+ hasDefault: false;
487
+ isPrimaryKey: false;
488
+ isAutoincrement: false;
489
+ hasRuntimeDefault: false;
490
+ enumValues: string[] | undefined;
491
+ baseColumn: never;
492
+ identity: undefined;
493
+ generated: undefined;
494
+ }, {}, {}>;
495
+ };
496
+ dialect: "pg";
497
+ }>;
498
+ //#endregion
499
+ export { type ChannelDefinition, type ChannelDeliveryRecord, type ChannelDeliveryState, type ChannelSendContext, DEFAULT_RECIPIENT_CAP, type EmailChannelConfig, NOTIFICATION_TOPICS, type NotificationChannel, type NotificationChannelId, NotificationClient, type NotificationClientConfig, NotificationFanoutTooLargeError, type NotificationPreferences, type NotificationRealtimePayload, type NotificationRowSnapshot, type NotificationTopic, type NotificationType, type NotifyOptions, type NotifyResult, type PreferencesResolver, type QueueEnqueueShape, type RecipientContext, RecipientRequiredError, type Recipients, type RenderArgs, type RenderedContent, type RenderedEmail, type RenderedInApp, type SendEmailHandler, type SendEmailHandlerConfig, type SendEmailJobContext, type SendEmailJobDeps, type SendEmailJobPayload, type SendEmailRecipient, ThrottleStore, UnknownNotificationTypeError, UnsupportedChannelError, clearNotificationTypeRegistry, createEmailChannel, createSendEmailHandler, defineNotificationType, getActiveNotificationClient, getAllNotificationTypes, getNotificationType, inAppChannel, notificationPreferencesSchema, notificationPreferencesSettings, notificationsSendEmailJob, notificationsTable, notify, parseThrottle, registerNotificationType, resolveEnabledChannels, runSendEmailJob, sendEmailJobPayloadSchema, setActiveNotificationClient, toolkitNotifications };
500
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/channels/types.ts","../src/channels/email.ts","../src/channels/in-app.ts","../src/throttle.ts","../src/client.ts","../src/errors.ts","../src/notifications-table.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{a as e,i as t,n,o as r,t as i}from"./email-DgfO6gZR.mjs";import{n as a,t as o}from"./types-Dy_AGX6X.mjs";import{clearNotificationTypeRegistry as s,defineNotificationType as c,getAllNotificationTypes as l,getNotificationType as u,registerNotificationType as d}from"./define.mjs";import{a as f,c as p,i as m,o as h,r as g,s as _}from"./recipients-DDN8AJzX.mjs";import{a as v,c as y,i as b,l as x,n as S,o as C,r as w,s as T,t as E,u as D}from"./client-CtklhnNF.mjs";throw Error(`This module cannot be imported from a Client Component module. It should only be used from a Server Component.`);export{o as DEFAULT_RECIPIENT_CAP,a as NOTIFICATION_TOPICS,E as NotificationClient,g as NotificationFanoutTooLargeError,m as RecipientRequiredError,v as ThrottleStore,f as UnknownNotificationTypeError,h as UnsupportedChannelError,s as clearNotificationTypeRegistry,i as createEmailChannel,n as createSendEmailHandler,c as defineNotificationType,S as getActiveNotificationClient,l as getAllNotificationTypes,u as getNotificationType,D as inAppChannel,T as notificationPreferencesSchema,y as notificationPreferencesSettings,t as notificationsSendEmailJob,_ as notificationsTable,w as notify,C as parseThrottle,d as registerNotificationType,x as resolveEnabledChannels,e as runSendEmailJob,r as sendEmailJobPayloadSchema,b as setActiveNotificationClient,p as toolkitNotifications};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../node_modules/.pnpm/server-only@0.0.1/node_modules/server-only/index.js"],"sourcesContent":["throw new Error(\n \"This module cannot be imported from a Client Component module. \" +\n \"It should only be used from a Server Component.\"\n);\n"],"x_google_ignoreList":[0],"mappings":"udAAA,MAAU,MACR,iHAED"}
@@ -0,0 +1,69 @@
1
+ import { o as NotificationChannelId, u as NotificationType } from "./types-B8qKgKMj.mjs";
2
+ import { r as resolveEnabledChannels } from "./preferences-BCkY2j9L.mjs";
3
+ import { Plugin } from "@murumets-ee/core";
4
+
5
+ //#region src/plugin.d.ts
6
+ interface NotificationsPluginOptions {
7
+ /**
8
+ * Notification types contributed at the consumer level (i.e. defined in
9
+ * the app, not in a plugin). Plugin-contributed types are also collected
10
+ * via `Plugin.shared.notifications` and merged in at init time.
11
+ */
12
+ types?: NotificationType[];
13
+ /**
14
+ * Default locale for renders when the recipient has no per-user locale.
15
+ * Defaults to `'en'`.
16
+ */
17
+ defaultLocale?: string;
18
+ /**
19
+ * Maximum number of recipients a single `resolveUsers()` call may return.
20
+ * Defaults to 1000 — bounds the blast radius of a misbehaving permission
21
+ * resolver. See PLAN-NOTIFICATIONS §6 Q6.
22
+ */
23
+ recipientCap?: number;
24
+ /**
25
+ * Per-channel options. Currently only `email` is configurable.
26
+ *
27
+ * The email channel auto-enables when both `@murumets-ee/mail` (with a
28
+ * provider) and `@murumets-ee/queue` are initialised in the same app.
29
+ * Set `email.enabled: false` to opt out even when both are available
30
+ * (e.g. an environment that should only deliver in-app).
31
+ */
32
+ channels?: {
33
+ email?: {
34
+ /**
35
+ * Override the default opt-in. `true` forces email enabled (throws on
36
+ * init if mail is unavailable), `false` disables it explicitly,
37
+ * `undefined` (default) auto-enables when mail+queue are present.
38
+ */
39
+ enabled?: boolean;
40
+ /**
41
+ * From-address. Falls back to `mail().defaultFrom`. One of the two
42
+ * MUST be configured or the email channel skips registration with
43
+ * a warning.
44
+ */
45
+ from?: string;
46
+ };
47
+ };
48
+ }
49
+ declare function notifications(options?: NotificationsPluginOptions): Plugin;
50
+ /**
51
+ * Cascade-loop guard for the queue terminal notifier.
52
+ *
53
+ * The notifications package owns `notifications:send-email` (and any
54
+ * future `notifications:*` jobs). If the mail provider is down, every
55
+ * send-email job dies after 3 retries. Without this guard each death
56
+ * would call `notify(QueueJobDead)` → enqueue one more send-email job
57
+ * per admin → each dies → geometric blow-up.
58
+ *
59
+ * The leading-edge alerter in `@murumets-ee/queue/alerter` still emails
60
+ * operators about send-email failures via its dedupe-window path, so
61
+ * the failure is still surfaced — just not via the bell drawer's
62
+ * per-admin path that would self-amplify.
63
+ *
64
+ * Exported for unit testing.
65
+ */
66
+ declare function isQueueDeadEventNotifiable(jobType: string): boolean;
67
+ //#endregion
68
+ export { type NotificationChannelId, NotificationsPluginOptions, isQueueDeadEventNotifiable, notifications, resolveEnabledChannels };
69
+ //# sourceMappingURL=plugin.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;UAoCiB,0BAAA;EA4C8D;;AAuQ/E;;;EA7SE,KAAA,GAAQ,gBAAA;EA6SgD;;;;EAxSxD,aAAA;;;;;;EAMA,YAAA;;;;;;;;;EASA,QAAA;IACE,KAAA;;;;;;MAME,OAAA;;;;;;MAMA,IAAA;IAAA;EAAA;AAAA;AAAA,iBAKU,aAAA,CAAc,OAAA,GAAS,0BAAA,GAAkC,MAAA;;;;;;;;;;;;;;;;;iBAuQzD,0BAAA,CAA2B,OAAA"}
@@ -0,0 +1,2 @@
1
+ import"./types-Dy_AGX6X.mjs";import{registerNotificationType as e}from"./define.mjs";import{c as t}from"./recipients-DDN8AJzX.mjs";import{c as n,i as r,l as i,r as a,t as o,u as s}from"./client-CtklhnNF.mjs";import{notificationsRoutes as c}from"./admin.mjs";import{schemaRegistry as l}from"@murumets-ee/db";import{user as u}from"@murumets-ee/auth/schema";import{eq as d}from"drizzle-orm";async function f(e){return(await e.select({id:u.id}).from(u).where(d(u.role,`admin`))).map(e=>e.id)}function p(i={}){let a=i.types??[];return{name:`@murumets-ee/notifications`,server:{tables:{toolkitNotifications:t},routes:[c()],init:async c=>{l.has(`toolkit_notifications`)||l.register(`toolkit_notifications`,t);for(let t of a)e(t);for(let t of c.plugins.all()){if(t.name===`@murumets-ee/notifications`)continue;let n=t.shared?.notifications;if(n)for(let t of n)e(t)}let u=async e=>{let{createSettingsClient:t}=await import(`@murumets-ee/settings`);return await t(n,{app:c,scopeId:e}).get(`prefs`)??{}},d={inApp:s},f=i.channels?.email;f?.enabled!==!1&&await h({app:c,channels:d,forced:f?.enabled===!0,from:f?.from,defaultLocale:i.defaultLocale??`en`}),r(new o({db:c.db.readWrite,logger:c.logger.child({pkg:`notifications`}),defaultLocale:i.defaultLocale??`en`,recipientCap:i.recipientCap??1e3,preferencesResolver:u,channels:d})),await _({app:c}),c.logger.info({types:Array.from(new Set(a.map(e=>e.id))),channels:Object.keys(d)},`Notifications plugin initialized`)}},shared:{settings:[n],notifications:a}}}const m=/^[^\s@]+@[^\s@]+\.[^\s@]+$|<[^\s@]+@[^\s@]+\.[^\s@]+>/;async function h(e){let{app:t,channels:n,forced:r,from:i,defaultLocale:a}=e,o;try{o=await import(`@murumets-ee/mail/plugin`)}catch(e){let n=`notifications: @murumets-ee/mail not available — email channel disabled (in-app still works)`;if(r)throw Error(`${n} (cause: ${e.message})`);t.logger.warn({err:e},n);return}let s;try{s=o.getMailConfig()}catch(e){let n=`notifications: mail() plugin not in plugins array — email channel disabled (in-app still works)`;if(r)throw Error(`${n} (cause: ${e.message})`);t.logger.warn({err:e},n);return}if(!s.provider){let e=`notifications: mail provider not configured — email channel disabled (in-app still works)`;if(r)throw Error(e);t.logger.warn(e);return}let c=i||s.defaultFrom;if(!c){let e=`notifications: no from-address — set channels.email.from or mail().defaultFrom (email channel disabled)`;if(r)throw Error(e);t.logger.warn(e);return}if(!m.test(c)){let e=`notifications: from-address "${c}" is not a valid email — email channel disabled`;if(r)throw Error(e);t.logger.warn(e);return}let l;try{l=await import(`@murumets-ee/queue/client`)}catch(e){let n=`notifications: @murumets-ee/queue not available — email channel disabled (in-app still works)`;if(r)throw Error(`${n} (cause: ${e.message})`);t.logger.warn({err:e},n);return}if(!t.plugins.all().some(e=>e.name===`@murumets-ee/queue`)){let e=`notifications: queue() plugin not in plugins array — email channel disabled (in-app still works)`;if(r)throw Error(e);t.logger.warn(e);return}let{createEmailChannel:u,createSendEmailHandler:d,notificationsSendEmailJob:f}=await import(`./email-DgfO6gZR.mjs`).then(e=>e.r),p=t.logger.child({pkg:`notifications`,channel:`email`});n.email=u({queue:new l.QueueClient({db:t.db.readWrite,logger:p}),logger:p}),l.registerJob(f,d({db:t.db.readWrite,mail:s.provider,from:c,defaultLocale:a,logger:p})),t.logger.info({from:c},`Notifications email channel enabled`)}function g(e){return!e.startsWith(`notifications:`)}async function _(e){let{app:t}=e;if(!t.plugins.all().some(e=>e.name===`@murumets-ee/queue`))return;let n;try{n=await import(`@murumets-ee/queue/client`)}catch(e){t.logger.warn({err:e},`notifications: @murumets-ee/queue not available — queue terminal notifier disabled`);return}let r;try{r=await import(`@murumets-ee/queue/notifications`)}catch(e){t.logger.warn({err:e},`notifications: @murumets-ee/queue/notifications subpath not available — queue terminal notifier disabled`);return}let{QueueJobDead:i,buildQueueJobDeadPayload:o}=r,s=t.db.readWrite,c=t.logger.child({pkg:`notifications`,wiring:`queue-notifier`});n.setActiveQueueNotifier({onJobDead:async e=>{if(g(e.jobType))try{await a({type:i,recipients:{resolveUsers:()=>f(s)},payload:o({jobId:e.jobId,jobType:e.jobType,attempts:e.attempts,error:e.error,failedAt:e.failedAt})})}catch(t){c.warn({err:t,jobId:e.jobId,jobType:e.jobType},`queue.job.dead notify() failed`)}}}),t.logger.info(`Notifications queue terminal notifier wired (queue.job.dead → admins)`)}export{g as isQueueDeadEventNotifiable,p as notifications,i as resolveEnabledChannels};
2
+ //# sourceMappingURL=plugin.mjs.map