@rpcbase/server 0.594.0 → 0.596.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.
Files changed (61) hide show
  1. package/dist/convertHeifToWebp-BT1n72DQ.js +148 -0
  2. package/dist/convertHeifToWebp-BT1n72DQ.js.map +1 -0
  3. package/dist/email-Co1GNjlT.js +7204 -0
  4. package/dist/email-Co1GNjlT.js.map +1 -0
  5. package/dist/handler-BqlcQ9sE.js +245 -0
  6. package/dist/handler-BqlcQ9sE.js.map +1 -0
  7. package/dist/handler-C92_gJkX.js +743 -0
  8. package/dist/handler-C92_gJkX.js.map +1 -0
  9. package/dist/handler-CapJTGzc.js +795 -0
  10. package/dist/handler-CapJTGzc.js.map +1 -0
  11. package/dist/handler-Fk-5fOSm.js +119 -0
  12. package/dist/handler-Fk-5fOSm.js.map +1 -0
  13. package/dist/htmlTemplate.d.ts +16 -0
  14. package/dist/htmlTemplate.d.ts.map +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5140 -5286
  18. package/dist/index.js.map +1 -1
  19. package/dist/notifications.js +273 -382
  20. package/dist/notifications.js.map +1 -1
  21. package/dist/postProcessors-iffHvje4.js +89 -0
  22. package/dist/postProcessors-iffHvje4.js.map +1 -0
  23. package/dist/queryWindow-DP8zD0lb.js +555 -0
  24. package/dist/queryWindow-DP8zD0lb.js.map +1 -0
  25. package/dist/render_resend-B1SSQ4f7.js +7 -0
  26. package/dist/render_resend-B1SSQ4f7.js.map +1 -0
  27. package/dist/rts/index.js +990 -1207
  28. package/dist/rts/index.js.map +1 -1
  29. package/dist/schemas-B2fUvgYo.js +4151 -0
  30. package/dist/schemas-B2fUvgYo.js.map +1 -0
  31. package/dist/shared-DfrVDSp0.js +79 -0
  32. package/dist/shared-DfrVDSp0.js.map +1 -0
  33. package/dist/ssrMiddleware.d.ts +7 -3
  34. package/dist/ssrMiddleware.d.ts.map +1 -1
  35. package/dist/uploads/worker.js +139 -168
  36. package/dist/uploads/worker.js.map +1 -1
  37. package/dist/uploads.js +12 -22
  38. package/dist/uploads.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/convertHeifToWebp-C-DGXZ2k.js +0 -169
  41. package/dist/convertHeifToWebp-C-DGXZ2k.js.map +0 -1
  42. package/dist/email-BCf24GmK.js +0 -8071
  43. package/dist/email-BCf24GmK.js.map +0 -1
  44. package/dist/handler-BqoKvylN.js +0 -147
  45. package/dist/handler-BqoKvylN.js.map +0 -1
  46. package/dist/handler-CUOJ51-w.js +0 -903
  47. package/dist/handler-CUOJ51-w.js.map +0 -1
  48. package/dist/handler-D-XdgeG_.js +0 -986
  49. package/dist/handler-D-XdgeG_.js.map +0 -1
  50. package/dist/handler-F0gFTzvh.js +0 -275
  51. package/dist/handler-F0gFTzvh.js.map +0 -1
  52. package/dist/postProcessors-D27fGZP0.js +0 -107
  53. package/dist/postProcessors-D27fGZP0.js.map +0 -1
  54. package/dist/queryWindow-Cdr7K-S1.js +0 -714
  55. package/dist/queryWindow-Cdr7K-S1.js.map +0 -1
  56. package/dist/render_resend_false-MiC__Smr.js +0 -6
  57. package/dist/render_resend_false-MiC__Smr.js.map +0 -1
  58. package/dist/schemas-Drf83ni9.js +0 -4517
  59. package/dist/schemas-Drf83ni9.js.map +0 -1
  60. package/dist/shared-CJrm9Wjp.js +0 -104
  61. package/dist/shared-CJrm9Wjp.js.map +0 -1
