@voltro/plugin-notifications 0.86.0 → 0.87.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/dist/index.d.ts CHANGED
@@ -65,6 +65,7 @@ export declare const buildNotificationService: (deps: {
65
65
  * a specific `channels:` list coalesce into one digest per subject per window.
66
66
  * 0 (default) disables digesting — every send goes out immediately. */
67
67
  readonly digestWindowMs?: number;
68
+ readonly deliveryGuards?: Readonly<Record<string, NotificationDeliveryGuard>>;
68
69
  }) => NotificationServiceShape;
69
70
 
70
71
  /** A delivery channel. `id` is the channel key prefs target (e.g. 'email',
@@ -410,6 +411,20 @@ export declare const minuteOfDayInZone: (at: Date, tz: string) => number;
410
411
 
411
412
  export declare const NOTIFICATION_SEND_EFFECT = "voltro.notification.send";
412
413
 
414
+ export declare type NotificationDeliveryGuard = (input: NotificationDeliveryGuardInput) => boolean | Promise<boolean>;
415
+
416
+ export declare interface NotificationDeliveryGuardInput {
417
+ readonly recipientId: string;
418
+ readonly tenantId: string | null;
419
+ readonly payload: Readonly<Record<string, unknown>>;
420
+ }
421
+
422
+ /** Persisted authorization reference; never forwarded to delivery channels. */
423
+ export declare interface NotificationDeliveryGuardRef {
424
+ readonly id: string;
425
+ readonly payload: Readonly<Record<string, unknown>>;
426
+ }
427
+
413
428
  export declare interface NotificationDescriptor<Audience = unknown, Payload = unknown> {
414
429
  readonly _tag: 'VoltroNotification';
415
430
  readonly name: string;
@@ -533,6 +548,8 @@ export declare interface NotificationsPluginOptions {
533
548
  * boundary by the scheduled flush). A send that forces its own `channels:`
534
549
  * bypasses the digest. 0 (default) = every send delivers immediately. */
535
550
  readonly digestWindowMs?: number;
551
+ /** Re-evaluated before actual delivery, including persisted quiet/digest holds. Unknown guards refuse. */
552
+ readonly deliveryGuards?: Readonly<Record<string, NotificationDeliveryGuard>>;
536
553
  /** How often the scheduled flush drains due digest windows + quiet-hours
537
554
  * deferrals. Default 30s. Only runs once the store is bound at boot. */
538
555
  readonly flushIntervalMs?: number;
@@ -839,6 +856,7 @@ export declare const richPushOf: (data: Readonly<Record<string, unknown>> | unde
839
856
 
840
857
  /** What a caller asks to send. `channels` overrides the default routing. */
841
858
  export declare interface SendInput {
859
+ readonly deliveryGuard?: NotificationDeliveryGuardRef;
842
860
  readonly to: string;
843
861
  readonly category: string;
844
862
  readonly title: string;
package/dist/index.js CHANGED
@@ -2,76 +2,76 @@ import { PushTokenRejected as e } from "./errors.js";
2
2
  import { consoleChannel as t, customChannel as n, emailChannel as r, pushChannel as i, resolveChannels as a, richPushOf as o, smsChannel as s, webhookChannel as c } from "./channels.js";
3
3
  import { dataStoreNotificationStore as l, memoryNotificationStore as u } from "./store.js";
4
4
  import { buildNotificationService as d, inAppChannel as f, inQuietHours as p, minuteOfDayInZone as m, quietHoursEnd as h } from "./service.js";
5
- import { BASE_NAME as g, archiveDescriptor as _, clearQuietHoursDescriptor as ee, inboxDescriptor as te, inboxFeed as ne, markAllReadDescriptor as re, markReadDescriptor as ie, markUnreadDescriptor as ae, notificationsRpcClientImports as v, preferencesDescriptor as oe, setPreferenceDescriptor as se, setQuietHoursDescriptor as ce, subscribeDescriptor as y, unarchiveDescriptor as le, unreadCountDescriptor as ue, unsubscribeDescriptor as de, webPushPublicKeyDescriptor as fe, webPushRpcClientImports as pe, webPushStatusDescriptor as me, webPushSubscribeDescriptor as he, webPushUnsubscribeDescriptor as ge } from "./rpc.js";
6
- import { clickTokenMatches as _e, decryptWebPushPayload as b, encryptWebPushPayload as x, generateVapidPrivateKey as S, sendWebPush as C, vapidAuthorization as w, vapidPublicKeyFor as T } from "./webPushProtocol.js";
7
- import { assertWebPushConfigured as ve, bindWebPushStore as ye, dataStorePushSubscriptionStore as E, endpointHashOf as D, memoryPushSubscriptionStore as O, pushSubscriptionsTable as k, resetWebPushStoreForTest as be, webPushCell as A, webPushChannel as j, webPushPublicKey as M } from "./webPush.js";
8
- import { defineNotification as N } from "./define.js";
9
- import { sendNotification as xe } from "./sendNotification.js";
10
- import { NOTIFICATION_SEND_EFFECT as Se, enqueueNotification as Ce, notificationOutboxHandler as we } from "./durable.js";
11
- import { APNS_HOSTS as Te, APNS_TOKEN_REJECTIONS as Ee, apnsBody as De, apnsProviderToken as Oe, apnsTransport as ke } from "./apnsPush.js";
12
- import { FCM_SCOPE as Ae, FCM_TOKEN_REJECTIONS as je, GOOGLE_TOKEN_URI as P, fcmAssertion as Me, fcmMessage as Ne, fcmSendEndpoint as Pe, fcmTransport as Fe, parseServiceAccount as Ie } from "./fcmPush.js";
13
- import { EXPO_PUSH_ENDPOINT as Le, expoPushTransport as Re, isExpoPushToken as ze } from "./expoPush.js";
14
- import { Context as Be, Effect as F, Layer as Ve } from "effect";
15
- import { boolean as He, id as I, integer as L, isNotNull as Ue, json as R, registerRetention as z, retentionTtlMsFromEnv as B, table as V, text as H, timestamp as U } from "@voltro/database";
16
- import { definePlugin as We, pluginInstanceName as Ge, publishReactivity as Ke } from "@voltro/protocol";
5
+ import { BASE_NAME as g, archiveDescriptor as ee, clearQuietHoursDescriptor as te, inboxDescriptor as ne, inboxFeed as re, markAllReadDescriptor as ie, markReadDescriptor as ae, markUnreadDescriptor as oe, notificationsRpcClientImports as _, preferencesDescriptor as se, preferencesFeed as ce, setPreferenceDescriptor as le, setQuietHoursDescriptor as v, subscribeDescriptor as ue, unarchiveDescriptor as de, unreadCountDescriptor as fe, unsubscribeDescriptor as pe, webPushPublicKeyDescriptor as me, webPushRpcClientImports as he, webPushStatusDescriptor as ge, webPushSubscribeDescriptor as _e, webPushUnsubscribeDescriptor as ve } from "./rpc.js";
6
+ import { clickTokenMatches as ye, decryptWebPushPayload as y, encryptWebPushPayload as b, generateVapidPrivateKey as x, sendWebPush as S, vapidAuthorization as C, vapidPublicKeyFor as w } from "./webPushProtocol.js";
7
+ import { assertWebPushConfigured as be, bindWebPushStore as xe, dataStorePushSubscriptionStore as T, endpointHashOf as E, memoryPushSubscriptionStore as D, pushSubscriptionsTable as O, resetWebPushStoreForTest as k, webPushCell as A, webPushChannel as j, webPushPublicKey as M } from "./webPush.js";
8
+ import { defineNotification as Se } from "./define.js";
9
+ import { sendNotification as Ce } from "./sendNotification.js";
10
+ import { NOTIFICATION_SEND_EFFECT as we, enqueueNotification as Te, notificationOutboxHandler as Ee } from "./durable.js";
11
+ import { APNS_HOSTS as De, APNS_TOKEN_REJECTIONS as Oe, apnsBody as ke, apnsProviderToken as Ae, apnsTransport as je } from "./apnsPush.js";
12
+ import { FCM_SCOPE as Me, FCM_TOKEN_REJECTIONS as N, GOOGLE_TOKEN_URI as Ne, fcmAssertion as Pe, fcmMessage as Fe, fcmSendEndpoint as Ie, fcmTransport as Le, parseServiceAccount as Re } from "./fcmPush.js";
13
+ import { EXPO_PUSH_ENDPOINT as ze, expoPushTransport as Be, isExpoPushToken as Ve } from "./expoPush.js";
14
+ import { Context as He, Effect as P, Layer as Ue } from "effect";
15
+ import { boolean as We, id as F, integer as I, isNotNull as Ge, json as L, registerRetention as R, retentionTtlMsFromEnv as z, table as B, text as V, timestamp as H } from "@voltro/database";
16
+ import { definePlugin as Ke, pluginInstanceName as qe, publishReactivity as U } from "@voltro/protocol";
17
17
  //#region src/index.ts
18
- var W = V("_voltro_notification_inbox", {
19
- id: I({ prefix: "notif" }),
20
- subjectId: H(),
21
- category: H(),
22
- title: H(),
23
- body: H(),
24
- data: R(),
25
- readAt: U().nullable(),
26
- archivedAt: U().nullable(),
27
- createdAt: U().default("now"),
28
- tenantId: H().nullable()
29
- }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), G = V("_voltro_notification_preferences", {
30
- id: I({ prefix: "notifpref" }),
31
- subjectId: H(),
32
- category: H(),
33
- channel: H(),
34
- enabled: He().default(!0)
18
+ var W = B("_voltro_notification_inbox", {
19
+ id: F({ prefix: "notif" }),
20
+ subjectId: V(),
21
+ category: V(),
22
+ title: V(),
23
+ body: V(),
24
+ data: L(),
25
+ readAt: H().nullable(),
26
+ archivedAt: H().nullable(),
27
+ createdAt: H().default("now"),
28
+ tenantId: V().nullable()
29
+ }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), G = B("_voltro_notification_preferences", {
30
+ id: F({ prefix: "notifpref" }),
31
+ subjectId: V(),
32
+ category: V(),
33
+ channel: V(),
34
+ enabled: We().default(!0)
35
35
  }).renamedFrom("notification_preferences").unique("byPreferencesSubjectCategoryChannel", [
36
36
  "subjectId",
37
37
  "category",
38
38
  "channel"
39
- ]), K = V("_voltro_notification_deliveries", {
40
- id: I({ prefix: "notifdlv" }),
41
- recipient: H(),
42
- category: H(),
43
- channel: H(),
44
- status: H(),
45
- error: H().nullable(),
46
- at: U().default("now"),
47
- endpoint: H().nullable(),
48
- clickToken: H().nullable(),
49
- clickedAt: U().nullable()
50
- }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), q = V("_voltro_notification_topic_subscriptions", {
51
- id: I({ prefix: "notiftsub" }),
52
- topic: H(),
53
- subjectId: H(),
54
- tenantId: H().nullable()
55
- }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), J = V("_voltro_notification_quiet_hours", {
56
- id: I({ prefix: "notifqh" }),
57
- subjectId: H(),
58
- startMinute: L(),
59
- endMinute: L(),
60
- tz: H().default("UTC"),
61
- policy: H().default("hold")
62
- }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Y = V("_voltro_notification_held", {
63
- id: I({ prefix: "notifheld" }),
64
- kind: H(),
65
- subjectId: H(),
66
- send: R(),
67
- flushAt: U()
68
- }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), X = class extends Be.Service()("@voltro/plugin-notifications/NotificationService") {}, Z = (e) => async (t) => {
39
+ ]), K = B("_voltro_notification_deliveries", {
40
+ id: F({ prefix: "notifdlv" }),
41
+ recipient: V(),
42
+ category: V(),
43
+ channel: V(),
44
+ status: V(),
45
+ error: V().nullable(),
46
+ at: H().default("now"),
47
+ endpoint: V().nullable(),
48
+ clickToken: V().nullable(),
49
+ clickedAt: H().nullable()
50
+ }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), q = B("_voltro_notification_topic_subscriptions", {
51
+ id: F({ prefix: "notiftsub" }),
52
+ topic: V(),
53
+ subjectId: V(),
54
+ tenantId: V().nullable()
55
+ }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), J = B("_voltro_notification_quiet_hours", {
56
+ id: F({ prefix: "notifqh" }),
57
+ subjectId: V(),
58
+ startMinute: I(),
59
+ endMinute: I(),
60
+ tz: V().default("UTC"),
61
+ policy: V().default("hold")
62
+ }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Y = B("_voltro_notification_held", {
63
+ id: F({ prefix: "notifheld" }),
64
+ kind: V(),
65
+ subjectId: V(),
66
+ send: L(),
67
+ flushAt: H()
68
+ }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), X = class extends He.Service()("@voltro/plugin-notifications/NotificationService") {}, Z = (e) => async (t) => {
69
69
  if (e !== void 0) try {
70
70
  let n = await e(t);
71
71
  if (typeof n == "string" && n !== "") return n;
72
72
  } catch {}
73
73
  return t.request.subject.id ?? "anonymous";
74
- }, Q = (e) => e.request.subject.tenantId ?? null, $ = g, qe = { collects: [
74
+ }, Q = (e) => e.request.subject.tenantId ?? null, $ = g, Je = { collects: [
75
75
  {
76
76
  category: "device-id",
77
77
  linked: !0,
@@ -90,33 +90,33 @@ var W = V("_voltro_notification_inbox", {
90
90
  tracking: !1,
91
91
  purposes: ["app-functionality"]
92
92
  }
93
- ] }, Je = (e = {}) => {
94
- let n = Z(e.resolveSubjectId), r = Ge({
93
+ ] }, Ye = (e = {}) => {
94
+ let n = Z(e.resolveSubjectId), r = qe({
95
95
  base: $,
96
96
  alias: e.alias,
97
97
  instance: e.name
98
98
  });
99
- z({
99
+ R({
100
100
  source: "plugin",
101
101
  table: "_voltro_notification_deliveries",
102
102
  timeColumn: "at",
103
- ttlMs: B(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
103
+ ttlMs: z(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
104
104
  envVar: "VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS"
105
- }), z({
105
+ }), R({
106
106
  source: "plugin",
107
107
  table: "_voltro_notification_inbox",
108
108
  timeColumn: "createdAt",
109
- ttlMs: B(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
109
+ ttlMs: z(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
110
110
  envVar: "VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS",
111
- where: Ue("readAt")
112
- }), z({
111
+ where: Ge("readAt")
112
+ }), R({
113
113
  source: "plugin",
114
114
  table: "_voltro_notification_held",
115
115
  timeColumn: "flushAt",
116
- ttlMs: B(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
116
+ ttlMs: z(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
117
117
  envVar: "VOLTRO_NOTIFICATION_HELD_TTL_HOURS"
118
118
  });
119
- let i = e.store === void 0, a = { current: e.store ?? u() }, o, s = (e) => e.then((e) => (Ke(o, ne), e)), c = {
119
+ let i = e.store === void 0, a = { current: e.store ?? u() }, o, s = (e) => e.then((e) => (U(o, re), e)), c = (e) => e.then((e) => (U(o, ce), e)), p = {
120
120
  addInbox: (e) => s(a.current.addInbox(e)),
121
121
  listInbox: (e, t) => a.current.listInbox(e, t),
122
122
  markRead: (e, t) => s(a.current.markRead(e, t)),
@@ -125,7 +125,7 @@ var W = V("_voltro_notification_inbox", {
125
125
  setArchived: (e, t, n) => s(a.current.setArchived(e, t, n)),
126
126
  unreadCount: (e) => a.current.unreadCount(e),
127
127
  getPreferences: (e) => a.current.getPreferences(e),
128
- setPreference: (e) => a.current.setPreference(e),
128
+ setPreference: (e) => c(a.current.setPreference(e)),
129
129
  recordDelivery: (e) => a.current.recordDelivery(e),
130
130
  listDeliveries: (e) => a.current.listDeliveries(e),
131
131
  markClicked: (e) => a.current.markClicked(e),
@@ -139,55 +139,56 @@ var W = V("_voltro_notification_inbox", {
139
139
  pendingDigest: (e) => a.current.pendingDigest(e),
140
140
  dueHeld: (e) => a.current.dueHeld(e),
141
141
  clearHeld: (e) => a.current.clearHeld(e)
142
- }, p = e.channels ?? [t()], m = p.some((e) => e.id === "inApp") ? p : [...p, f(c)], h = e.digestWindowMs ?? 0, g = m.some((e) => e.id === "webPush"), b = d({
143
- channels: m,
144
- store: c,
145
- digestWindowMs: h
142
+ }, m = e.channels ?? [t()], h = m.some((e) => e.id === "inApp") ? m : [...m, f(p)], g = e.digestWindowMs ?? 0, y = h.some((e) => e.id === "webPush"), b = d({
143
+ channels: h,
144
+ store: p,
145
+ digestWindowMs: g,
146
+ ...e.deliveryGuards === void 0 ? {} : { deliveryGuards: e.deliveryGuards }
146
147
  }), x, S = [
147
148
  {
148
- ...te,
149
+ ...ne,
149
150
  description: "The caller's in-app inbox items.",
150
- execute: (e, t) => F.promise(async () => b.inbox(await n(t), e))
151
+ execute: (e, t) => P.promise(async () => b.inbox(await n(t), e))
151
152
  },
152
153
  {
153
- ...ue,
154
+ ...fe,
154
155
  description: "The caller's unread count.",
155
- execute: (e, t) => F.promise(async () => ({ count: await b.unreadCount(await n(t)) }))
156
+ execute: (e, t) => P.promise(async () => ({ count: await b.unreadCount(await n(t)) }))
156
157
  },
157
158
  {
158
- ...ie,
159
+ ...ae,
159
160
  description: "Mark an inbox item read.",
160
- execute: (e, t) => F.promise(async () => ({ ok: await b.markRead(e.id, await n(t)) }))
161
+ execute: (e, t) => P.promise(async () => ({ ok: await b.markRead(e.id, await n(t)) }))
161
162
  },
162
163
  {
163
- ...ae,
164
+ ...oe,
164
165
  description: "Mark an inbox item unread again.",
165
- execute: (e, t) => F.promise(async () => ({ ok: await b.markUnread(e.id, await n(t)) }))
166
+ execute: (e, t) => P.promise(async () => ({ ok: await b.markUnread(e.id, await n(t)) }))
166
167
  },
167
168
  {
168
- ...re,
169
+ ...ie,
169
170
  description: "Mark every unread item in the caller's inbox read.",
170
- execute: (e, t) => F.promise(async () => ({ count: await b.markAllRead(await n(t)) }))
171
+ execute: (e, t) => P.promise(async () => ({ count: await b.markAllRead(await n(t)) }))
171
172
  },
172
173
  {
173
- ..._,
174
+ ...ee,
174
175
  description: "Archive an inbox item — a different state from read; this is what empties the inbox.",
175
- execute: (e, t) => F.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !0) }))
176
+ execute: (e, t) => P.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !0) }))
176
177
  },
177
178
  {
178
- ...le,
179
+ ...de,
179
180
  description: "Return an archived item to the inbox.",
180
- execute: (e, t) => F.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !1) }))
181
+ execute: (e, t) => P.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !1) }))
181
182
  },
182
183
  {
183
- ...oe,
184
+ ...se,
184
185
  description: "The caller's channel preferences.",
185
- execute: (e, t) => F.promise(async () => b.getPreferences(await n(t)))
186
+ execute: (e, t) => P.promise(async () => b.getPreferences(await n(t)))
186
187
  },
187
188
  {
188
- ...se,
189
+ ...le,
189
190
  description: "Toggle a channel on/off for a category.",
190
- execute: (e, t) => F.promise(async () => {
191
+ execute: (e, t) => P.promise(async () => {
191
192
  let { category: r, channel: i, enabled: a } = e;
192
193
  return await b.setPreference({
193
194
  subjectId: await n(t),
@@ -198,19 +199,19 @@ var W = V("_voltro_notification_inbox", {
198
199
  })
199
200
  },
200
201
  {
201
- ...y,
202
+ ...ue,
202
203
  description: "Subscribe the caller to a broadcast topic.",
203
- execute: (e, t) => F.promise(async () => (await b.subscribe(e.topic, await n(t), Q(t)), { ok: !0 }))
204
+ execute: (e, t) => P.promise(async () => (await b.subscribe(e.topic, await n(t), Q(t)), { ok: !0 }))
204
205
  },
205
206
  {
206
- ...de,
207
+ ...pe,
207
208
  description: "Unsubscribe the caller from a broadcast topic.",
208
- execute: (e, t) => F.promise(async () => (await b.unsubscribe(e.topic, await n(t)), { ok: !0 }))
209
+ execute: (e, t) => P.promise(async () => (await b.unsubscribe(e.topic, await n(t)), { ok: !0 }))
209
210
  },
210
211
  {
211
- ...ce,
212
+ ...v,
212
213
  description: "Set the caller's quiet-hours (DND) window.",
213
- execute: (e, t) => F.promise(async () => {
214
+ execute: (e, t) => P.promise(async () => {
214
215
  let r = e;
215
216
  return await b.setQuietHours({
216
217
  subjectId: await n(t),
@@ -222,25 +223,25 @@ var W = V("_voltro_notification_inbox", {
222
223
  })
223
224
  },
224
225
  {
225
- ...ee,
226
+ ...te,
226
227
  description: "Clear the caller's quiet-hours window.",
227
- execute: (e, t) => F.promise(async () => (await b.clearQuietHours(await n(t)), { ok: !0 }))
228
+ execute: (e, t) => P.promise(async () => (await b.clearQuietHours(await n(t)), { ok: !0 }))
228
229
  }
229
- ], C = g ? [
230
+ ], C = y ? [
230
231
  {
231
- ...fe,
232
+ ...me,
232
233
  description: "The VAPID application-server key a subscribing browser needs.",
233
- execute: () => F.sync(() => ({ key: M(A().channelOptions?.privateKey) }))
234
+ execute: () => P.sync(() => ({ key: M(A().channelOptions?.privateKey) }))
234
235
  },
235
236
  {
236
- ...he,
237
+ ..._e,
237
238
  description: "Register the calling subject's browser push subscription (one row per endpoint).",
238
- execute: (e, t) => F.promise(async () => {
239
+ execute: (e, t) => P.promise(async () => {
239
240
  let r = e;
240
241
  return await A().store.upsert({
241
242
  subjectId: await n(t),
242
243
  endpoint: r.endpoint,
243
- endpointHash: D(r.endpoint),
244
+ endpointHash: E(r.endpoint),
244
245
  p256dh: r.p256dh,
245
246
  auth: r.auth,
246
247
  ua: r.ua ?? null,
@@ -249,14 +250,14 @@ var W = V("_voltro_notification_inbox", {
249
250
  })
250
251
  },
251
252
  {
252
- ...ge,
253
+ ...ve,
253
254
  description: "Remove one of the calling subject's own subscriptions.",
254
- execute: (e, t) => F.promise(async () => ({ ok: await A().store.removeForSubject(D(e.endpoint), await n(t)) }))
255
+ execute: (e, t) => P.promise(async () => ({ ok: await A().store.removeForSubject(E(e.endpoint), await n(t)) }))
255
256
  },
256
257
  {
257
- ...me,
258
+ ...ge,
258
259
  description: "How many push endpoints the calling subject has registered.",
259
- execute: (e, t) => F.promise(async () => ({ endpoints: await A().store.countForSubject(await n(t)) }))
260
+ execute: (e, t) => P.promise(async () => ({ endpoints: await A().store.countForSubject(await n(t)) }))
260
261
  }
261
262
  ] : [], w = (e) => ({
262
263
  kind: "json",
@@ -265,18 +266,18 @@ var W = V("_voltro_notification_inbox", {
265
266
  kind: "json",
266
267
  status: e,
267
268
  data: { error: t }
268
- }), E = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), O = [{
269
+ }), D = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), k = [{
269
270
  method: "GET",
270
271
  path: "/deliveries",
271
272
  scope: "process",
272
273
  description: "Recent delivery records (newest first) + per-channel + per-status counts.",
273
- handler: () => F.promise(async () => {
274
- let e = await c.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
274
+ handler: () => P.promise(async () => {
275
+ let e = await p.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
275
276
  for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : i.status === "failed" && r++;
276
277
  let i = e.slice(0, 100).map(({ clickToken: e, ...t }) => t);
277
278
  return w({
278
279
  deliveries: i,
279
- channels: m.map((e) => e.id),
280
+ channels: h.map((e) => e.id),
280
281
  stats: {
281
282
  sent: n,
282
283
  failed: r,
@@ -290,15 +291,15 @@ var W = V("_voltro_notification_inbox", {
290
291
  scope: "process",
291
292
  description: "In-app inbox for a subject (?subjectId=…).",
292
293
  handler: (e) => {
293
- let t = E(e.url, "subjectId");
294
- return t ? F.promise(async () => w({
295
- items: await c.listInbox(t, { limit: 100 }),
296
- unread: await c.unreadCount(t)
297
- })) : F.succeed(T(400, "subjectId query param required"));
294
+ let t = D(e.url, "subjectId");
295
+ return t ? P.promise(async () => w({
296
+ items: await p.listInbox(t, { limit: 100 }),
297
+ unread: await p.unreadCount(t)
298
+ })) : P.succeed(T(400, "subjectId query param required"));
298
299
  }
299
300
  }];
300
- return We({
301
- privacy: qe,
301
+ return Ke({
302
+ privacy: Je,
302
303
  name: r,
303
304
  baseName: $,
304
305
  description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
@@ -310,9 +311,9 @@ var W = V("_voltro_notification_inbox", {
310
311
  q,
311
312
  J,
312
313
  Y,
313
- k
314
+ O
314
315
  ] } },
315
- ...g ? {
316
+ ...y ? {
316
317
  declaredEnv: [{
317
318
  name: "VOLTRO_VAPID_PRIVATE_KEY",
318
319
  required: !0,
@@ -328,12 +329,12 @@ var W = V("_voltro_notification_inbox", {
328
329
  try {
329
330
  t = JSON.parse(Buffer.from(e.rawBody).toString("utf8")).token;
330
331
  } catch {}
331
- if (typeof t != "string" || t === "" || !_e(t, t)) return {
332
+ if (typeof t != "string" || t === "" || !ye(t, t)) return {
332
333
  status: 400,
333
334
  body: JSON.stringify({ error: "token required" }),
334
335
  contentType: "application/json"
335
336
  };
336
- let n = await c.markClicked(t);
337
+ let n = await p.markClicked(t);
337
338
  return {
338
339
  status: n ? 200 : 404,
339
340
  body: JSON.stringify({ ok: n }),
@@ -343,27 +344,27 @@ var W = V("_voltro_notification_inbox", {
343
344
  maxBodyBytes: 4096
344
345
  }]
345
346
  } : {},
346
- services: Ve.succeed(X, b),
347
+ services: Ue.succeed(X, b),
347
348
  routes: [...S, ...C],
348
- rpcClientDescriptors: g ? [...v, ...pe] : v,
349
- inspectEndpoints: O,
349
+ rpcClientDescriptors: y ? [..._, ...he] : _,
350
+ inspectEndpoints: k,
350
351
  bindDataStore: (t) => {
351
- o = t, i && (a.current = l(t)), ye(t);
352
+ o = t, i && (a.current = l(t)), xe(t);
352
353
  let n = e.flushIntervalMs ?? 3e4;
353
354
  x = setInterval(() => {
354
355
  b.flushDue().catch(() => {});
355
356
  }, n), typeof x.unref == "function" && x.unref();
356
357
  },
357
- onActivate: (e) => F.sync(() => {
358
- g && ve(), e.logger.info("notifications active", {
359
- channels: m.map((e) => e.id),
360
- digestWindowMs: h
358
+ onActivate: (e) => P.sync(() => {
359
+ y && be(), e.logger.info("notifications active", {
360
+ channels: h.map((e) => e.id),
361
+ digestWindowMs: g
361
362
  });
362
363
  }),
363
- onDeactivate: () => F.sync(() => {
364
+ onDeactivate: () => P.sync(() => {
364
365
  x && clearInterval(x), x = void 0;
365
366
  })
366
367
  });
367
368
  };
368
369
  //#endregion
369
- export { Te as APNS_HOSTS, Ee as APNS_TOKEN_REJECTIONS, Le as EXPO_PUSH_ENDPOINT, Ae as FCM_SCOPE, je as FCM_TOKEN_REJECTIONS, P as GOOGLE_TOKEN_URI, Se as NOTIFICATION_SEND_EFFECT, X as NotificationService, e as PushTokenRejected, De as apnsBody, Oe as apnsProviderToken, ke as apnsTransport, d as buildNotificationService, t as consoleChannel, n as customChannel, l as dataStoreNotificationStore, E as dataStorePushSubscriptionStore, b as decryptWebPushPayload, N as defineNotification, K as deliveriesTable, r as emailChannel, x as encryptWebPushPayload, D as endpointHashOf, Ce as enqueueNotification, Re as expoPushTransport, Me as fcmAssertion, Ne as fcmMessage, Pe as fcmSendEndpoint, Fe as fcmTransport, S as generateVapidPrivateKey, Y as heldTable, f as inAppChannel, p as inQuietHours, W as inboxTable, ze as isExpoPushToken, Z as makeSubjectId, u as memoryNotificationStore, O as memoryPushSubscriptionStore, m as minuteOfDayInZone, we as notificationOutboxHandler, Je as notificationsPlugin, Ie as parseServiceAccount, G as preferencesTable, i as pushChannel, k as pushSubscriptionsTable, h as quietHoursEnd, J as quietHoursTable, be as resetWebPushStoreForTest, a as resolveChannels, o as richPushOf, xe as sendNotification, C as sendWebPush, s as smsChannel, q as topicSubscriptionsTable, w as vapidAuthorization, T as vapidPublicKeyFor, j as webPushChannel, M as webPushPublicKey, c as webhookChannel };
370
+ export { De as APNS_HOSTS, Oe as APNS_TOKEN_REJECTIONS, ze as EXPO_PUSH_ENDPOINT, Me as FCM_SCOPE, N as FCM_TOKEN_REJECTIONS, Ne as GOOGLE_TOKEN_URI, we as NOTIFICATION_SEND_EFFECT, X as NotificationService, e as PushTokenRejected, ke as apnsBody, Ae as apnsProviderToken, je as apnsTransport, d as buildNotificationService, t as consoleChannel, n as customChannel, l as dataStoreNotificationStore, T as dataStorePushSubscriptionStore, y as decryptWebPushPayload, Se as defineNotification, K as deliveriesTable, r as emailChannel, b as encryptWebPushPayload, E as endpointHashOf, Te as enqueueNotification, Be as expoPushTransport, Pe as fcmAssertion, Fe as fcmMessage, Ie as fcmSendEndpoint, Le as fcmTransport, x as generateVapidPrivateKey, Y as heldTable, f as inAppChannel, p as inQuietHours, W as inboxTable, Ve as isExpoPushToken, Z as makeSubjectId, u as memoryNotificationStore, D as memoryPushSubscriptionStore, m as minuteOfDayInZone, Ee as notificationOutboxHandler, Ye as notificationsPlugin, Re as parseServiceAccount, G as preferencesTable, i as pushChannel, O as pushSubscriptionsTable, h as quietHoursEnd, J as quietHoursTable, k as resetWebPushStoreForTest, a as resolveChannels, o as richPushOf, Ce as sendNotification, S as sendWebPush, s as smsChannel, q as topicSubscriptionsTable, C as vapidAuthorization, w as vapidPublicKeyFor, j as webPushChannel, M as webPushPublicKey, c as webhookChannel };
package/dist/rpc.d.ts CHANGED
@@ -22,6 +22,14 @@ export declare const archiveDescriptor: MutationProcedureDescriptor<"notificatio
22
22
  */
23
23
  export declare const BASE_NAME = "@voltro/plugin-notifications";
24
24
 
25
+ /** One channel switch of the calling subject for one category. */
26
+ export declare const ChannelPreferenceSchema: Schema.Struct<{
27
+ readonly subjectId: Schema.String;
28
+ readonly category: Schema.String;
29
+ readonly channel: Schema.String;
30
+ readonly enabled: Schema.Boolean;
31
+ }>;
32
+
25
33
  export declare const clearQuietHoursDescriptor: MutationProcedureDescriptor<"notifications.clearQuietHours", Schema.Struct<{}>, Schema.Struct<{
26
34
  readonly ok: Schema.Boolean;
27
35
  }>, Schema.Never>;
@@ -55,7 +63,15 @@ export declare const markUnreadDescriptor: MutationProcedureDescriptor<"notifica
55
63
  * emitted into the generated client group. Kept in lockstep with the exports. */
56
64
  export declare const notificationsRpcClientImports: ReadonlyArray<PluginRpcClientDescriptor>;
57
65
 
58
- export declare const preferencesDescriptor: QueryProcedureDescriptor<"notifications.preferences", Schema.Struct<{}>, Schema.$Array<Schema.$Record<Schema.String, Schema.Unknown>>, Schema.Never>;
66
+ export declare const preferencesDescriptor: QueryProcedureDescriptor<"notifications.preferences", Schema.Struct<{}>, Schema.$Array<Schema.Struct<{
67
+ readonly subjectId: Schema.String;
68
+ readonly category: Schema.String;
69
+ readonly channel: Schema.String;
70
+ readonly enabled: Schema.Boolean;
71
+ }>>, Schema.Never>;
72
+
73
+ /** Every preference write publishes this channel, so `notifications.preferences` subscribers see the change live. */
74
+ export declare const preferencesFeed: ReactivityChannel;
59
75
 
60
76
  export declare const setPreferenceDescriptor: MutationProcedureDescriptor<"notifications.setPreference", Schema.Struct<{
61
77
  readonly category: Schema.String;