@voltro/plugin-notifications 0.86.0 → 0.88.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.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";
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)
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
+ // src/index.ts
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,26 @@ 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
- //#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 };
369
+ 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;
package/dist/rpc.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import { Schema as e } from "effect";
2
2
  import { defineMutation as t, defineQuery as n, reactivityChannel as r } from "@voltro/protocol";
3
- //#region src/rpc.ts
4
- var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
3
+ // src/rpc.ts
4
+ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = r("notifications-preferences"), s = e.Struct({
5
+ subjectId: e.String,
6
+ category: e.String,
7
+ channel: e.String,
8
+ enabled: e.Boolean
9
+ }), c = n({
5
10
  name: "notifications.inbox",
6
11
  source: a,
7
12
  input: e.Struct({
@@ -10,43 +15,44 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
10
15
  }),
11
16
  output: e.Array(e.Record(e.String, e.Unknown)),
12
17
  openAccess: "self-scoped read: returns only the calling subject's own inbox items"
13
- }), s = n({
18
+ }), l = n({
14
19
  name: "notifications.unreadCount",
15
20
  source: a,
16
21
  input: e.Struct({}),
17
22
  output: e.Struct({ count: e.Number }),
18
23
  openAccess: "self-scoped read: counts only the calling subject's own unread items"
19
- }), c = t({
24
+ }), u = t({
20
25
  name: "notifications.markRead",
21
26
  input: e.Struct({ id: e.String }),
22
27
  output: e.Struct({ ok: e.Boolean }),
23
28
  openAccess: "self-scoped write: marks an item read only within the calling subject's own inbox"
24
- }), l = t({
29
+ }), d = t({
25
30
  name: "notifications.markUnread",
26
31
  input: e.Struct({ id: e.String }),
27
32
  output: e.Struct({ ok: e.Boolean }),
28
33
  openAccess: "self-scoped write: marks an item unread only within the calling subject's own inbox"
29
- }), u = t({
34
+ }), f = t({
30
35
  name: "notifications.markAllRead",
31
36
  input: e.Struct({}),
32
37
  output: e.Struct({ count: e.Number }),
33
38
  openAccess: "self-scoped write: clears unread state only in the calling subject's own inbox"
34
- }), d = t({
39
+ }), p = t({
35
40
  name: "notifications.archive",
36
41
  input: e.Struct({ id: e.String }),
37
42
  output: e.Struct({ ok: e.Boolean }),
38
43
  openAccess: "self-scoped write: archives an item only within the calling subject's own inbox"
39
- }), f = t({
44
+ }), m = t({
40
45
  name: "notifications.unarchive",
41
46
  input: e.Struct({ id: e.String }),
42
47
  output: e.Struct({ ok: e.Boolean }),
43
48
  openAccess: "self-scoped write: unarchives an item only within the calling subject's own inbox"
44
- }), p = n({
49
+ }), h = n({
45
50
  name: "notifications.preferences",
51
+ source: o,
46
52
  input: e.Struct({}),
47
- output: e.Array(e.Record(e.String, e.Unknown)),
53
+ output: e.Array(s),
48
54
  openAccess: "self-scoped read: returns only the calling subject's own channel preferences"
49
- }), m = t({
55
+ }), g = t({
50
56
  name: "notifications.setPreference",
51
57
  input: e.Struct({
52
58
  category: e.String,
@@ -55,17 +61,17 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
55
61
  }),
56
62
  output: e.Struct({ ok: e.Boolean }),
57
63
  openAccess: "self-scoped write: toggles a channel only on the calling subject's own preferences"
58
- }), h = t({
64
+ }), _ = t({
59
65
  name: "notifications.subscribe",
60
66
  input: e.Struct({ topic: e.String }),
61
67
  output: e.Struct({ ok: e.Boolean }),
62
68
  openAccess: "self-service: subscribes the calling subject itself to a topic (the subject is never caller-supplied)"
63
- }), g = t({
69
+ }), v = t({
64
70
  name: "notifications.unsubscribe",
65
71
  input: e.Struct({ topic: e.String }),
66
72
  output: e.Struct({ ok: e.Boolean }),
67
73
  openAccess: "self-service: unsubscribes the calling subject itself from a topic"
68
- }), _ = t({
74
+ }), y = t({
69
75
  name: "notifications.setQuietHours",
70
76
  input: e.Struct({
71
77
  startMinute: e.Number,
@@ -75,17 +81,17 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
75
81
  }),
76
82
  output: e.Struct({ ok: e.Boolean }),
77
83
  openAccess: "self-scoped write: sets only the calling subject's own quiet-hours window"
78
- }), v = t({
84
+ }), b = t({
79
85
  name: "notifications.clearQuietHours",
80
86
  input: e.Struct({}),
81
87
  output: e.Struct({ ok: e.Boolean }),
82
88
  openAccess: "self-scoped write: clears only the calling subject's own quiet-hours window"
83
- }), y = n({
89
+ }), x = n({
84
90
  name: "notifications.webPushPublicKey",
85
91
  input: e.Struct({}),
86
92
  output: e.Struct({ key: e.String }),
87
93
  openAccess: "public application-server key — the value every subscribing browser is handed by design"
88
- }), b = t({
94
+ }), S = t({
89
95
  name: "notifications.webPushSubscribe",
90
96
  input: e.Struct({
91
97
  endpoint: e.String,
@@ -95,17 +101,17 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
95
101
  }),
96
102
  output: e.Struct({ ok: e.Boolean }),
97
103
  openAccess: "self-scoped write: registers the calling subject's own browser subscription (the subject is never caller-supplied)"
98
- }), x = t({
104
+ }), C = t({
99
105
  name: "notifications.webPushUnsubscribe",
100
106
  input: e.Struct({ endpoint: e.String }),
101
107
  output: e.Struct({ ok: e.Boolean }),
102
108
  openAccess: "self-scoped write: removes only a subscription the calling subject owns"
103
- }), S = n({
109
+ }), w = n({
104
110
  name: "notifications.webPushStatus",
105
111
  input: e.Struct({}),
106
112
  output: e.Struct({ endpoints: e.Number }),
107
113
  openAccess: "self-scoped read: counts only the calling subject's own subscriptions"
108
- }), C = [
114
+ }), T = [
109
115
  {
110
116
  tag: "notifications.webPushPublicKey",
111
117
  kind: "query",
@@ -138,7 +144,7 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
138
144
  name: "webPushStatusDescriptor"
139
145
  }
140
146
  }