@@ -0,0 +1,743 @@
1
+ import { createNotification, sendNotificationsDigestForUser } from "./notifications.js";
2
+ import { a as object, c as unknown, i as number, n as array, o as record, r as boolean, s as string, t as _enum } from "./schemas-B2fUvgYo.js";
3
+ import { models } from "@rpcbase/db";
4
+ import { buildAbilityFromSession, getAccessibleByQuery } from "@rpcbase/db/acl";
5
+ //#region src/notifications/api/notifications/shared.ts
6
+ var getSessionUser = (ctx) => {
7
+ const rawSessionUser = ctx.req.session?.user;
8
+ const userId = typeof rawSessionUser?.id === "string" ? rawSessionUser.id.trim() : "";
9
+ const tenantId = typeof rawSessionUser?.currentTenantId === "string" ? rawSessionUser.currentTenantId.trim() : "";
10
+ if (!userId) {
11
+ ctx.res.status(401);
12
+ return null;
13
+ }
14
+ if (!tenantId) {
15
+ ctx.res.status(400);
16
+ return null;
17
+ }
18
+ return {
19
+ userId,
20
+ tenantId
21
+ };
22
+ };
23
+ //#endregion
24
+ //#region src/notifications/api/notifications/index.ts
25
+ var ListRoute = "/api/rb/notifications";
26
+ var CreateRoute = "/api/rb/notifications/create";
27
+ var MarkReadRoute = "/api/rb/notifications/:notificationId/read";
28
+ var MarkReadByUrlRoute = "/api/rb/notifications/mark-read-by-url";
29
+ var ArchiveRoute = "/api/rb/notifications/:notificationId/archive";
30
+ var MarkAllReadRoute = "/api/rb/notifications/mark-all-read";
31
+ var SettingsRoute = "/api/rb/notifications/settings";
32
+ var DigestRunRoute = "/api/rb/notifications/digest/run";
33
+ var listRequestSchema = object({
34
+ includeArchived: boolean().optional(),
35
+ unreadOnly: boolean().optional(),
36
+ limit: number().int().min(1).max(200).optional(),
37
+ markSeen: boolean().optional()
38
+ });
39
+ var notificationDataSchema = record(string(), string());
40
+ var notificationPlatformPayloadSchema = object({
41
+ webpush: record(string(), unknown()).optional(),
42
+ fcmOptions: record(string(), unknown()).optional(),
43
+ android: record(string(), unknown()).optional(),
44
+ apns: record(string(), unknown()).optional()
45
+ }).passthrough();
46
+ var createRequestStringSchema = string().trim();
47
+ var notificationUrlModeSchema = _enum(["navigate", "merge_current_search"]);
48
+ var notificationContentFieldsSchema = object({
49
+ title: string().min(1),
50
+ body: string().optional(),
51
+ image: string().optional()
52
+ });
53
+ var createRequestSchema = object({
54
+ topic: createRequestStringSchema.min(1).optional(),
55
+ tag: createRequestStringSchema.min(1).optional(),
56
+ title: createRequestStringSchema.min(1),
57
+ body: createRequestStringSchema.optional(),
58
+ image: createRequestStringSchema.optional(),
59
+ url: createRequestStringSchema.min(1).optional(),
60
+ urlMode: notificationUrlModeSchema.optional(),
61
+ metadata: record(string(), unknown()).optional(),
62
+ data: notificationDataSchema.optional(),
63
+ platform: notificationPlatformPayloadSchema.optional()
64
+ });
65
+ var normalizeNotificationActionUrl = (url, origin) => {
66
+ const raw = url.trim();
67
+ if (!raw) return null;
68
+ try {
69
+ const base = new URL(origin);
70
+ const parsed = new URL(raw, base);
71
+ if (parsed.origin !== base.origin) return null;
72
+ return `${parsed.pathname}${parsed.search}`;
73
+ } catch {
74
+ return null;
75
+ }
76
+ };
77
+ var normalizeNotificationActionLink = (url, origin, urlMode = "navigate") => {
78
+ const raw = url.trim();
79
+ if (!raw) return null;
80
+ try {
81
+ const base = new URL(origin);
82
+ const parsed = new URL(raw, base);
83
+ if (parsed.origin !== base.origin) return null;
84
+ if (urlMode === "navigate") return `${parsed.pathname}${parsed.search}${parsed.hash}`;
85
+ return `${parsed.search}${parsed.hash}` || null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ };
90
+ var notificationSchema = object({
91
+ id: string(),
92
+ topic: string().optional(),
93
+ tag: string().optional(),
94
+ deliveryId: string().optional(),
95
+ title: notificationContentFieldsSchema.shape.title,
96
+ body: notificationContentFieldsSchema.shape.body,
97
+ image: notificationContentFieldsSchema.shape.image,
98
+ data: notificationDataSchema.optional(),
99
+ platform: notificationPlatformPayloadSchema.optional(),
100
+ createdAt: string(),
101
+ seenAt: string().optional(),
102
+ readAt: string().optional(),
103
+ archivedAt: string().optional()
104
+ });
105
+ var listResponseSchema = object({
106
+ ok: boolean(),
107
+ error: string().optional(),
108
+ notifications: array(notificationSchema).optional(),
109
+ unreadCount: number().int().min(0).optional(),
110
+ unseenCount: number().int().min(0).optional()
111
+ });
112
+ var createResponseSchema = object({
113
+ ok: boolean(),
114
+ error: string().optional(),
115
+ id: string().optional()
116
+ });
117
+ object({
118
+ ok: boolean(),
119
+ error: string().optional()
120
+ });
121
+ var markReadByUrlRequestSchema = object({ url: string().trim().min(1) });
122
+ var markReadByUrlResponseSchema = object({
123
+ ok: boolean(),
124
+ error: string().optional(),
125
+ matchedCount: number().int().min(0).optional(),
126
+ modifiedCount: number().int().min(0).optional()
127
+ });
128
+ object({
129
+ ok: boolean(),
130
+ error: string().optional()
131
+ });
132
+ object({
133
+ ok: boolean(),
134
+ error: string().optional()
135
+ });
136
+ var digestFrequencySchema = _enum([
137
+ "off",
138
+ "daily",
139
+ "weekly"
140
+ ]);
141
+ var topicPreferenceSchema = object({
142
+ topic: string(),
143
+ inApp: boolean(),
144
+ emailDigest: boolean(),
145
+ push: boolean()
146
+ });
147
+ var settingsSchema = object({
148
+ digestFrequency: digestFrequencySchema,
149
+ topicPreferences: array(topicPreferenceSchema),
150
+ lastDigestSentAt: string().optional()
151
+ });
152
+ var settingsResponseSchema = object({
153
+ ok: boolean(),
154
+ error: string().optional(),
155
+ settings: settingsSchema.optional()
156
+ });
157
+ var updateSettingsRequestSchema = object({
158
+ digestFrequency: digestFrequencySchema.optional(),
159
+ topicPreferences: array(topicPreferenceSchema).optional()
160
+ });
161
+ var updateSettingsResponseSchema = object({
162
+ ok: boolean(),
163
+ error: string().optional(),
164
+ settings: settingsSchema.optional()
165
+ });
166
+ var digestRunRequestSchema = object({ force: boolean().optional() });
167
+ object({
168
+ ok: boolean(),
169
+ error: string().optional(),
170
+ sent: boolean().optional(),
171
+ skippedReason: string().optional()
172
+ });
173
+ //#endregion
174
+ //#region src/notifications/api/notifications/handler.ts
175
+ var toIso = (value) => value instanceof Date ? value.toISOString() : void 0;
176
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
177
+ var toStringMap = (value) => {
178
+ if (!isRecord(value)) return void 0;
179
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
180
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
181
+ };
182
+ var getForwardedHeaderValue = (value) => {
183
+ const raw = Array.isArray(value) ? value[0] : value;
184
+ if (typeof raw !== "string") return void 0;
185
+ return raw.split(",")[0]?.trim() || void 0;
186
+ };
187
+ var getRequestOrigin = (ctx) => {
188
+ const protocol = getForwardedHeaderValue(ctx.req.headers["x-forwarded-proto"]) ?? ctx.req.protocol;
189
+ const host = getForwardedHeaderValue(ctx.req.headers["x-forwarded-host"]) ?? ctx.req.get("host");
190
+ return protocol && host ? `${protocol}://${host}` : "";
191
+ };
192
+ var getNotificationActionLinks = (notification) => {
193
+ const links = [];
194
+ if (typeof notification.data?.link === "string") links.push({
195
+ link: notification.data.link,
196
+ urlMode: notification.data.linkMode === "merge_current_search" ? "merge_current_search" : "navigate"
197
+ });
198
+ const platformWebpush = notification.platform?.webpush;
199
+ if (platformWebpush && typeof platformWebpush === "object" && !Array.isArray(platformWebpush)) {
200
+ const fcmOptions = platformWebpush.fcmOptions;
201
+ if (fcmOptions && typeof fcmOptions === "object" && !Array.isArray(fcmOptions)) {
202
+ const link = fcmOptions.link;
203
+ if (typeof link === "string") links.push({
204
+ link,
205
+ urlMode: "navigate"
206
+ });
207
+ }
208
+ }
209
+ return links;
210
+ };
211
+ var actionSearchParamsMatchTargetUrl = (actionUrl, targetUrl, origin) => {
212
+ try {
213
+ const base = new URL(origin);
214
+ const action = new URL(actionUrl, base);
215
+ const target = new URL(targetUrl, base);
216
+ for (const [key, value] of action.searchParams) if (target.searchParams.get(key) !== value) return false;
217
+ return Array.from(action.searchParams.keys()).length > 0;
218
+ } catch {
219
+ return false;
220
+ }
221
+ };
222
+ var notificationActionLinkMatchesUrl = (action, targetUrl, origin) => {
223
+ if (action.urlMode === "merge_current_search") return actionSearchParamsMatchTargetUrl(action.link, targetUrl, origin);
224
+ return normalizeNotificationActionUrl(action.link, origin) === targetUrl;
225
+ };
226
+ var buildDisabledTopics = (settings, key) => {
227
+ const raw = settings?.topicPreferences;
228
+ if (!Array.isArray(raw) || raw.length === 0) return [];
229
+ return raw.map((pref) => {
230
+ if (!pref || typeof pref !== "object") return null;
231
+ const topic = typeof pref.topic === "string" ? pref.topic.trim() : "";
232
+ if (!topic) return null;
233
+ return pref[key] === true ? null : topic;
234
+ }).filter((topic) => Boolean(topic));
235
+ };
236
+ var listNotifications = async (payload, ctx) => {
237
+ const session = getSessionUser(ctx);
238
+ if (!session) return {
239
+ ok: false,
240
+ error: "unauthorized"
241
+ };
242
+ const ability = buildAbilityFromSession({
243
+ tenantId: session.tenantId,
244
+ session: ctx.req.session
245
+ });
246
+ if (!ability.can("read", "RBNotification")) {
247
+ ctx.res.status(403);
248
+ return {
249
+ ok: false,
250
+ error: "forbidden"
251
+ };
252
+ }
253
+ const parsed = listRequestSchema.safeParse(payload);
254
+ if (!parsed.success) {
255
+ ctx.res.status(400);
256
+ return {
257
+ ok: false,
258
+ error: "invalid_payload"
259
+ };
260
+ }
261
+ const { userId } = session;
262
+ const includeArchived = parsed.data.includeArchived === true;
263
+ const unreadOnly = parsed.data.unreadOnly === true;
264
+ const limit = parsed.data.limit ?? 50;
265
+ const markSeen = parsed.data.markSeen === true;
266
+ const disabledTopics = buildDisabledTopics(await (await models.get("RBNotificationSettings", {
267
+ req: ctx.req,
268
+ ability
269
+ })).findOne({ userId }).lean(), "inApp");
270
+ const NotificationModel = await models.get("RBNotification", {
271
+ req: ctx.req,
272
+ ability
273
+ });
274
+ const queryFilters = [{ userId }, getAccessibleByQuery(ability, "read", "RBNotification")];
275
+ if (!includeArchived) queryFilters.push({ archivedAt: { $exists: false } });
276
+ if (unreadOnly) queryFilters.push({ readAt: { $exists: false } });
277
+ if (disabledTopics.length > 0) queryFilters.push({ topic: { $nin: disabledTopics } });
278
+ const query = { $and: queryFilters };
279
+ const notifications = await NotificationModel.find(query).sort({ createdAt: -1 }).limit(limit).lean();
280
+ const unseenQueryFilters = [
281
+ { userId },
282
+ { archivedAt: { $exists: false } },
283
+ { seenAt: { $exists: false } },
284
+ getAccessibleByQuery(ability, "read", "RBNotification")
285
+ ];
286
+ if (disabledTopics.length > 0) unseenQueryFilters.push({ topic: { $nin: disabledTopics } });
287
+ const unseenQuery = { $and: unseenQueryFilters };
288
+ const unreadQueryFilters = [
289
+ { userId },
290
+ { archivedAt: { $exists: false } },
291
+ { readAt: { $exists: false } },
292
+ getAccessibleByQuery(ability, "read", "RBNotification")
293
+ ];
294
+ if (disabledTopics.length > 0) unreadQueryFilters.push({ topic: { $nin: disabledTopics } });
295
+ const unreadQuery = { $and: unreadQueryFilters };
296
+ const [unreadCount, unseenCount] = await Promise.all([NotificationModel.countDocuments(unreadQuery), NotificationModel.countDocuments(unseenQuery)]);
297
+ const now = markSeen ? /* @__PURE__ */ new Date() : null;
298
+ if (now && unseenCount > 0) await NotificationModel.updateMany(unseenQuery, { $set: { seenAt: now } });
299
+ return listResponseSchema.parse({
300
+ ok: true,
301
+ notifications: notifications.map((n) => ({
302
+ id: String(n._id),
303
+ topic: typeof n.topic === "string" ? n.topic : void 0,
304
+ tag: typeof n.tag === "string" ? n.tag : void 0,
305
+ deliveryId: typeof n.deliveryId === "string" ? n.deliveryId : void 0,
306
+ title: typeof n.title === "string" ? n.title : "",
307
+ body: typeof n.body === "string" ? n.body : void 0,
308
+ image: typeof n.image === "string" ? n.image : void 0,
309
+ data: toStringMap(n.data),
310
+ platform: isRecord(n.platform) ? n.platform : void 0,
311
+ createdAt: toIso(n.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
312
+ seenAt: toIso(n.seenAt) ?? (now && !n.archivedAt && !n.seenAt ? now.toISOString() : void 0),
313
+ readAt: toIso(n.readAt),
314
+ archivedAt: toIso(n.archivedAt)
315
+ })),
316
+ unreadCount,
317
+ unseenCount: now ? 0 : unseenCount
318
+ });
319
+ };
320
+ var createNotificationForCurrentUser = async (payload, ctx) => {
321
+ const session = getSessionUser(ctx);
322
+ if (!session) return {
323
+ ok: false,
324
+ error: "unauthorized"
325
+ };
326
+ const ability = buildAbilityFromSession({
327
+ tenantId: session.tenantId,
328
+ session: ctx.req.session
329
+ });
330
+ if (!ability.can("create", "RBNotification")) {
331
+ ctx.res.status(403);
332
+ return {
333
+ ok: false,
334
+ error: "forbidden"
335
+ };
336
+ }
337
+ const parsed = createRequestSchema.safeParse(payload);
338
+ if (!parsed.success) {
339
+ ctx.res.status(400);
340
+ return {
341
+ ok: false,
342
+ error: "invalid_payload"
343
+ };
344
+ }
345
+ const origin = getRequestOrigin(ctx);
346
+ const actionLink = parsed.data.url ? normalizeNotificationActionLink(parsed.data.url, origin, parsed.data.urlMode) ?? void 0 : void 0;
347
+ if (parsed.data.url && !actionLink) {
348
+ ctx.res.status(400);
349
+ return {
350
+ ok: false,
351
+ error: "invalid_payload"
352
+ };
353
+ }
354
+ const created = await createNotification(ctx, {
355
+ userId: session.userId,
356
+ topic: parsed.data.topic,
357
+ tag: parsed.data.tag,
358
+ title: parsed.data.title,
359
+ body: parsed.data.body,
360
+ image: parsed.data.image,
361
+ url: actionLink,
362
+ urlMode: parsed.data.urlMode,
363
+ metadata: parsed.data.metadata,
364
+ data: parsed.data.data,
365
+ platform: parsed.data.platform
366
+ }, ability);
367
+ return createResponseSchema.parse({
368
+ ok: true,
369
+ id: created.id
370
+ });
371
+ };
372
+ var markRead = async (_payload, ctx) => {
373
+ const session = getSessionUser(ctx);
374
+ if (!session) return {
375
+ ok: false,
376
+ error: "unauthorized"
377
+ };
378
+ const ability = buildAbilityFromSession({
379
+ tenantId: session.tenantId,
380
+ session: ctx.req.session
381
+ });
382
+ if (!ability.can("update", "RBNotification")) {
383
+ ctx.res.status(403);
384
+ return {
385
+ ok: false,
386
+ error: "forbidden"
387
+ };
388
+ }
389
+ const notificationId = typeof ctx.req.params.notificationId === "string" ? ctx.req.params.notificationId.trim() : "";
390
+ if (!notificationId) {
391
+ ctx.res.status(400);
392
+ return {
393
+ ok: false,
394
+ error: "missing_notification_id"
395
+ };
396
+ }
397
+ const NotificationModel = await models.get("RBNotification", {
398
+ req: ctx.req,
399
+ ability
400
+ });
401
+ const now = /* @__PURE__ */ new Date();
402
+ try {
403
+ await NotificationModel.updateOne({ $and: [
404
+ { _id: notificationId },
405
+ { archivedAt: { $exists: false } },
406
+ getAccessibleByQuery(ability, "update", "RBNotification")
407
+ ] }, { $set: {
408
+ readAt: now,
409
+ seenAt: now
410
+ } });
411
+ } catch {
412
+ ctx.res.status(400);
413
+ return {
414
+ ok: false,
415
+ error: "invalid_notification_id"
416
+ };
417
+ }
418
+ return { ok: true };
419
+ };
420
+ var markReadByUrl = async (payload, ctx) => {
421
+ const session = getSessionUser(ctx);
422
+ if (!session) return {
423
+ ok: false,
424
+ error: "unauthorized"
425
+ };
426
+ const ability = buildAbilityFromSession({
427
+ tenantId: session.tenantId,
428
+ session: ctx.req.session
429
+ });
430
+ if (!ability.can("update", "RBNotification")) {
431
+ ctx.res.status(403);
432
+ return {
433
+ ok: false,
434
+ error: "forbidden"
435
+ };
436
+ }
437
+ const parsed = markReadByUrlRequestSchema.safeParse(payload);
438
+ if (!parsed.success) {
439
+ ctx.res.status(400);
440
+ return {
441
+ ok: false,
442
+ error: "invalid_payload"
443
+ };
444
+ }
445
+ const origin = getRequestOrigin(ctx);
446
+ const targetUrl = normalizeNotificationActionUrl(parsed.data.url, origin);
447
+ if (!targetUrl) {
448
+ ctx.res.status(400);
449
+ return {
450
+ ok: false,
451
+ error: "invalid_payload"
452
+ };
453
+ }
454
+ const NotificationModel = await models.get("RBNotification", {
455
+ req: ctx.req,
456
+ ability
457
+ });
458
+ const query = { $and: [
459
+ { userId: session.userId },
460
+ { archivedAt: { $exists: false } },
461
+ { readAt: { $exists: false } },
462
+ { $or: [{ "data.link": { $type: "string" } }, { "platform.webpush.fcmOptions.link": { $type: "string" } }] },
463
+ getAccessibleByQuery(ability, "update", "RBNotification")
464
+ ] };
465
+ const matchingIds = (await NotificationModel.find(query).select({
466
+ _id: 1,
467
+ data: 1,
468
+ platform: 1
469
+ }).lean()).filter((notification) => getNotificationActionLinks(notification).some((action) => notificationActionLinkMatchesUrl(action, targetUrl, origin))).map((notification) => notification._id);
470
+ if (matchingIds.length === 0) return markReadByUrlResponseSchema.parse({
471
+ ok: true,
472
+ matchedCount: 0,
473
+ modifiedCount: 0
474
+ });
475
+ const now = /* @__PURE__ */ new Date();
476
+ const result = await NotificationModel.updateMany({ $and: [
477
+ { _id: { $in: matchingIds } },
478
+ { userId: session.userId },
479
+ { archivedAt: { $exists: false } },
480
+ { readAt: { $exists: false } },
481
+ getAccessibleByQuery(ability, "update", "RBNotification")
482
+ ] }, { $set: {
483
+ readAt: now,
484
+ seenAt: now
485
+ } });
486
+ const modifiedCount = typeof result.modifiedCount === "number" ? result.modifiedCount : matchingIds.length;
487
+ return markReadByUrlResponseSchema.parse({
488
+ ok: true,
489
+ matchedCount: matchingIds.length,
490
+ modifiedCount
491
+ });
492
+ };
493
+ var markAllRead = async (_payload, ctx) => {
494
+ const session = getSessionUser(ctx);
495
+ if (!session) return {
496
+ ok: false,
497
+ error: "unauthorized"
498
+ };
499
+ const ability = buildAbilityFromSession({
500
+ tenantId: session.tenantId,
501
+ session: ctx.req.session
502
+ });
503
+ if (!ability.can("update", "RBNotification")) {
504
+ ctx.res.status(403);
505
+ return {
506
+ ok: false,
507
+ error: "forbidden"
508
+ };
509
+ }
510
+ const disabledTopics = buildDisabledTopics(await (await models.get("RBNotificationSettings", {
511
+ req: ctx.req,
512
+ ability
513
+ })).findOne({ userId: session.userId }).lean(), "inApp");
514
+ const NotificationModel = await models.get("RBNotification", {
515
+ req: ctx.req,
516
+ ability
517
+ });
518
+ const queryFilters = [
519
+ { userId: session.userId },
520
+ { archivedAt: { $exists: false } },
521
+ { readAt: { $exists: false } },
522
+ getAccessibleByQuery(ability, "update", "RBNotification")
523
+ ];
524
+ if (disabledTopics.length > 0) queryFilters.push({ topic: { $nin: disabledTopics } });
525
+ const query = { $and: queryFilters };
526
+ const now = /* @__PURE__ */ new Date();
527
+ await NotificationModel.updateMany(query, { $set: {
528
+ readAt: now,
529
+ seenAt: now
530
+ } });
531
+ return { ok: true };
532
+ };
533
+ var archiveNotification = async (_payload, ctx) => {
534
+ const session = getSessionUser(ctx);
535
+ if (!session) return {
536
+ ok: false,
537
+ error: "unauthorized"
538
+ };
539
+ const ability = buildAbilityFromSession({
540
+ tenantId: session.tenantId,
541
+ session: ctx.req.session
542
+ });
543
+ if (!ability.can("update", "RBNotification")) {
544
+ ctx.res.status(403);
545
+ return {
546
+ ok: false,
547
+ error: "forbidden"
548
+ };
549
+ }
550
+ const notificationId = typeof ctx.req.params.notificationId === "string" ? ctx.req.params.notificationId.trim() : "";
551
+ if (!notificationId) {
552
+ ctx.res.status(400);
553
+ return {
554
+ ok: false,
555
+ error: "missing_notification_id"
556
+ };
557
+ }
558
+ const NotificationModel = await models.get("RBNotification", {
559
+ req: ctx.req,
560
+ ability
561
+ });
562
+ try {
563
+ await NotificationModel.updateOne({ $and: [
564
+ { _id: notificationId },
565
+ { archivedAt: { $exists: false } },
566
+ getAccessibleByQuery(ability, "update", "RBNotification")
567
+ ] }, { $set: {
568
+ archivedAt: /* @__PURE__ */ new Date(),
569
+ drawerActive: false
570
+ } });
571
+ } catch {
572
+ ctx.res.status(400);
573
+ return {
574
+ ok: false,
575
+ error: "invalid_notification_id"
576
+ };
577
+ }
578
+ return { ok: true };
579
+ };
580
+ var getSettings = async (_payload, ctx) => {
581
+ const session = getSessionUser(ctx);
582
+ if (!session) return {
583
+ ok: false,
584
+ error: "unauthorized"
585
+ };
586
+ const ability = buildAbilityFromSession({
587
+ tenantId: session.tenantId,
588
+ session: ctx.req.session
589
+ });
590
+ if (!ability.can("read", "RBNotificationSettings")) {
591
+ ctx.res.status(403);
592
+ return {
593
+ ok: false,
594
+ error: "forbidden"
595
+ };
596
+ }
597
+ const settings = await (await models.get("RBNotificationSettings", {
598
+ req: ctx.req,
599
+ ability
600
+ })).findOne({ $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "read", "RBNotificationSettings")] }).lean();
601
+ const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
602
+ const digestFrequency = digestFrequencyRaw === "off" || digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" ? digestFrequencyRaw : "weekly";
603
+ const topicPreferences = Array.isArray(settings?.topicPreferences) ? settings.topicPreferences.map((pref) => ({
604
+ topic: typeof pref.topic === "string" ? pref.topic : "",
605
+ inApp: pref.inApp === true,
606
+ emailDigest: pref.emailDigest === true,
607
+ push: pref.push === true
608
+ })).filter((pref) => pref.topic.length > 0) : [];
609
+ return settingsResponseSchema.parse({
610
+ ok: true,
611
+ settings: {
612
+ digestFrequency,
613
+ topicPreferences,
614
+ lastDigestSentAt: toIso(settings?.lastDigestSentAt)
615
+ }
616
+ });
617
+ };
618
+ var updateSettings = async (payload, ctx) => {
619
+ const session = getSessionUser(ctx);
620
+ if (!session) return {
621
+ ok: false,
622
+ error: "unauthorized"
623
+ };
624
+ const ability = buildAbilityFromSession({
625
+ tenantId: session.tenantId,
626
+ session: ctx.req.session
627
+ });
628
+ if (!ability.can("update", "RBNotificationSettings")) {
629
+ ctx.res.status(403);
630
+ return {
631
+ ok: false,
632
+ error: "forbidden"
633
+ };
634
+ }
635
+ const parsed = updateSettingsRequestSchema.safeParse(payload);
636
+ if (!parsed.success) {
637
+ ctx.res.status(400);
638
+ return {
639
+ ok: false,
640
+ error: "invalid_payload"
641
+ };
642
+ }
643
+ const SettingsModel = await models.get("RBNotificationSettings", {
644
+ req: ctx.req,
645
+ ability
646
+ });
647
+ const nextValues = {};
648
+ if (parsed.data.digestFrequency) nextValues.digestFrequency = parsed.data.digestFrequency;
649
+ if (parsed.data.topicPreferences) {
650
+ const seen = /* @__PURE__ */ new Set();
651
+ nextValues.topicPreferences = parsed.data.topicPreferences.map((pref) => ({
652
+ topic: pref.topic.trim(),
653
+ inApp: pref.inApp,
654
+ emailDigest: pref.emailDigest,
655
+ push: pref.push
656
+ })).filter((pref) => pref.topic.length > 0).filter((pref) => {
657
+ if (seen.has(pref.topic)) return false;
658
+ seen.add(pref.topic);
659
+ return true;
660
+ });
661
+ }
662
+ const ops = { $setOnInsert: { userId: session.userId } };
663
+ if (Object.keys(nextValues).length > 0) ops.$set = nextValues;
664
+ const settings = await SettingsModel.findOneAndUpdate({ $and: [{ userId: session.userId }, getAccessibleByQuery(ability, "update", "RBNotificationSettings")] }, ops, {
665
+ upsert: true,
666
+ returnDocument: "after",
667
+ setDefaultsOnInsert: true
668
+ }).lean();
669
+ const digestFrequencyRaw = typeof settings?.digestFrequency === "string" ? settings.digestFrequency : "weekly";
670
+ const digestFrequency = digestFrequencyRaw === "off" || digestFrequencyRaw === "daily" || digestFrequencyRaw === "weekly" ? digestFrequencyRaw : "weekly";
671
+ const topicPreferences = Array.isArray(settings?.topicPreferences) ? settings.topicPreferences.map((pref) => ({
672
+ topic: typeof pref.topic === "string" ? pref.topic : "",
673
+ inApp: pref.inApp === true,
674
+ emailDigest: pref.emailDigest === true,
675
+ push: pref.push === true
676
+ })).filter((pref) => pref.topic.length > 0) : [];
677
+ return updateSettingsResponseSchema.parse({
678
+ ok: true,
679
+ settings: {
680
+ digestFrequency,
681
+ topicPreferences,
682
+ lastDigestSentAt: toIso(settings?.lastDigestSentAt)
683
+ }
684
+ });
685
+ };
686
+ var runDigest = async (payload, ctx) => {
687
+ const session = getSessionUser(ctx);
688
+ if (!session) return {
689
+ ok: false,
690
+ error: "unauthorized"
691
+ };
692
+ const ability = buildAbilityFromSession({
693
+ tenantId: session.tenantId,
694
+ session: ctx.req.session
695
+ });
696
+ if (!ability.can("read", "RBNotification")) {
697
+ ctx.res.status(403);
698
+ return {
699
+ ok: false,
700
+ error: "forbidden"
701
+ };
702
+ }
703
+ const parsed = digestRunRequestSchema.safeParse(payload);
704
+ if (!parsed.success) {
705
+ ctx.res.status(400);
706
+ return {
707
+ ok: false,
708
+ error: "invalid_payload"
709
+ };
710
+ }
711
+ const result = await sendNotificationsDigestForUser(ctx, {
712
+ userId: session.userId,
713
+ ability,
714
+ force: parsed.data.force === true
715
+ });
716
+ if (!result.ok) {
717
+ ctx.res.status(500);
718
+ return {
719
+ ok: false,
720
+ error: result.error
721
+ };
722
+ }
723
+ return {
724
+ ok: true,
725
+ sent: result.sent,
726
+ ...result.skippedReason ? { skippedReason: result.skippedReason } : {}
727
+ };
728
+ };
729
+ var handler_default = (api) => {
730
+ api.post(ListRoute, listNotifications);
731
+ api.post(CreateRoute, createNotificationForCurrentUser);
732
+ api.post(MarkReadRoute, markRead);
733
+ api.post(MarkReadByUrlRoute, markReadByUrl);
734
+ api.post(MarkAllReadRoute, markAllRead);
735
+ api.post(ArchiveRoute, archiveNotification);
736
+ api.get(SettingsRoute, getSettings);
737
+ api.put(SettingsRoute, updateSettings);
738
+ api.post(DigestRunRoute, runDigest);
739
+ };
740
+ //#endregion
741
+ export { handler_default as default };
742
+
743
+ //# sourceMappingURL=handler-C92_gJkX.js.map