@voltro/plugin-notifications 0.85.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/CHANGELOG.md +5987 -0
- package/THIRD-PARTY-NOTICES.md +1125 -34
- package/dist/apnsPush.js +97 -0
- package/dist/channels.js +92 -0
- package/dist/durable.js +42 -0
- package/dist/errors.js +8 -0
- package/dist/expoPush.js +35 -0
- package/dist/fcmPush.js +82 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +203 -1322
- package/dist/rpc.d.ts +17 -1
- package/dist/rpc.js +27 -21
- package/dist/sendNotification.js +42 -0
- package/dist/service.js +246 -0
- package/dist/store.js +303 -0
- package/dist/webPush.js +161 -0
- package/dist/webPushProtocol.js +117 -0
- package/package.json +4 -4
package/dist/apnsPush.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { PushTokenRejected as e } from "./errors.js";
|
|
2
|
+
import { sign as t } from "node:crypto";
|
|
3
|
+
//#region src/apnsPush.ts
|
|
4
|
+
var n = {
|
|
5
|
+
production: "https://api.push.apple.com",
|
|
6
|
+
sandbox: "https://api.sandbox.push.apple.com"
|
|
7
|
+
}, r = 3e6, i = /* @__PURE__ */ new Set([
|
|
8
|
+
"BadDeviceToken",
|
|
9
|
+
"Unregistered",
|
|
10
|
+
"DeviceTokenNotForTopic",
|
|
11
|
+
"ExpiredToken"
|
|
12
|
+
]), a = (e) => Buffer.from(e).toString("base64url"), o = (e, n) => {
|
|
13
|
+
let r = a(JSON.stringify({
|
|
14
|
+
alg: "ES256",
|
|
15
|
+
kid: e.keyId
|
|
16
|
+
})), i = a(JSON.stringify({
|
|
17
|
+
iss: e.teamId,
|
|
18
|
+
iat: Math.floor(n / 1e3)
|
|
19
|
+
}));
|
|
20
|
+
return `${r}.${i}.${a(t("sha256", Buffer.from(`${r}.${i}`), {
|
|
21
|
+
key: e.privateKey,
|
|
22
|
+
dsaEncoding: "ieee-p1363"
|
|
23
|
+
}))}`;
|
|
24
|
+
}, s = (e) => ({
|
|
25
|
+
aps: {
|
|
26
|
+
alert: {
|
|
27
|
+
title: e.title,
|
|
28
|
+
body: e.body
|
|
29
|
+
},
|
|
30
|
+
...e.badge === void 0 ? {} : { badge: e.badge },
|
|
31
|
+
sound: "default",
|
|
32
|
+
...e.image === void 0 ? {} : { "mutable-content": 1 },
|
|
33
|
+
...e.actions !== void 0 && typeof e.data.notification == "string" ? { category: e.data.notification } : {}
|
|
34
|
+
},
|
|
35
|
+
...e.data
|
|
36
|
+
}), c = async ({ url: e, headers: t, body: n }) => {
|
|
37
|
+
let r = await import("node:http2"), i = new URL(e), a = r.connect(i.origin);
|
|
38
|
+
try {
|
|
39
|
+
return await new Promise((e, r) => {
|
|
40
|
+
a.once("error", r);
|
|
41
|
+
let o = a.request({
|
|
42
|
+
":method": "POST",
|
|
43
|
+
":path": i.pathname,
|
|
44
|
+
...t
|
|
45
|
+
}), s = 0, c = [];
|
|
46
|
+
o.on("response", (e) => {
|
|
47
|
+
s = Number(e[":status"] ?? 0);
|
|
48
|
+
}), o.on("data", (e) => c.push(e)), o.on("end", () => e({
|
|
49
|
+
status: s,
|
|
50
|
+
body: Buffer.concat(c).toString("utf8")
|
|
51
|
+
})), o.on("error", r), o.end(n);
|
|
52
|
+
});
|
|
53
|
+
} finally {
|
|
54
|
+
a.close();
|
|
55
|
+
}
|
|
56
|
+
}, l = (t) => {
|
|
57
|
+
let a = [
|
|
58
|
+
"keyId",
|
|
59
|
+
"teamId",
|
|
60
|
+
"privateKey",
|
|
61
|
+
"bundleId"
|
|
62
|
+
].filter((e) => !t[e]);
|
|
63
|
+
if (a.length > 0) throw Error(`apnsTransport: ${a.join(", ")} required — supply APNS_KEY_ID, APNS_TEAM_ID, APNS_PRIVATE_KEY (the .p8) and APNS_BUNDLE_ID from the environment`);
|
|
64
|
+
let l = t.request ?? c, u = t.now ?? Date.now, d = n[t.environment ?? "production"], f = null, p = () => {
|
|
65
|
+
let e = u();
|
|
66
|
+
return (f === null || e - f.at > r) && (f = {
|
|
67
|
+
token: o(t, e),
|
|
68
|
+
at: e
|
|
69
|
+
}), f.token;
|
|
70
|
+
};
|
|
71
|
+
return async (n) => {
|
|
72
|
+
let r = await l({
|
|
73
|
+
url: `${d}/3/device/${encodeURIComponent(n.token)}`,
|
|
74
|
+
headers: {
|
|
75
|
+
authorization: `bearer ${p()}`,
|
|
76
|
+
"apns-topic": t.bundleId,
|
|
77
|
+
"apns-push-type": "alert",
|
|
78
|
+
"apns-priority": "10",
|
|
79
|
+
"content-type": "application/json"
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(s(n))
|
|
82
|
+
});
|
|
83
|
+
if (r.status === 200) return;
|
|
84
|
+
let a = "";
|
|
85
|
+
try {
|
|
86
|
+
a = String(JSON.parse(r.body).reason ?? "");
|
|
87
|
+
} catch {
|
|
88
|
+
a = "";
|
|
89
|
+
}
|
|
90
|
+
throw r.status === 410 || i.has(a) ? new e({
|
|
91
|
+
token: n.token,
|
|
92
|
+
reason: a || "Unregistered"
|
|
93
|
+
}) : Error(`apns: HTTP ${r.status}${a ? ` — ${a}` : ""}`);
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
export { n as APNS_HOSTS, i as APNS_TOKEN_REJECTIONS, s as apnsBody, o as apnsProviderToken, l as apnsTransport };
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { PushTokenRejected as e } from "./errors.js";
|
|
2
|
+
//#region src/channels.ts
|
|
3
|
+
var t = () => ({
|
|
4
|
+
id: "console",
|
|
5
|
+
deliver: async (e) => {
|
|
6
|
+
process.stdout.write(`[notify:${e.category}] → ${e.to}: ${e.title} — ${e.body}\n`);
|
|
7
|
+
}
|
|
8
|
+
}), n = (e) => ({
|
|
9
|
+
id: e.id ?? "slack",
|
|
10
|
+
deliver: async (t) => {
|
|
11
|
+
let n = e.format ? e.format(t) : { text: `*${t.title}*\n${t.body}` }, r = await fetch(e.url, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"content-type": "application/json",
|
|
15
|
+
...e.headers ?? {}
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify(n)
|
|
18
|
+
});
|
|
19
|
+
if (!r.ok) throw Error(`webhook ${e.id ?? "slack"} POST ${r.status}`);
|
|
20
|
+
}
|
|
21
|
+
}), r = (e) => ({
|
|
22
|
+
id: "email",
|
|
23
|
+
deliver: (t) => e({
|
|
24
|
+
to: t.to,
|
|
25
|
+
subject: t.title,
|
|
26
|
+
html: `<p>${t.body}</p>`,
|
|
27
|
+
text: t.body
|
|
28
|
+
})
|
|
29
|
+
}), i = (e) => ({
|
|
30
|
+
id: "sms",
|
|
31
|
+
deliver: (t) => e(t.to, `${t.title}: ${t.body}`)
|
|
32
|
+
}), a = (e, t) => ({
|
|
33
|
+
id: e,
|
|
34
|
+
deliver: t
|
|
35
|
+
}), o = (e) => {
|
|
36
|
+
let t = typeof e?.image == "string" ? e.image : void 0, n = e?.actions, r = Array.isArray(n) ? n.filter((e) => typeof e == "object" && !!e && typeof e.id == "string" && typeof e.title == "string").map((e) => ({
|
|
37
|
+
id: e.id,
|
|
38
|
+
title: e.title
|
|
39
|
+
})) : void 0;
|
|
40
|
+
return {
|
|
41
|
+
...t === void 0 ? {} : { image: t },
|
|
42
|
+
...r !== void 0 && r.length > 0 ? { actions: r } : {}
|
|
43
|
+
};
|
|
44
|
+
}, s = (t) => {
|
|
45
|
+
let n = async (n, r, i) => {
|
|
46
|
+
let a = {
|
|
47
|
+
token: r,
|
|
48
|
+
title: n.title,
|
|
49
|
+
body: n.body,
|
|
50
|
+
...i === void 0 ? {} : { badge: i },
|
|
51
|
+
data: n.data ?? {},
|
|
52
|
+
...o(n.data)
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
return await t.transport(a), {
|
|
56
|
+
endpoint: r,
|
|
57
|
+
status: "sent"
|
|
58
|
+
};
|
|
59
|
+
} catch (n) {
|
|
60
|
+
let i = n instanceof e, a = i ? n.reason : n instanceof Error ? n.message : String(n);
|
|
61
|
+
if (i && t.onTokenRejected !== void 0) try {
|
|
62
|
+
await t.onTokenRejected(r, a);
|
|
63
|
+
} catch {}
|
|
64
|
+
return {
|
|
65
|
+
endpoint: r,
|
|
66
|
+
status: "failed",
|
|
67
|
+
error: a,
|
|
68
|
+
...i ? { pruned: t.onTokenRejected !== void 0 } : {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}, r = async (e) => {
|
|
72
|
+
let r = await t.tokensFor(e.to), i = e.data?.badge, a = typeof i == "number" ? i : void 0, o = [];
|
|
73
|
+
for (let t of r) o.push(await n(e, t, a));
|
|
74
|
+
return o;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
id: t.id ?? "push",
|
|
78
|
+
deliver: async (t) => {
|
|
79
|
+
let n = await r(t), i = n.filter((e) => e.status === "failed");
|
|
80
|
+
if (n.length > 0 && i.length === n.length) throw new e({
|
|
81
|
+
token: n[0].endpoint,
|
|
82
|
+
reason: i[0].error ?? "all tokens rejected"
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
deliverDetailed: r
|
|
86
|
+
};
|
|
87
|
+
}, c = (e, t, n, r, i) => {
|
|
88
|
+
let a = n ? e.filter((e) => n.includes(e)) : [...e], o = new Set(r.filter((e) => e.subjectId === i && e.category === t && e.enabled === !1).map((e) => e.channel));
|
|
89
|
+
return a.filter((e) => !o.has(e));
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { t as consoleChannel, a as customChannel, r as emailChannel, s as pushChannel, c as resolveChannels, o as richPushOf, i as smsChannel, n as webhookChannel };
|
package/dist/durable.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defaultRecipients as e, sendInputFor as t } from "./sendNotification.js";
|
|
2
|
+
import { Schema as n } from "effect";
|
|
3
|
+
//#region src/durable.ts
|
|
4
|
+
var r = "voltro.notification.send", i = async (t, i, a) => {
|
|
5
|
+
let o = n.decodeUnknownSync(i.audience)(a.audience), s = n.is(i.payload)(a.payload) ? n.encodeUnknownSync(i.payload)(a.payload) : (n.decodeUnknownSync(i.payload)(a.payload), a.payload), c = a.recipients ? await a.recipients(o) : e(o), l = [];
|
|
6
|
+
for (let e of c) {
|
|
7
|
+
let n = (a.localeFor ? await a.localeFor(e) : null) ?? a.defaultLocale ?? "en", o = {
|
|
8
|
+
notification: i.name,
|
|
9
|
+
recipientId: e,
|
|
10
|
+
locale: n,
|
|
11
|
+
payload: s,
|
|
12
|
+
tenantId: a.tenantId ?? null
|
|
13
|
+
}, c = await t.enqueue(r, { ...o }, {
|
|
14
|
+
...a.idempotencyKey === void 0 ? {} : { idempotencyKey: `${i.name}:${e}:${a.idempotencyKey}` },
|
|
15
|
+
...a.maxAttempts === void 0 ? {} : { maxAttempts: a.maxAttempts },
|
|
16
|
+
...a.delayMs === void 0 ? {} : { delayMs: a.delayMs }
|
|
17
|
+
});
|
|
18
|
+
l.push({
|
|
19
|
+
recipientId: e,
|
|
20
|
+
locale: n,
|
|
21
|
+
outboxId: c
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return l;
|
|
25
|
+
}, a = (e) => typeof e.notification == "string" && typeof e.recipientId == "string" && typeof e.locale == "string", o = (e) => {
|
|
26
|
+
let i = new Map(e.notifications.map((e) => [e.name, e]));
|
|
27
|
+
return {
|
|
28
|
+
effect: r,
|
|
29
|
+
maxAttempts: e.maxAttempts ?? 5,
|
|
30
|
+
handler: async ({ payload: o }) => {
|
|
31
|
+
if (!a(o)) throw Error(`${r}: row payload needs notification, recipientId and locale — got keys ${Object.keys(o).join(", ") || "(none)"}`);
|
|
32
|
+
let s = i.get(o.notification);
|
|
33
|
+
if (s === void 0) throw Error(`${r}: no descriptor named ${JSON.stringify(o.notification)} in this handler — list it in notificationOutboxHandler({ notifications })`);
|
|
34
|
+
let c = e.service();
|
|
35
|
+
if (c === void 0) throw Error(`${r}: the notification service is not available in this process`);
|
|
36
|
+
let l = n.decodeUnknownSync(s.payload)(o.payload);
|
|
37
|
+
return c.send(t(s, l, o.recipientId, o.locale, o.tenantId));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { r as NOTIFICATION_SEND_EFFECT, i as enqueueNotification, o as notificationOutboxHandler };
|
package/dist/errors.js
ADDED
package/dist/expoPush.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PushTokenRejected as e } from "./errors.js";
|
|
2
|
+
//#region src/expoPush.ts
|
|
3
|
+
var t = "https://exp.host/--/api/v2/push/send", n = (e) => /^Expo(nent)?PushToken\[[^\]]+\]$/.test(e), r = (t = {}) => async (r) => {
|
|
4
|
+
if (!n(r.token)) throw new e({
|
|
5
|
+
token: r.token,
|
|
6
|
+
reason: "not an Expo push token"
|
|
7
|
+
});
|
|
8
|
+
let i = await (t.fetch ?? fetch)(t.endpoint ?? "https://exp.host/--/api/v2/push/send", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
accept: "application/json",
|
|
13
|
+
...t.accessToken ? { authorization: `Bearer ${t.accessToken}` } : {}
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
to: r.token,
|
|
17
|
+
title: r.title,
|
|
18
|
+
body: r.body,
|
|
19
|
+
data: r.data,
|
|
20
|
+
...r.badge === void 0 ? {} : { badge: r.badge },
|
|
21
|
+
...r.image === void 0 ? {} : { richContent: { image: r.image } },
|
|
22
|
+
...r.actions !== void 0 && typeof r.data.notification == "string" ? { categoryId: r.data.notification } : {}
|
|
23
|
+
})
|
|
24
|
+
}), a = await i.json().catch(() => ({}));
|
|
25
|
+
if (!i.ok) throw Error(`expo push: HTTP ${i.status}${a.errors?.[0]?.message ? ` — ${a.errors[0].message}` : ""}`);
|
|
26
|
+
let o = Array.isArray(a.data) ? a.data[0] : a.data;
|
|
27
|
+
if (!o || o.status === "ok") return;
|
|
28
|
+
let s = o.details?.error;
|
|
29
|
+
throw s === "DeviceNotRegistered" || s === "InvalidCredentials" ? new e({
|
|
30
|
+
token: r.token,
|
|
31
|
+
reason: s
|
|
32
|
+
}) : Error(`expo push: ${s ?? "error"}${o.message ? ` — ${o.message}` : ""}`);
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { t as EXPO_PUSH_ENDPOINT, r as expoPushTransport, n as isExpoPushToken };
|
package/dist/fcmPush.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { PushTokenRejected as e } from "./errors.js";
|
|
2
|
+
import { sign as t } from "node:crypto";
|
|
3
|
+
//#region src/fcmPush.ts
|
|
4
|
+
var n = "https://www.googleapis.com/auth/firebase.messaging", r = "https://oauth2.googleapis.com/token", i = (e) => `https://fcm.googleapis.com/v1/projects/${encodeURIComponent(e)}/messages:send`, a = /* @__PURE__ */ new Set(["UNREGISTERED", "NOT_FOUND"]), o = (e) => Buffer.from(e).toString("base64url"), s = (e) => {
|
|
5
|
+
let t;
|
|
6
|
+
if (typeof e == "string") try {
|
|
7
|
+
t = JSON.parse(e);
|
|
8
|
+
} catch {
|
|
9
|
+
throw Error("fcmTransport: FCM_SERVICE_ACCOUNT is not the JSON Google issued");
|
|
10
|
+
}
|
|
11
|
+
else t = e;
|
|
12
|
+
let n = [
|
|
13
|
+
"project_id",
|
|
14
|
+
"client_email",
|
|
15
|
+
"private_key"
|
|
16
|
+
].filter((e) => typeof t[e] != "string" || t[e].length === 0);
|
|
17
|
+
if (n.length > 0) throw Error(`fcmTransport: service account is missing ${n.join(", ")} — supply FCM_SERVICE_ACCOUNT from the environment`);
|
|
18
|
+
return t;
|
|
19
|
+
}, c = (e, r) => {
|
|
20
|
+
let i = Math.floor(r / 1e3), a = o(JSON.stringify({
|
|
21
|
+
alg: "RS256",
|
|
22
|
+
typ: "JWT"
|
|
23
|
+
})), s = o(JSON.stringify({
|
|
24
|
+
iss: e.client_email,
|
|
25
|
+
scope: n,
|
|
26
|
+
aud: e.token_uri ?? "https://oauth2.googleapis.com/token",
|
|
27
|
+
iat: i,
|
|
28
|
+
exp: i + 3600
|
|
29
|
+
}));
|
|
30
|
+
return `${a}.${s}.${o(t("sha256", Buffer.from(`${a}.${s}`), e.private_key))}`;
|
|
31
|
+
}, l = (e) => ({ message: {
|
|
32
|
+
token: e.token,
|
|
33
|
+
notification: {
|
|
34
|
+
title: e.title,
|
|
35
|
+
body: e.body,
|
|
36
|
+
...e.image === void 0 ? {} : { image: e.image }
|
|
37
|
+
},
|
|
38
|
+
data: Object.fromEntries(Object.entries(e.data).map(([e, t]) => [e, typeof t == "string" ? t : JSON.stringify(t)])),
|
|
39
|
+
...e.badge !== void 0 || e.image !== void 0 ? { apns: {
|
|
40
|
+
payload: { aps: {
|
|
41
|
+
...e.badge === void 0 ? {} : { badge: e.badge },
|
|
42
|
+
...e.image === void 0 ? {} : { "mutable-content": 1 }
|
|
43
|
+
} },
|
|
44
|
+
...e.image === void 0 ? {} : { fcm_options: { image: e.image } }
|
|
45
|
+
} } : {}
|
|
46
|
+
} }), u = (t) => {
|
|
47
|
+
let n = s(t.serviceAccount), r = t.fetch ?? fetch, o = t.now ?? Date.now, u = t.endpoint ?? i(n.project_id), d = null, f = async () => {
|
|
48
|
+
let e = o();
|
|
49
|
+
if (d !== null && d.expiresAt - 6e4 > e) return d.token;
|
|
50
|
+
let t = await r(n.token_uri ?? "https://oauth2.googleapis.com/token", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
53
|
+
body: new URLSearchParams({
|
|
54
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
55
|
+
assertion: c(n, e)
|
|
56
|
+
}).toString()
|
|
57
|
+
}), i = await t.json().catch(() => ({}));
|
|
58
|
+
if (!t.ok || typeof i.access_token != "string") throw Error(`fcm: token exchange HTTP ${t.status}${i.error ? ` — ${i.error}` : ""}`);
|
|
59
|
+
return d = {
|
|
60
|
+
token: i.access_token,
|
|
61
|
+
expiresAt: e + (i.expires_in ?? 3600) * 1e3
|
|
62
|
+
}, d.token;
|
|
63
|
+
};
|
|
64
|
+
return async (t) => {
|
|
65
|
+
let n = await r(u, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
authorization: `Bearer ${await f()}`,
|
|
69
|
+
"content-type": "application/json"
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify(l(t))
|
|
72
|
+
});
|
|
73
|
+
if (n.ok) return;
|
|
74
|
+
let i = await n.json().catch(() => ({})), o = i.error?.details?.find((e) => typeof e.errorCode == "string")?.errorCode ?? i.error?.status ?? "";
|
|
75
|
+
throw n.status === 404 || a.has(o) ? new e({
|
|
76
|
+
token: t.token,
|
|
77
|
+
reason: o || "UNREGISTERED"
|
|
78
|
+
}) : Error(`fcm: HTTP ${n.status}${o ? ` — ${o}` : ""}${i.error?.message ? ` — ${i.error.message}` : ""}`);
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { n as FCM_SCOPE, a as FCM_TOKEN_REJECTIONS, r as GOOGLE_TOKEN_URI, c as fcmAssertion, l as fcmMessage, i as fcmSendEndpoint, u as fcmTransport, s as parseServiceAccount };
|
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;
|