@12-apps/notifications 4.12.0 → 4.13.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/{chunk-PNY6S6WH.js → chunk-4DTUD74E.js} +2 -2
- package/dist/{chunk-WZBX7YCE.js → chunk-53SH5ABN.js} +82 -34
- package/dist/chunk-53SH5ABN.js.map +1 -0
- package/dist/{chunk-CPQKKLPS.js → chunk-CSMFJJXY.js} +1 -1
- package/dist/{chunk-CPQKKLPS.js.map → chunk-CSMFJJXY.js.map} +1 -1
- package/dist/{chunk-FBBPS2LT.js → chunk-QPO6NSRR.js} +2 -2
- package/dist/{chunk-GK6GSC2J.js → chunk-XFODKRRB.js} +2 -2
- package/dist/{create-api-notifications-BTudlaSC.d.ts → create-api-notifications-cIEbFqoZ.d.ts} +121 -30
- package/dist/{create-web-notifications-2xxbKnrW.d.ts → create-web-notifications-BODiQRK-.d.ts} +2 -2
- package/dist/{generators-CQYdJfB5.d.ts → generators-GUF-Kml-.d.ts} +1 -1
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/{jobs-CaovU4GM.d.ts → jobs-wKoOz91Q.d.ts} +1 -1
- package/dist/manifest/server.d.ts +6 -6
- package/dist/manifest/server.js +4 -4
- package/dist/manifest/web.d.ts +3 -3
- package/dist/manifest/web.js +1 -1
- package/dist/{preferences-screen-S3ZHX5LB.js → preferences-screen-YRPNUZS5.js} +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +4 -4
- package/dist/{types-H_aFzLA0.d.ts → types-DPiePHJD.d.ts} +19 -1
- package/dist/web-push/index.d.ts +2 -2
- package/dist/{web-push-C6U-5JCV.d.ts → web-push-DUn_d_gj.d.ts} +12 -2
- package/dist/{wire-Bn6aA2nL.d.ts → wire-CJka1AvM.d.ts} +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20260914210000_scope_push_subscriptions_per_store/migration.sql +31 -0
- package/prisma/notifications.prisma +12 -0
- package/src/server/context.ts +15 -0
- package/src/server/db.ts +50 -8
- package/src/server/dispatch.ts +9 -2
- package/src/server/inbox.ts +87 -18
- package/src/server/index.ts +2 -0
- package/src/server/push-subscriptions.ts +61 -7
- package/src/server/router.ts +5 -1
- package/src/server/routes.ts +14 -5
- package/src/server/transports/web-push.ts +15 -2
- package/src/types.ts +19 -1
- package/dist/chunk-WZBX7YCE.js.map +0 -1
- /package/dist/{chunk-PNY6S6WH.js.map → chunk-4DTUD74E.js.map} +0 -0
- /package/dist/{chunk-FBBPS2LT.js.map → chunk-QPO6NSRR.js.map} +0 -0
- /package/dist/{chunk-GK6GSC2J.js.map → chunk-XFODKRRB.js.map} +0 -0
- /package/dist/{preferences-screen-S3ZHX5LB.js.map → preferences-screen-YRPNUZS5.js.map} +0 -0
|
@@ -306,7 +306,7 @@ import { Suspense as Suspense2, lazy as lazy2 } from "react";
|
|
|
306
306
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
307
307
|
function lazyPreferencesPage(parts) {
|
|
308
308
|
const Bound = lazy2(async () => {
|
|
309
|
-
const { PreferencesScreen } = await import("./preferences-screen-
|
|
309
|
+
const { PreferencesScreen } = await import("./preferences-screen-YRPNUZS5.js");
|
|
310
310
|
return {
|
|
311
311
|
default: /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx3(PreferencesScreen, { ...props, ...parts }), "default")
|
|
312
312
|
};
|
|
@@ -395,4 +395,4 @@ export {
|
|
|
395
395
|
httpNotificationsTransport,
|
|
396
396
|
createWebNotifications
|
|
397
397
|
};
|
|
398
|
-
//# sourceMappingURL=chunk-
|
|
398
|
+
//# sourceMappingURL=chunk-4DTUD74E.js.map
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
mergeStoredRow,
|
|
10
10
|
normalizePhoneE164,
|
|
11
11
|
resolveTypeChannels
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XFODKRRB.js";
|
|
13
13
|
import {
|
|
14
14
|
messagesOf
|
|
15
15
|
} from "./chunk-M2TVBVH2.js";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
NOTIFICATION_CHANNELS,
|
|
21
21
|
taxonomyOf
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-CSMFJJXY.js";
|
|
23
23
|
import {
|
|
24
24
|
renderEmail
|
|
25
25
|
} from "./chunk-EKUSNUBT.js";
|
|
@@ -439,7 +439,7 @@ function webPushTransport(declaration, subscriptions, extraDrivers = {}) {
|
|
|
439
439
|
supports: /* @__PURE__ */ __name((recipient) => recipient.pushSubscriptionCount > 0, "supports"),
|
|
440
440
|
format: formatWebPush,
|
|
441
441
|
async send(message, recipient) {
|
|
442
|
-
const rows = await subscriptions.list(recipient.userId);
|
|
442
|
+
const rows = await subscriptions.list(recipient.userId, recipient.clientId);
|
|
443
443
|
if (rows.length === 0) throw new Error("Recipient no longer has push subscriptions.");
|
|
444
444
|
const payload = JSON.stringify(message);
|
|
445
445
|
let delivered = 0;
|
|
@@ -682,7 +682,7 @@ var DEFAULT_MAX_DELIVERY_ATTEMPTS = 5;
|
|
|
682
682
|
var DEFAULT_SWEEP_TAKE = 200;
|
|
683
683
|
var DEFAULT_SWEEP_CUTOFF_MS = 5 * 6e4;
|
|
684
684
|
var RETRYABLE = ["FAILED", "QUEUED", "SENDING"];
|
|
685
|
-
async function loadRecipient(deps, userId) {
|
|
685
|
+
async function loadRecipient(deps, userId, clientId) {
|
|
686
686
|
const contact = await deps.contacts.getContact(userId);
|
|
687
687
|
if (!contact) return null;
|
|
688
688
|
return {
|
|
@@ -693,7 +693,11 @@ async function loadRecipient(deps, userId) {
|
|
|
693
693
|
// absent case has to stay distinguishable from a stated language, because
|
|
694
694
|
// that is what lets a generator apply its own default in one place.
|
|
695
695
|
...contact.locale === void 0 ? {} : { locale: contact.locale },
|
|
696
|
-
|
|
696
|
+
clientId,
|
|
697
|
+
// SCOPED, and this is what keeps `supports()` honest: it gates on this
|
|
698
|
+
// number, so an unscoped count would enqueue a WEB_PUSH delivery for a
|
|
699
|
+
// notification no reachable subscription exists for.
|
|
700
|
+
pushSubscriptionCount: await deps.pushSubscriptions.count(userId, clientId)
|
|
697
701
|
};
|
|
698
702
|
}
|
|
699
703
|
__name(loadRecipient, "loadRecipient");
|
|
@@ -759,7 +763,7 @@ async function dispatchOne(deps, notificationId) {
|
|
|
759
763
|
where: { notificationId, status: "QUEUED" }
|
|
760
764
|
});
|
|
761
765
|
if (queued.length === 0) return 0;
|
|
762
|
-
const recipient = await loadRecipient(deps, notification.userId);
|
|
766
|
+
const recipient = await loadRecipient(deps, notification.userId, notification.clientId);
|
|
763
767
|
if (!recipient) {
|
|
764
768
|
await abandonUnreachable(deps, client, queued, notification.userId);
|
|
765
769
|
return 0;
|
|
@@ -805,6 +809,10 @@ async function drainPending(deps, olderThanMs, take) {
|
|
|
805
809
|
__name(drainPending, "drainPending");
|
|
806
810
|
|
|
807
811
|
// src/server/inbox.ts
|
|
812
|
+
function scopeBranch(scope) {
|
|
813
|
+
return scope === void 0 ? [] : [{ OR: [{ clientId: scope }, { clientId: null }] }];
|
|
814
|
+
}
|
|
815
|
+
__name(scopeBranch, "scopeBranch");
|
|
808
816
|
var DEFAULT_PAGE = 20;
|
|
809
817
|
var MAX_PAGE = 100;
|
|
810
818
|
async function resolveAnchor(notifications, userId, cursor) {
|
|
@@ -813,31 +821,47 @@ async function resolveAnchor(notifications, userId, cursor) {
|
|
|
813
821
|
return { createdAt: anchor.createdAt, id: anchor.id };
|
|
814
822
|
}
|
|
815
823
|
__name(resolveAnchor, "resolveAnchor");
|
|
816
|
-
function pageWhere(userId, filter, anchor) {
|
|
824
|
+
function pageWhere(userId, filter, anchor, scope) {
|
|
825
|
+
const clauses = [
|
|
826
|
+
// `(createdAt, id) < (anchor.createdAt, anchor.id)`, as a portable `where`.
|
|
827
|
+
...anchor ? [
|
|
828
|
+
{
|
|
829
|
+
OR: [
|
|
830
|
+
{ createdAt: { lt: anchor.createdAt } },
|
|
831
|
+
{ createdAt: anchor.createdAt, id: { lt: anchor.id } }
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
] : [],
|
|
835
|
+
...scopeBranch(scope)
|
|
836
|
+
];
|
|
817
837
|
return {
|
|
818
838
|
userId,
|
|
819
839
|
deletedAt: null,
|
|
820
840
|
...filter === "unread" ? { readAt: null } : {},
|
|
821
|
-
|
|
822
|
-
...anchor ? {
|
|
823
|
-
OR: [
|
|
824
|
-
{ createdAt: { lt: anchor.createdAt } },
|
|
825
|
-
{ createdAt: anchor.createdAt, id: { lt: anchor.id } }
|
|
826
|
-
]
|
|
827
|
-
} : {}
|
|
841
|
+
...clauses.length > 0 ? { AND: clauses } : {}
|
|
828
842
|
};
|
|
829
843
|
}
|
|
830
844
|
__name(pageWhere, "pageWhere");
|
|
845
|
+
function unreadWhere(userId, scope) {
|
|
846
|
+
const scoped = scopeBranch(scope);
|
|
847
|
+
return {
|
|
848
|
+
userId,
|
|
849
|
+
deletedAt: null,
|
|
850
|
+
readAt: null,
|
|
851
|
+
...scoped.length > 0 ? { AND: scoped } : {}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
__name(unreadWhere, "unreadWhere");
|
|
831
855
|
function createInboxStore(db) {
|
|
832
856
|
return {
|
|
833
857
|
/** The owner's inbox, newest first, keyset-paginated, deleted excluded. */
|
|
834
|
-
async list(userId, input = {}) {
|
|
858
|
+
async list(userId, input = {}, scope) {
|
|
835
859
|
const client = await db();
|
|
836
860
|
const limit = Math.min(Math.max(input.limit ?? DEFAULT_PAGE, 1), MAX_PAGE);
|
|
837
861
|
const anchor = input.cursor ? await resolveAnchor(client.notification, userId, input.cursor) : void 0;
|
|
838
862
|
if (input.cursor && !anchor) return { items: [], nextCursor: null };
|
|
839
863
|
const rows = await client.notification.findMany({
|
|
840
|
-
where: pageWhere(userId, input.filter, anchor),
|
|
864
|
+
where: pageWhere(userId, input.filter, anchor, scope),
|
|
841
865
|
// `id` tie-breaks equal timestamps so pages never skip/repeat.
|
|
842
866
|
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
|
843
867
|
take: limit + 1
|
|
@@ -849,9 +873,9 @@ function createInboxStore(db) {
|
|
|
849
873
|
};
|
|
850
874
|
},
|
|
851
875
|
/** Unread badge count (non-deleted, unread). */
|
|
852
|
-
async unreadCount(userId) {
|
|
876
|
+
async unreadCount(userId, scope) {
|
|
853
877
|
const client = await db();
|
|
854
|
-
return client.notification.count({ where:
|
|
878
|
+
return client.notification.count({ where: unreadWhere(userId, scope) });
|
|
855
879
|
},
|
|
856
880
|
/**
|
|
857
881
|
* Mark specific notifications read. Only the owner's own, still-unread,
|
|
@@ -868,10 +892,10 @@ function createInboxStore(db) {
|
|
|
868
892
|
return result.count;
|
|
869
893
|
},
|
|
870
894
|
/** Mark every unread notification of the owner read ("mark all"). */
|
|
871
|
-
async markAllRead(userId) {
|
|
895
|
+
async markAllRead(userId, scope) {
|
|
872
896
|
const client = await db();
|
|
873
897
|
const result = await client.notification.updateMany({
|
|
874
|
-
where:
|
|
898
|
+
where: unreadWhere(userId, scope),
|
|
875
899
|
data: { readAt: /* @__PURE__ */ new Date() }
|
|
876
900
|
});
|
|
877
901
|
return result.count;
|
|
@@ -962,6 +986,11 @@ function createPreferenceStore(db, taxonomy, channelDefaults = {}) {
|
|
|
962
986
|
__name(createPreferenceStore, "createPreferenceStore");
|
|
963
987
|
|
|
964
988
|
// src/server/push-subscriptions.ts
|
|
989
|
+
function reachableBy(userId, notificationClientId) {
|
|
990
|
+
if (notificationClientId === void 0 || notificationClientId === null) return { userId };
|
|
991
|
+
return { userId, OR: [{ clientId: null }, { clientId: notificationClientId }] };
|
|
992
|
+
}
|
|
993
|
+
__name(reachableBy, "reachableBy");
|
|
965
994
|
function createPushSubscriptionStore(db, logger) {
|
|
966
995
|
return {
|
|
967
996
|
async save(userId, input) {
|
|
@@ -981,12 +1010,18 @@ function createPushSubscriptionStore(db, logger) {
|
|
|
981
1010
|
endpoint: input.endpoint,
|
|
982
1011
|
p256dh: input.keys.p256dh,
|
|
983
1012
|
auth: input.keys.auth,
|
|
1013
|
+
clientId: input.clientId ?? null,
|
|
984
1014
|
userAgent: input.userAgent ?? null
|
|
985
1015
|
},
|
|
1016
|
+
// Re-stamped on every save, so the same browser moving between a store's
|
|
1017
|
+
// app and the platform corrects its own scope rather than keeping the
|
|
1018
|
+
// first origin it ever subscribed from. A scope change on the SAME user
|
|
1019
|
+
// is not a re-own and must not log one.
|
|
986
1020
|
update: {
|
|
987
1021
|
userId,
|
|
988
1022
|
p256dh: input.keys.p256dh,
|
|
989
1023
|
auth: input.keys.auth,
|
|
1024
|
+
clientId: input.clientId ?? null,
|
|
990
1025
|
userAgent: input.userAgent ?? null
|
|
991
1026
|
}
|
|
992
1027
|
});
|
|
@@ -995,18 +1030,20 @@ function createPushSubscriptionStore(db, logger) {
|
|
|
995
1030
|
const client = await db();
|
|
996
1031
|
await client.pushSubscription.deleteMany({ where: { userId, endpoint } });
|
|
997
1032
|
},
|
|
998
|
-
async count(userId) {
|
|
1033
|
+
async count(userId, notificationClientId) {
|
|
999
1034
|
const client = await db();
|
|
1000
|
-
return client.pushSubscription.count({ where:
|
|
1035
|
+
return client.pushSubscription.count({ where: reachableBy(userId, notificationClientId) });
|
|
1001
1036
|
},
|
|
1002
1037
|
async isRegisteredTo(userId, endpoint) {
|
|
1003
1038
|
const client = await db();
|
|
1004
1039
|
const row = await client.pushSubscription.findUnique({ where: { endpoint } });
|
|
1005
1040
|
return row?.userId === userId;
|
|
1006
1041
|
},
|
|
1007
|
-
async list(userId) {
|
|
1042
|
+
async list(userId, notificationClientId) {
|
|
1008
1043
|
const client = await db();
|
|
1009
|
-
const rows = await client.pushSubscription.findMany({
|
|
1044
|
+
const rows = await client.pushSubscription.findMany({
|
|
1045
|
+
where: reachableBy(userId, notificationClientId)
|
|
1046
|
+
});
|
|
1010
1047
|
return rows.map((row) => ({
|
|
1011
1048
|
id: row.id,
|
|
1012
1049
|
endpoint: row.endpoint,
|
|
@@ -1023,12 +1060,13 @@ function createPushSubscriptionStore(db, logger) {
|
|
|
1023
1060
|
__name(createPushSubscriptionStore, "createPushSubscriptionStore");
|
|
1024
1061
|
|
|
1025
1062
|
// src/server/routes.ts
|
|
1026
|
-
async function channelAvailability(deps, userId) {
|
|
1063
|
+
async function channelAvailability(deps, userId, scopeClientId) {
|
|
1027
1064
|
const contact = await deps.contacts.getContact(userId);
|
|
1028
1065
|
const recipient = {
|
|
1029
1066
|
userId,
|
|
1030
1067
|
email: contact?.email ?? null,
|
|
1031
1068
|
phone: contact?.phone ?? null,
|
|
1069
|
+
clientId: scopeClientId,
|
|
1032
1070
|
pushSubscriptionCount: 1
|
|
1033
1071
|
};
|
|
1034
1072
|
return Object.fromEntries(
|
|
@@ -1039,10 +1077,10 @@ async function channelAvailability(deps, userId) {
|
|
|
1039
1077
|
);
|
|
1040
1078
|
}
|
|
1041
1079
|
__name(channelAvailability, "channelAvailability");
|
|
1042
|
-
async function preferencesPayload(deps, userId) {
|
|
1080
|
+
async function preferencesPayload(deps, userId, scopeClientId) {
|
|
1043
1081
|
const [preferences, availability] = await Promise.all([
|
|
1044
1082
|
deps.preferences.get(userId),
|
|
1045
|
-
channelAvailability(deps, userId)
|
|
1083
|
+
channelAvailability(deps, userId, scopeClientId)
|
|
1046
1084
|
]);
|
|
1047
1085
|
return { preferences, availability, categories: [...deps.categories] };
|
|
1048
1086
|
}
|
|
@@ -1056,7 +1094,8 @@ function inboxRoutes(deps) {
|
|
|
1056
1094
|
async ({ actor, query, locale }) => ok(
|
|
1057
1095
|
await deps.inbox.list(
|
|
1058
1096
|
actor.userId,
|
|
1059
|
-
parseListQuery(query, messagesOf(deps, locale))
|
|
1097
|
+
parseListQuery(query, messagesOf(deps, locale)),
|
|
1098
|
+
actor.scopeClientId
|
|
1060
1099
|
)
|
|
1061
1100
|
)
|
|
1062
1101
|
)
|
|
@@ -1067,7 +1106,7 @@ function inboxRoutes(deps) {
|
|
|
1067
1106
|
handle: guarded(
|
|
1068
1107
|
async ({ actor }) => (
|
|
1069
1108
|
// Polled by the SPAs, so it stays a single indexed COUNT.
|
|
1070
|
-
ok({ count: await deps.inbox.unreadCount(actor.userId) })
|
|
1109
|
+
ok({ count: await deps.inbox.unreadCount(actor.userId, actor.scopeClientId) })
|
|
1071
1110
|
)
|
|
1072
1111
|
)
|
|
1073
1112
|
},
|
|
@@ -1076,7 +1115,7 @@ function inboxRoutes(deps) {
|
|
|
1076
1115
|
path: "/notifications/mark-read",
|
|
1077
1116
|
handle: guarded(async ({ actor, body, locale }) => {
|
|
1078
1117
|
const target = parseMarkReadBody(body, messagesOf(deps, locale));
|
|
1079
|
-
const updated = "all" in target ? await deps.inbox.markAllRead(actor.userId) : await deps.inbox.markRead(actor.userId, target.ids);
|
|
1118
|
+
const updated = "all" in target ? await deps.inbox.markAllRead(actor.userId, actor.scopeClientId) : await deps.inbox.markRead(actor.userId, target.ids);
|
|
1080
1119
|
if (updated > 0) deps.onInboxChanged?.(actor.userId);
|
|
1081
1120
|
return ok({ updated });
|
|
1082
1121
|
})
|
|
@@ -1102,7 +1141,9 @@ function preferenceRoutes(deps) {
|
|
|
1102
1141
|
{
|
|
1103
1142
|
method: "GET",
|
|
1104
1143
|
path: "/notification-preferences",
|
|
1105
|
-
handle: guarded(
|
|
1144
|
+
handle: guarded(
|
|
1145
|
+
async ({ actor }) => ok(await preferencesPayload(deps, actor.userId, actor.scopeClientId ?? null))
|
|
1146
|
+
)
|
|
1106
1147
|
},
|
|
1107
1148
|
{
|
|
1108
1149
|
method: "PUT",
|
|
@@ -1112,7 +1153,7 @@ function preferenceRoutes(deps) {
|
|
|
1112
1153
|
actor.userId,
|
|
1113
1154
|
parsePreferencesBody(body, messagesOf(deps, locale))
|
|
1114
1155
|
);
|
|
1115
|
-
return ok(await preferencesPayload(deps, actor.userId));
|
|
1156
|
+
return ok(await preferencesPayload(deps, actor.userId, actor.scopeClientId ?? null));
|
|
1116
1157
|
})
|
|
1117
1158
|
}
|
|
1118
1159
|
];
|
|
@@ -1143,6 +1184,9 @@ function pushRoutes(deps) {
|
|
|
1143
1184
|
const input = parsePushSubscriptionBody(body, messagesOf(deps, locale));
|
|
1144
1185
|
const userAgent = headers?.["user-agent"];
|
|
1145
1186
|
await deps.pushSubscriptions.save(actor.userId, {
|
|
1187
|
+
// The HOST's resolved origin, never the body's — a caller cannot
|
|
1188
|
+
// choose which store's app their browser counts as.
|
|
1189
|
+
clientId: actor.scopeClientId ?? null,
|
|
1146
1190
|
...input,
|
|
1147
1191
|
...userAgent ? { userAgent } : {}
|
|
1148
1192
|
});
|
|
@@ -1249,7 +1293,11 @@ function createNotificationRouter(deps) {
|
|
|
1249
1293
|
drainPending: /* @__PURE__ */ __name((olderThanMs = DEFAULT_SWEEP_CUTOFF_MS, take = DEFAULT_SWEEP_TAKE) => drainPending(deps, olderThanMs, take), "drainPending"),
|
|
1250
1294
|
async notify(event, options = {}) {
|
|
1251
1295
|
const generator = deps.generators.resolve(event.type);
|
|
1252
|
-
const recipient = await loadRecipient(
|
|
1296
|
+
const recipient = await loadRecipient(
|
|
1297
|
+
deps,
|
|
1298
|
+
event.recipient.userId,
|
|
1299
|
+
event.recipient.clientId ?? null
|
|
1300
|
+
);
|
|
1253
1301
|
if (!recipient) throw new UnknownNotificationRecipientError(event.recipient.userId);
|
|
1254
1302
|
const content = generator.generate(event.payload, { locale: recipient.locale });
|
|
1255
1303
|
const channels = await resolveChannels(deps, event, generator, recipient);
|
|
@@ -1370,4 +1418,4 @@ export {
|
|
|
1370
1418
|
createTransportRegistry,
|
|
1371
1419
|
createApiNotifications
|
|
1372
1420
|
};
|
|
1373
|
-
//# sourceMappingURL=chunk-
|
|
1421
|
+
//# sourceMappingURL=chunk-53SH5ABN.js.map
|