141
- ], w = [
147
+ ], E = [
142
148
  {
143
149
  tag: "notifications.inbox",
144
150
  kind: "query",
@@ -244,5 +250,4 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
244
250
  }
245
251
  }
246
252
  ];
247
- //#endregion
248
- export { i as BASE_NAME, d as archiveDescriptor, v as clearQuietHoursDescriptor, o as inboxDescriptor, a as inboxFeed, u as markAllReadDescriptor, c as markReadDescriptor, l as markUnreadDescriptor, w as notificationsRpcClientImports, p as preferencesDescriptor, m as setPreferenceDescriptor, _ as setQuietHoursDescriptor, h as subscribeDescriptor, f as unarchiveDescriptor, s as unreadCountDescriptor, g as unsubscribeDescriptor, y as webPushPublicKeyDescriptor, C as webPushRpcClientImports, S as webPushStatusDescriptor, b as webPushSubscribeDescriptor, x as webPushUnsubscribeDescriptor };
253
+ export { i as BASE_NAME, s as ChannelPreferenceSchema, p as archiveDescriptor, b as clearQuietHoursDescriptor, c as inboxDescriptor, a as inboxFeed, f as markAllReadDescriptor, u as markReadDescriptor, d as markUnreadDescriptor, E as notificationsRpcClientImports, h as preferencesDescriptor, o as preferencesFeed, g as setPreferenceDescriptor, y as setQuietHoursDescriptor, _ as subscribeDescriptor, m as unarchiveDescriptor, l as unreadCountDescriptor, v as unsubscribeDescriptor, x as webPushPublicKeyDescriptor, T as webPushRpcClientImports, w as webPushStatusDescriptor, S as webPushSubscribeDescriptor, C as webPushUnsubscribeDescriptor };