@12-apps/notifications 4.12.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{chunk-WZBX7YCE.js → chunk-53SH5ABN.js} +82 -34
  2. package/dist/chunk-53SH5ABN.js.map +1 -0
  3. package/dist/{chunk-PNY6S6WH.js → chunk-643HNPQJ.js} +4 -4
  4. package/dist/chunk-643HNPQJ.js.map +1 -0
  5. package/dist/{chunk-2IAHFIXS.js → chunk-AN6IX4AE.js} +4 -3
  6. package/dist/chunk-AN6IX4AE.js.map +1 -0
  7. package/dist/{chunk-CPQKKLPS.js → chunk-CSMFJJXY.js} +1 -1
  8. package/dist/{chunk-CPQKKLPS.js.map → chunk-CSMFJJXY.js.map} +1 -1
  9. package/dist/{chunk-FBBPS2LT.js → chunk-QPO6NSRR.js} +2 -2
  10. package/dist/{chunk-GK6GSC2J.js → chunk-XFODKRRB.js} +2 -2
  11. package/dist/{create-api-notifications-BTudlaSC.d.ts → create-api-notifications-cIEbFqoZ.d.ts} +121 -30
  12. package/dist/{create-web-notifications-2xxbKnrW.d.ts → create-web-notifications-32Myo-ww.d.ts} +31 -9
  13. package/dist/{generators-CQYdJfB5.d.ts → generators-GUF-Kml-.d.ts} +1 -1
  14. package/dist/hono/index.d.ts +5 -5
  15. package/dist/hono/index.js +3 -3
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +2 -2
  18. package/dist/{jobs-CaovU4GM.d.ts → jobs-wKoOz91Q.d.ts} +1 -1
  19. package/dist/manifest/server.d.ts +6 -6
  20. package/dist/manifest/server.js +4 -4
  21. package/dist/manifest/web.d.ts +3 -3
  22. package/dist/manifest/web.js +2 -2
  23. package/dist/{panel-MKI4PTNZ.js → panel-JL5RDEHY.js} +2 -2
  24. package/dist/{preferences-screen-S3ZHX5LB.js → preferences-screen-YRPNUZS5.js} +2 -2
  25. package/dist/react/index.d.ts +4 -4
  26. package/dist/react/index.js +2 -2
  27. package/dist/server/index.d.ts +7 -7
  28. package/dist/server/index.js +4 -4
  29. package/dist/{types-H_aFzLA0.d.ts → types-DPiePHJD.d.ts} +19 -1
  30. package/dist/web-push/index.d.ts +2 -2
  31. package/dist/{web-push-C6U-5JCV.d.ts → web-push-DUn_d_gj.d.ts} +12 -2
  32. package/dist/{wire-Bn6aA2nL.d.ts → wire-CJka1AvM.d.ts} +1 -1
  33. package/package.json +2 -2
  34. package/prisma/migrations/20260914210000_scope_push_subscriptions_per_store/migration.sql +31 -0
  35. package/prisma/notifications.prisma +12 -0
  36. package/src/react/create-web-notifications.tsx +12 -2
  37. package/src/react/hooks.ts +24 -4
  38. package/src/react/index.ts +1 -0
  39. package/src/server/context.ts +15 -0
  40. package/src/server/db.ts +50 -8
  41. package/src/server/dispatch.ts +9 -2
  42. package/src/server/inbox.ts +87 -18
  43. package/src/server/index.ts +2 -0
  44. package/src/server/push-subscriptions.ts +61 -7
  45. package/src/server/router.ts +5 -1
  46. package/src/server/routes.ts +14 -5
  47. package/src/server/transports/web-push.ts +15 -2
  48. package/src/types.ts +19 -1
  49. package/dist/chunk-2IAHFIXS.js.map +0 -1
  50. package/dist/chunk-PNY6S6WH.js.map +0 -1
  51. package/dist/chunk-WZBX7YCE.js.map +0 -1
  52. /package/dist/{chunk-FBBPS2LT.js.map → chunk-QPO6NSRR.js.map} +0 -0
  53. /package/dist/{chunk-GK6GSC2J.js.map → chunk-XFODKRRB.js.map} +0 -0
  54. /package/dist/{panel-MKI4PTNZ.js.map → panel-JL5RDEHY.js.map} +0 -0
  55. /package/dist/{preferences-screen-S3ZHX5LB.js.map → preferences-screen-YRPNUZS5.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/inbox-state.ts","../src/react/hooks.ts","../src/react/bell-icon.tsx"],"sourcesContent":["import type { InboxNotification } from '../wire';\n\nimport type { NotificationsApiClient } from './api';\n\n/**\n * The inbox's client state, as ONE store shared by the bell and the panel.\n *\n * They have to share it: marking a row read in the panel must move the badge in\n * the same tick, and an arrival must add a row to the list AND to the count.\n * the origin got that for free from a react-query cache the host had already\n * mounted; a published package cannot assume one — a query client is a host\n * decision, and requiring a particular one (or a particular version of one) is\n * the kind of dependency that keeps a package out of a host that made the other\n * choice. So the sharing is explicit and dependency-free: one subscribable\n * store, read through `useSyncExternalStore`.\n *\n * Optimistic on every write, with invalidate-on-error: the badge and the list\n * update instantly, and a failed write refetches the server truth rather than\n * leaving the screen asserting something the database does not say.\n */\n\nexport const PAGE_SIZE = 20;\n\n/** The badge's poll while nothing is pushing to us. */\nexport const BADGE_POLL_MS = 60_000;\n\n/**\n * The badge's interval while a realtime connection is live.\n *\n * Five minutes, not \"never\": this is the reconcile that catches an event the bus\n * dropped, and it costs one COUNT per open tab per five minutes. Deliberately\n * far slower than an operational screen's — a bell badge is ambient, and the\n * arrival that matters is pushed within milliseconds anyway. The poll does NOT\n * stop, which is the standing contract: a dropped event must cost latency and\n * never correctness.\n */\nexport const BADGE_RECONCILE_MS = 300_000;\n\nexport type InboxListStatus = 'idle' | 'pending' | 'ready' | 'error';\n\nexport interface InboxState {\n unread: number;\n items: InboxNotification[];\n status: InboxListStatus;\n /** A cursor means there is another page. */\n nextCursor: string | null;\n loadingMore: boolean;\n}\n\nexport interface InboxStore {\n getState(): InboxState;\n subscribe(listener: () => void): () => void;\n /** Load the first page (idempotent while one is in flight). */\n open(): void;\n /** Refetch the badge count. */\n refreshBadge(): void;\n /** Refetch both — what a realtime hint or a failed write triggers. */\n invalidate(): void;\n loadMore(): void;\n markRead(ids: readonly string[]): void;\n markAllRead(): void;\n remove(id: string): void;\n}\n\nconst EMPTY: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/** The mutable cell the functions below share, so each one stays small. */\ninterface Cell {\n state: InboxState;\n listeners: Set<() => void>;\n /** Fences a stale reload: a newer one must always win. */\n request: number;\n}\n\nfunction patch(cell: Cell, next: Partial<InboxState>): void {\n cell.state = { ...cell.state, ...next };\n for (const listener of cell.listeners) listener();\n}\n\n/** Refetch the badge count. The number is always one the server just gave us. */\nfunction refreshBadge(cell: Cell, api: NotificationsApiClient): void {\n void api\n .unreadCount()\n .then((unread) => patch(cell, { unread }))\n .catch(() => undefined);\n}\n\n/**\n * Reload page one, discarding whatever the optimistic path had produced.\n * `request` fences it: a reload that started before a newer one must not land\n * after it and reinstate stale rows.\n */\nfunction reloadList(cell: Cell, api: NotificationsApiClient): void {\n const token = (cell.request += 1);\n patch(cell, { status: cell.state.items.length > 0 ? cell.state.status : 'pending' });\n void api\n .listNotifications({ limit: PAGE_SIZE })\n .then((page) => {\n if (token !== cell.request) return;\n patch(cell, { items: page.items, nextCursor: page.nextCursor, status: 'ready' });\n })\n .catch(() => {\n if (token !== cell.request) return;\n patch(cell, { status: 'error' });\n });\n}\n\nfunction invalidate(cell: Cell, api: NotificationsApiClient): void {\n refreshBadge(cell, api);\n if (cell.state.status !== 'idle') reloadList(cell, api);\n}\n\n/** Apply an optimistic edit; on failure, take the server's word instead. */\nfunction write(\n cell: Cell,\n api: NotificationsApiClient,\n apply: () => void,\n send: () => Promise<{ ok: boolean }>,\n): void {\n apply();\n void send()\n .then((result) => {\n if (!result.ok) invalidate(cell, api);\n })\n .catch(() => invalidate(cell, api));\n}\n\nfunction bumpUnread(cell: Cell, delta: number): void {\n patch(cell, { unread: Math.max(0, cell.state.unread + delta) });\n}\n\nfunction loadMore(cell: Cell, api: NotificationsApiClient): void {\n const cursor = cell.state.nextCursor;\n if (!cursor || cell.state.loadingMore) return;\n patch(cell, { loadingMore: true });\n void api\n .listNotifications({ cursor, limit: PAGE_SIZE })\n .then((page) => {\n patch(cell, {\n items: [...cell.state.items, ...page.items],\n nextCursor: page.nextCursor,\n loadingMore: false,\n });\n })\n .catch(() => patch(cell, { loadingMore: false }));\n}\n\nfunction markRead(cell: Cell, api: NotificationsApiClient, ids: readonly string[]): void {\n const readAt = new Date().toISOString();\n let flipped = 0;\n const items = cell.state.items.map((item) => {\n if (!ids.includes(item.id) || item.readAt !== null) return item;\n flipped += 1;\n return { ...item, readAt };\n });\n if (flipped === 0) return;\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n bumpUnread(cell, -flipped);\n },\n () => api.markRead(ids),\n );\n}\n\nfunction remove(cell: Cell, api: NotificationsApiClient, id: string): void {\n const target = cell.state.items.find((item) => item.id === id);\n if (!target) return;\n const items = cell.state.items.filter((item) => item.id !== id);\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n if (target.readAt === null) bumpUnread(cell, -1);\n },\n () => api.remove([id]),\n );\n}\n\nexport function createInboxStore(api: NotificationsApiClient): InboxStore {\n const cell: Cell = { state: EMPTY, listeners: new Set(), request: 0 };\n return {\n getState: () => cell.state,\n subscribe(listener) {\n cell.listeners.add(listener);\n return () => cell.listeners.delete(listener);\n },\n open() {\n if (cell.state.status === 'idle') reloadList(cell, api);\n },\n refreshBadge: () => refreshBadge(cell, api),\n invalidate: () => invalidate(cell, api),\n loadMore: () => loadMore(cell, api),\n markRead: (ids) => markRead(cell, api, ids),\n markAllRead() {\n const readAt = new Date().toISOString();\n const items = cell.state.items.map((item) => ({ ...item, readAt: item.readAt ?? readAt }));\n write(\n cell,\n api,\n () => patch(cell, { items, unread: 0 }),\n () => api.markAllRead(),\n );\n },\n remove: (id) => remove(cell, api, id),\n };\n}\n","import { useEffect, useSyncExternalStore } from 'react';\n\nimport {\n BADGE_POLL_MS,\n BADGE_RECONCILE_MS,\n type InboxState,\n type InboxStore,\n} from './inbox-state';\n\n/**\n * The two hooks the bell and the panel use, and the realtime seam between them.\n *\n * A host that has a message bus passes `subscribe`; one that has not passes\n * nothing and keeps the 60 s poll. The bell ships in this package and mounts in\n * whatever embeds it, so it must not require the host to have adopted anything.\n */\n\n/**\n * How the surface learns an inbox changed without asking.\n *\n * Called once per mounted bell with a callback that means only \"ask again\" — no\n * payload, so the number on screen is always one the server just gave us.\n * Returns its own teardown. A host wires this to whatever it already has.\n */\nexport type NotificationsSubscribe = (onHint: () => void) => () => void;\n\n/**\n * The same wiring, as a HOOK — for a host whose realtime connection lives in\n * React context rather than in a module.\n *\n * `subscribe` above is supplied at FACTORY time, which is module scope, and a\n * context-bound connection cannot be reached from there: the provider holding\n * it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a\n * `useUserTopics` hook, which is the common one) had no way to pass anything at\n * all, and the badge simply never heard an event.\n *\n * So this is the second door, and it is the one `@12-apps/app-shell` already\n * uses for the same problem — its consent dialog takes a `useSignal` hook for\n * exactly this reason. Two packages solving one problem two ways is how an\n * adopter ends up believing the feature is unavailable to it.\n *\n * Called during render, so it may use context and hooks freely. Pass one or\n * the other; passing both runs both, which is a host's business.\n */\nexport type NotificationsSignalHook = (onHint: () => void) => void;\n\nexport function useInboxState(store: InboxStore): InboxState {\n return useSyncExternalStore(store.subscribe, store.getState, store.getState);\n}\n\n/** What both badge hooks below take, and what the bell passes them. */\nexport interface BadgeSyncOptions {\n enabled?: boolean;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n /**\n * The host's own word that its realtime channel is up RIGHT NOW.\n *\n * `subscribe` relaxes the poll because a live subscription is implied by its\n * presence. `useSignal` cannot say the same — a hook handed a callback knows\n * nothing about whether its connection opened — and neither can a host that\n * wires the invalidate itself, outside this package, because its connection\n * is session-scoped and must stay behind `enabled` (which `useSignal` is\n * called in front of). Such a host had no way to relax the badge at all, and\n * every signed-in reader polled once a minute on top of a working stream.\n *\n * `true` relaxes the poll to {@link BADGE_RECONCILE_MS}, exactly as a\n * `subscribe` does; anything else keeps {@link BADGE_POLL_MS}. It is a STATUS,\n * so pass it live — a stream that drops takes the badge straight back to the\n * fast poll on the next render.\n */\n live?: boolean;\n}\n\n/**\n * What a disabled badge reads, instead of whatever the store happens to hold.\n *\n * A CONSTANT, so `useSyncExternalStore`'s identity comparison sees no change\n * across the renders of a signed-out session.\n */\nconst NOTHING_TO_SHOW: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/**\n * The badge's server state, kept fresh: pushed while a subscription is live,\n * polled otherwise.\n *\n * The whole state rather than the count, because every badge hook that layers\n * on top of it needs the poll and the subscription mounted exactly ONCE per\n * bell — read through two hooks, a bell that showed both a number and a tone\n * would open two of everything.\n *\n * ## `enabled` gates the ANSWER, not only the fetching\n *\n * It gates the poll and the subscription, which is the obvious half. It also\n * blanks the returned state, which is the half that was missing and matters\n * more: the store is per FACTORY and a host builds one at module scope for the\n * whole app, so signing out does not empty it — `refreshBadge` swallows the 401\n * and leaves the last number in place. Without this, a hook told there is\n * nobody signed in hands back the PREVIOUS reader's unread count and their\n * inbox rows.\n *\n * Deliberately here rather than at each caller. It was at each caller, three\n * times, in three shapes, and two of them were dead weight no test could reach\n * — which is what an invariant looks like just before one copy of it goes\n * missing.\n *\n * INTERNAL. Not exported from `./index`: it hands back rows as well as a count,\n * and a host wanting a number has `useUnreadCount` or the factory's\n * `useBellBadge`.\n */\nexport function useBadgeState(store: InboxStore, options: BadgeSyncOptions = {}): InboxState {\n const enabled = options.enabled ?? true;\n const subscribe = options.subscribe;\n // Pushed to by a subscription this package holds, or by one the host holds\n // and says is up — see `BadgeSyncOptions.live`.\n const relaxed = subscribe !== undefined || options.live === true;\n const live = useInboxState(store);\n const state = enabled ? live : NOTHING_TO_SHOW;\n\n // Called unconditionally — it is a hook, so it cannot sit behind `enabled`.\n // The host's own hook decides what to do when there is nothing to hear.\n options.useSignal?.(() => {\n if (enabled) store.invalidate();\n });\n\n useEffect(() => {\n if (!enabled) return;\n store.refreshBadge();\n const unsubscribe = subscribe?.(() => store.invalidate());\n // A live channel relaxes the poll to the reconcile interval; without one it\n // stays the 60 s poll.\n const interval = setInterval(\n () => store.refreshBadge(),\n relaxed ? BADGE_RECONCILE_MS : BADGE_POLL_MS,\n );\n const onFocus = (): void => store.refreshBadge();\n globalThis.addEventListener?.('focus', onFocus);\n return () => {\n clearInterval(interval);\n globalThis.removeEventListener?.('focus', onFocus);\n unsubscribe?.();\n };\n }, [store, enabled, subscribe, relaxed]);\n\n return state;\n}\n\n/**\n * The bell badge number, for a host with its own trigger chrome.\n *\n * A host that also publishes live activities wants `useBellBadge` from the\n * factory instead — this one counts inbox rows and knows nothing about what is\n * happening right now.\n */\nexport function useUnreadCount(store: InboxStore, options: BadgeSyncOptions = {}): number {\n return useBadgeState(store, options).unread;\n}\n\n/** The panel's list — only fetches while the panel is open. */\nexport function useInboxList(store: InboxStore, open: boolean): InboxState {\n const state = useInboxState(store);\n useEffect(() => {\n if (open) store.open();\n }, [store, open]);\n return state;\n}\n","/** Inline SVG bell (no icon-library dependency in this package). */\nimport type { JSX } from 'react';\n\nimport { Box } from '@12-apps/ui/mui/Box';\n\nexport function BellIcon({\n size = 28,\n dim = false,\n}: {\n size?: number;\n dim?: boolean;\n}): JSX.Element {\n return (\n <Box\n component=\"svg\"\n viewBox=\"0 0 24 24\"\n aria-hidden\n sx={{\n width: size,\n height: size,\n fill: 'none',\n stroke: 'currentColor',\n opacity: dim ? 0.4 : 1,\n }}\n strokeWidth={1.8}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\n <path d=\"M10 20a2 2 0 0 0 4 0\" />\n </Box>\n );\n}\n"],"mappings":"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","names":[]}
@@ -19,4 +19,4 @@ export {
19
19
  NOTIFICATION_CHANNELS,
20
20
  taxonomyOf
21
21
  };
22
- //# sourceMappingURL=chunk-CPQKKLPS.js.map
22
+ //# sourceMappingURL=chunk-CSMFJJXY.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Core types of the channel-agnostic notification system (12-15).\n *\n * Three decoupled layers, each open for extension without touching the others:\n * - GENERATORS map a typed domain event to agnostic content (title/body/…).\n * - The CHANNEL ROUTER always writes the notification-centre inbox record,\n * then fans out one delivery per enabled channel.\n * - TRANSPORTS format the agnostic content for one channel and send it.\n *\n * Nothing here knows about a concrete channel's wire format — that lives\n * entirely inside each transport adapter — and nothing here knows about a\n * concrete DOMAIN either: the event `type` set, the preference categories and\n * the channel list are all host config (see {@link NotificationTaxonomy}).\n */\n\n/** Transport channels a notification can fan out to (DB CHECK mirrors this). */\nexport const NOTIFICATION_CHANNELS = ['EMAIL', 'SMS', 'WHATSAPP', 'WEB_PUSH'] as const;\nexport type NotificationChannel = (typeof NOTIFICATION_CHANNELS)[number];\n\n/**\n * The preference categories are the HOST's, and required.\n *\n * There used to be a `NOTIFICATION_CATEGORIES = ['orders','payments','stock',\n * 'system']` here — one product's set — and `taxonomyOf` fell back to it\n * whenever a host passed none. The docstring argued the case itself: \"it is\n * product vocabulary, not machinery\", and then shipped the vocabulary anyway as\n * the default, which is the only part a forgetful host would ever see.\n *\n * The consequence was quiet rather than loud: the settings screen renders four\n * rows a foreign host never chose, its own categories are absent, and every\n * preference a user sets is filed against a taxonomy nothing else in that\n * system uses. Nothing throws, because `category` is deliberately a free string\n * — the packaged migration puts **no CHECK** on it, precisely because a closed\n * set would be wrong for every host but the first. That freedom is what made\n * the default undetectable.\n *\n * `channel` and `status` are different and keep their CHECKs: those ARE this\n * library's own closed sets.\n */\nexport type NotificationCategory = string;\n\n/**\n * Per-channel delivery lifecycle (DB CHECK mirrors this).\n *\n * `SENDING` is the CLAIM: exactly one dispatcher moves a row out of `QUEUED`,\n * so two dispatchers can never both send the same delivery. A row left\n * `SENDING` is a dispatcher that died mid-send, and the sweep reclaims it once\n * it is older than the cutoff.\n *\n * `DEAD` is terminal: the attempt ceiling was reached (or the recipient no\n * longer exists), and no sweep will pick the row up again. Without it a\n * permanently invalid destination is a billed provider call on every sweep,\n * forever, and the sweep's working set only grows.\n */\nexport type DeliveryStatus = 'QUEUED' | 'SENDING' | 'SENT' | 'FAILED' | 'DEAD';\n\n/**\n * Channel-agnostic content a generator produces. This is what the inbox stores\n * verbatim and what every transport's formatter receives — no channel may leak\n * its wire format into it.\n */\nexport interface NotificationContent {\n title: string;\n body: string;\n /** In-app deep link (a same-origin path such as `/orders/123`). */\n link?: string;\n /**\n * Structured extras for consumers that want more than text.\n *\n * ONE KEY IS RESERVED: `liveSubject` (`LIVE_SUBJECT_KEY` in `./live`) ties\n * this notification to a live activity, and the WEB_PUSH transport turns it\n * into the tray `tag` that collapses every push about one subject onto a\n * single entry. A host already using that name for something else acquires\n * that behaviour without asking for it — so it is named here, on the field a\n * generator actually writes, and not only where the feature is documented.\n */\n data?: Record<string, unknown>;\n}\n\n/** Who receives a notification. `clientId` scopes it to a tenant when set. */\nexport interface NotificationRecipient {\n userId: string;\n clientId?: string;\n}\n\n/**\n * A typed domain event handed to `notify`. `type` selects the registered\n * generator; `payload` is that generator's typed input. Callers never touch\n * channels, formatting, or preferences.\n */\nexport interface NotificationEvent<TPayload = unknown> {\n type: string;\n recipient: NotificationRecipient;\n payload: TPayload;\n}\n\n/**\n * Maps one domain event type to agnostic content. Registered through the\n * server config (or `registerGenerator` for a late arrival); adding a\n * generator never touches existing generators, the router, or any transport\n * (open/closed).\n */\n/**\n * Who the content is being rendered FOR — the reader, at the moment the\n * generator is asked.\n *\n * A notification is stored as rendered TEXT: title and body are columns, so\n * the language is chosen once, when the row is written, and never again. That\n * makes this the only honest place to ask. A generator is registered at BOOT —\n * a host that resolved its words there would pin every future reader to\n * whichever language the process happened to start in, invisibly, because a\n * single-locale host cannot tell the difference.\n *\n * The tag is the RECIPIENT's, never the request's. The person who triggers a\n * notification is routinely not the person who reads it: an invite is sent\n * because an administrator acted and is read by the invitee. Reading\n * `Accept-Language` here would be a bug that only ever surfaces as somebody\n * being told things in a language they do not speak.\n *\n * Absent means \"nobody said\" — a host with one audience, or one that stores no\n * per-user language, populates nothing and every generator answers with its\n * own default exactly as it did before this existed.\n */\nexport interface NotificationGenerateContext {\n readonly locale?: string | null;\n}\n\nexport interface NotificationGenerator<TPayload = unknown> {\n /** The event key, dot-namespaced (\"order.paid\"). One generator per type. */\n type: string;\n /** The preference category the router gates this type's fan-out on. */\n category: NotificationCategory;\n /**\n * The channels this type may EVER use — AVAILABILITY, not preference.\n *\n * Absent (the default) means every channel, which is what every generator\n * written before this field keeps doing. A declared list is a hard cap the\n * router applies AFTER preferences: a user whose stored row explicitly\n * enables a channel this type does not offer still does not get it, because\n * the channel was never on offer for this message. That is the difference\n * from {@link channelDefaults} — a default is a starting point a user can\n * move, availability is the set of starting points that exist.\n *\n * It is per TYPE because the category is too coarse to say it: the three\n * comanda kitchen messages sit in `orders` next to `order.paid`, and a diner\n * three metres from the food wants a push, not correspondence, while the\n * buyer of a delivery order still wants the e-mail. Declaring `[\"WEB_PUSH\"]`\n * on the mesa messages says that without splitting the category or taking\n * `order.paid`'s e-mail away with it.\n *\n * Unknown entries are ignored and order is irrelevant — the list is coerced\n * onto {@link NOTIFICATION_CHANNELS}. An EMPTY list is legal and means no\n * transport channel at all; the inbox record is written regardless, because\n * the inbox is not a channel a user opts out of.\n */\n channels?: readonly NotificationChannel[];\n /**\n * This type's starting toggles, overriding the category's defaults for the\n * channels it names and only where the user has made NO explicit choice.\n *\n * Stored preferences still win over it — that is what makes it a default.\n * To take a channel away from a user who asked for it, declare\n * {@link channels} instead.\n */\n channelDefaults?: Partial<Record<NotificationChannel, boolean>>;\n /**\n * Render this event's content for ONE recipient.\n *\n * `context` is OPTIONAL, and that is what keeps every generator written\n * before it working: a one-parameter `generate` is assignable to this\n * signature unchanged. A host that passes nothing is stating a fact — it has\n * no language for this reader — rather than asserting a default.\n */\n generate: (payload: TPayload, context?: NotificationGenerateContext) => NotificationContent;\n}\n\n/**\n * The recipient as a transport sees them: resolved destinations only. Built by\n * the router from the host's contact directory + the push subscriptions this\n * package owns; transports use it to answer\n * {@link NotificationTransport.supports}.\n */\nexport interface TransportRecipient {\n userId: string;\n email: string | null;\n /**\n * The recipient's own language, when the host's contact directory states\n * one. Absent means \"nobody said\" — see {@link NotificationGenerateContext}.\n */\n locale?: string | null;\n /** Phone as the host stores it (transports normalize per provider rules). */\n phone: string | null;\n /** How many active browser push subscriptions the user holds. */\n pushSubscriptionCount: number;\n}\n\n/**\n * One pluggable channel adapter: a FORMATTER (agnostic content → channel\n * message) plus a SENDER. Adding a channel = registering one of these; the\n * router dispatches through the registry and needs no change.\n *\n * `send` resolves on success and THROWS on failure — the router records the\n * error on the delivery row and isolates it from other channels. Sends must be\n * retry-safe: the router may re-dispatch a QUEUED/FAILED delivery.\n */\nexport interface NotificationTransport<TMessage = unknown> {\n channel: NotificationChannel;\n /**\n * Whether this recipient is addressable on this channel right now — the\n * destination exists (e-mail / phone / push subscription) AND the provider\n * is configured. `false` simply skips the channel (no delivery row).\n */\n supports(recipient: TransportRecipient): boolean;\n /** Transform the agnostic content into this channel's message shape. */\n format(content: NotificationContent): TMessage;\n /** Deliver the formatted message to the recipient. Throws on failure. */\n send(message: TMessage, recipient: TransportRecipient): Promise<void>;\n}\n\n/**\n * The host's product vocabulary. Everything below the surface (routing,\n * delivery rows, retries, the wire) is identical for every host; WHICH\n * categories exist and how they are labelled is not.\n */\nexport interface NotificationTaxonomy {\n /** The preference categories, in the order the settings screen lists them. */\n categories: readonly NotificationCategory[];\n}\n\n/**\n * The taxonomy in force. `categories` is REQUIRED — see above.\n *\n * The empty check was already here and stays: an empty list and a missing one\n * are the same mistake, and both now fail at assembly rather than rendering an\n * empty settings screen or somebody else's four rows.\n */\nexport function taxonomyOf(config: {\n categories: readonly NotificationCategory[];\n}): NotificationTaxonomy {\n const categories = config.categories;\n if (!categories || categories.length === 0) {\n throw new Error(\n '@12-apps/notifications: `categories` is required and must not be empty — ' +\n 'the preference categories are the host\\'s product vocabulary.',\n );\n }\n return { categories: [...categories] };\n}\n\n/** The host's logger. Defaults to the console (the @12-apps/jobs precedent). */\nexport interface NotificationLogger {\n info(message: string, ...meta: unknown[]): void;\n error(message: string, ...meta: unknown[]): void;\n}\n"],"mappings":";;;;;AAgBO,IAAM,wBAAwB,CAAC,SAAS,OAAO,YAAY,UAAU;AA4NrE,SAAS,WAAW,QAEF;AACvB,QAAM,aAAa,OAAO;AAC1B,MAAI,CAAC,cAAc,WAAW,WAAW,GAAG;AAC1C,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO,EAAE,YAAY,CAAC,GAAG,UAAU,EAAE;AACvC;AAXgB;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Core types of the channel-agnostic notification system (12-15).\n *\n * Three decoupled layers, each open for extension without touching the others:\n * - GENERATORS map a typed domain event to agnostic content (title/body/…).\n * - The CHANNEL ROUTER always writes the notification-centre inbox record,\n * then fans out one delivery per enabled channel.\n * - TRANSPORTS format the agnostic content for one channel and send it.\n *\n * Nothing here knows about a concrete channel's wire format — that lives\n * entirely inside each transport adapter — and nothing here knows about a\n * concrete DOMAIN either: the event `type` set, the preference categories and\n * the channel list are all host config (see {@link NotificationTaxonomy}).\n */\n\n/** Transport channels a notification can fan out to (DB CHECK mirrors this). */\nexport const NOTIFICATION_CHANNELS = ['EMAIL', 'SMS', 'WHATSAPP', 'WEB_PUSH'] as const;\nexport type NotificationChannel = (typeof NOTIFICATION_CHANNELS)[number];\n\n/**\n * The preference categories are the HOST's, and required.\n *\n * There used to be a `NOTIFICATION_CATEGORIES = ['orders','payments','stock',\n * 'system']` here — one product's set — and `taxonomyOf` fell back to it\n * whenever a host passed none. The docstring argued the case itself: \"it is\n * product vocabulary, not machinery\", and then shipped the vocabulary anyway as\n * the default, which is the only part a forgetful host would ever see.\n *\n * The consequence was quiet rather than loud: the settings screen renders four\n * rows a foreign host never chose, its own categories are absent, and every\n * preference a user sets is filed against a taxonomy nothing else in that\n * system uses. Nothing throws, because `category` is deliberately a free string\n * — the packaged migration puts **no CHECK** on it, precisely because a closed\n * set would be wrong for every host but the first. That freedom is what made\n * the default undetectable.\n *\n * `channel` and `status` are different and keep their CHECKs: those ARE this\n * library's own closed sets.\n */\nexport type NotificationCategory = string;\n\n/**\n * Per-channel delivery lifecycle (DB CHECK mirrors this).\n *\n * `SENDING` is the CLAIM: exactly one dispatcher moves a row out of `QUEUED`,\n * so two dispatchers can never both send the same delivery. A row left\n * `SENDING` is a dispatcher that died mid-send, and the sweep reclaims it once\n * it is older than the cutoff.\n *\n * `DEAD` is terminal: the attempt ceiling was reached (or the recipient no\n * longer exists), and no sweep will pick the row up again. Without it a\n * permanently invalid destination is a billed provider call on every sweep,\n * forever, and the sweep's working set only grows.\n */\nexport type DeliveryStatus = 'QUEUED' | 'SENDING' | 'SENT' | 'FAILED' | 'DEAD';\n\n/**\n * Channel-agnostic content a generator produces. This is what the inbox stores\n * verbatim and what every transport's formatter receives — no channel may leak\n * its wire format into it.\n */\nexport interface NotificationContent {\n title: string;\n body: string;\n /** In-app deep link (a same-origin path such as `/orders/123`). */\n link?: string;\n /**\n * Structured extras for consumers that want more than text.\n *\n * ONE KEY IS RESERVED: `liveSubject` (`LIVE_SUBJECT_KEY` in `./live`) ties\n * this notification to a live activity, and the WEB_PUSH transport turns it\n * into the tray `tag` that collapses every push about one subject onto a\n * single entry. A host already using that name for something else acquires\n * that behaviour without asking for it — so it is named here, on the field a\n * generator actually writes, and not only where the feature is documented.\n */\n data?: Record<string, unknown>;\n}\n\n/** Who receives a notification. `clientId` scopes it to a tenant when set. */\nexport interface NotificationRecipient {\n userId: string;\n clientId?: string;\n}\n\n/**\n * A typed domain event handed to `notify`. `type` selects the registered\n * generator; `payload` is that generator's typed input. Callers never touch\n * channels, formatting, or preferences.\n */\nexport interface NotificationEvent<TPayload = unknown> {\n type: string;\n recipient: NotificationRecipient;\n payload: TPayload;\n}\n\n/**\n * Maps one domain event type to agnostic content. Registered through the\n * server config (or `registerGenerator` for a late arrival); adding a\n * generator never touches existing generators, the router, or any transport\n * (open/closed).\n */\n/**\n * Who the content is being rendered FOR — the reader, at the moment the\n * generator is asked.\n *\n * A notification is stored as rendered TEXT: title and body are columns, so\n * the language is chosen once, when the row is written, and never again. That\n * makes this the only honest place to ask. A generator is registered at BOOT —\n * a host that resolved its words there would pin every future reader to\n * whichever language the process happened to start in, invisibly, because a\n * single-locale host cannot tell the difference.\n *\n * The tag is the RECIPIENT's, never the request's. The person who triggers a\n * notification is routinely not the person who reads it: an invite is sent\n * because an administrator acted and is read by the invitee. Reading\n * `Accept-Language` here would be a bug that only ever surfaces as somebody\n * being told things in a language they do not speak.\n *\n * Absent means \"nobody said\" — a host with one audience, or one that stores no\n * per-user language, populates nothing and every generator answers with its\n * own default exactly as it did before this existed.\n */\nexport interface NotificationGenerateContext {\n readonly locale?: string | null;\n}\n\nexport interface NotificationGenerator<TPayload = unknown> {\n /** The event key, dot-namespaced (\"order.paid\"). One generator per type. */\n type: string;\n /** The preference category the router gates this type's fan-out on. */\n category: NotificationCategory;\n /**\n * The channels this type may EVER use — AVAILABILITY, not preference.\n *\n * Absent (the default) means every channel, which is what every generator\n * written before this field keeps doing. A declared list is a hard cap the\n * router applies AFTER preferences: a user whose stored row explicitly\n * enables a channel this type does not offer still does not get it, because\n * the channel was never on offer for this message. That is the difference\n * from {@link channelDefaults} — a default is a starting point a user can\n * move, availability is the set of starting points that exist.\n *\n * It is per TYPE because the category is too coarse to say it: the three\n * comanda kitchen messages sit in `orders` next to `order.paid`, and a diner\n * three metres from the food wants a push, not correspondence, while the\n * buyer of a delivery order still wants the e-mail. Declaring `[\"WEB_PUSH\"]`\n * on the mesa messages says that without splitting the category or taking\n * `order.paid`'s e-mail away with it.\n *\n * Unknown entries are ignored and order is irrelevant — the list is coerced\n * onto {@link NOTIFICATION_CHANNELS}. An EMPTY list is legal and means no\n * transport channel at all; the inbox record is written regardless, because\n * the inbox is not a channel a user opts out of.\n */\n channels?: readonly NotificationChannel[];\n /**\n * This type's starting toggles, overriding the category's defaults for the\n * channels it names and only where the user has made NO explicit choice.\n *\n * Stored preferences still win over it — that is what makes it a default.\n * To take a channel away from a user who asked for it, declare\n * {@link channels} instead.\n */\n channelDefaults?: Partial<Record<NotificationChannel, boolean>>;\n /**\n * Render this event's content for ONE recipient.\n *\n * `context` is OPTIONAL, and that is what keeps every generator written\n * before it working: a one-parameter `generate` is assignable to this\n * signature unchanged. A host that passes nothing is stating a fact — it has\n * no language for this reader — rather than asserting a default.\n */\n generate: (payload: TPayload, context?: NotificationGenerateContext) => NotificationContent;\n}\n\n/**\n * The recipient as a transport sees them: resolved destinations only. Built by\n * the router from the host's contact directory + the push subscriptions this\n * package owns; transports use it to answer\n * {@link NotificationTransport.supports}.\n */\nexport interface TransportRecipient {\n userId: string;\n email: string | null;\n /**\n * The recipient's own language, when the host's contact directory states\n * one. Absent means \"nobody said\" — see {@link NotificationGenerateContext}.\n */\n locale?: string | null;\n /** Phone as the host stores it (transports normalize per provider rules). */\n phone: string | null;\n /**\n * How many push subscriptions this NOTIFICATION can actually reach — not how\n * many the user holds. Scoped by {@link TransportRecipient.clientId}.\n */\n pushSubscriptionCount: number;\n /**\n * The store this notification belongs to, or `null` for a platform-wide one\n *.\n *\n * REQUIRED, and deliberately so where optional is the tempting answer. An\n * optional field fails OPEN: a construction site that forgets it yields\n * `undefined`, which the push scope reads as \"platform notification -> every\n * subscription\" — precisely the cross-store fan-out this exists to stop.\n * Three construction sites is the argument FOR requiring it, not against; the\n * compiler then names them.\n *\n * Not a breaking change for adopters: every external use CONSUMES a recipient\n * through `supports`/`send` below, and the only constructors are in-package.\n */\n clientId: string | null;\n}\n\n/**\n * One pluggable channel adapter: a FORMATTER (agnostic content → channel\n * message) plus a SENDER. Adding a channel = registering one of these; the\n * router dispatches through the registry and needs no change.\n *\n * `send` resolves on success and THROWS on failure — the router records the\n * error on the delivery row and isolates it from other channels. Sends must be\n * retry-safe: the router may re-dispatch a QUEUED/FAILED delivery.\n */\nexport interface NotificationTransport<TMessage = unknown> {\n channel: NotificationChannel;\n /**\n * Whether this recipient is addressable on this channel right now — the\n * destination exists (e-mail / phone / push subscription) AND the provider\n * is configured. `false` simply skips the channel (no delivery row).\n */\n supports(recipient: TransportRecipient): boolean;\n /** Transform the agnostic content into this channel's message shape. */\n format(content: NotificationContent): TMessage;\n /** Deliver the formatted message to the recipient. Throws on failure. */\n send(message: TMessage, recipient: TransportRecipient): Promise<void>;\n}\n\n/**\n * The host's product vocabulary. Everything below the surface (routing,\n * delivery rows, retries, the wire) is identical for every host; WHICH\n * categories exist and how they are labelled is not.\n */\nexport interface NotificationTaxonomy {\n /** The preference categories, in the order the settings screen lists them. */\n categories: readonly NotificationCategory[];\n}\n\n/**\n * The taxonomy in force. `categories` is REQUIRED — see above.\n *\n * The empty check was already here and stays: an empty list and a missing one\n * are the same mistake, and both now fail at assembly rather than rendering an\n * empty settings screen or somebody else's four rows.\n */\nexport function taxonomyOf(config: {\n categories: readonly NotificationCategory[];\n}): NotificationTaxonomy {\n const categories = config.categories;\n if (!categories || categories.length === 0) {\n throw new Error(\n '@12-apps/notifications: `categories` is required and must not be empty — ' +\n 'the preference categories are the host\\'s product vocabulary.',\n );\n }\n return { categories: [...categories] };\n}\n\n/** The host's logger. Defaults to the console (the @12-apps/jobs precedent). */\nexport interface NotificationLogger {\n info(message: string, ...meta: unknown[]): void;\n error(message: string, ...meta: unknown[]): void;\n}\n"],"mappings":";;;;;AAgBO,IAAM,wBAAwB,CAAC,SAAS,OAAO,YAAY,UAAU;AA8OrE,SAAS,WAAW,QAEF;AACvB,QAAM,aAAa,OAAO;AAC1B,MAAI,CAAC,cAAc,WAAW,WAAW,GAAG;AAC1C,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO,EAAE,YAAY,CAAC,GAAG,UAAU,EAAE;AACvC;AAXgB;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  UnknownNotificationRecipientError,
3
3
  UnknownNotificationTypeError
4
- } from "./chunk-GK6GSC2J.js";
4
+ } from "./chunk-XFODKRRB.js";
5
5
  import {
6
6
  __name
7
7
  } from "./chunk-7QVYU63E.js";
@@ -91,4 +91,4 @@ export {
91
91
  NOTIFICATIONS_DRAIN_LEASE_MS,
92
92
  NOTIFICATIONS_JOBS
93
93
  };
94
- //# sourceMappingURL=chunk-FBBPS2LT.js.map
94
+ //# sourceMappingURL=chunk-QPO6NSRR.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NOTIFICATION_CHANNELS
3
- } from "./chunk-CPQKKLPS.js";
3
+ } from "./chunk-CSMFJJXY.js";
4
4
  import {
5
5
  __name
6
6
  } from "./chunk-7QVYU63E.js";
@@ -166,4 +166,4 @@ export {
166
166
  normalizePhoneE164,
167
167
  inboxWire
168
168
  };
169
- //# sourceMappingURL=chunk-GK6GSC2J.js.map
169
+ //# sourceMappingURL=chunk-XFODKRRB.js.map
@@ -1,7 +1,7 @@
1
- import { N as NotificationGeneratorRegistry } from './generators-CQYdJfB5.js';
2
- import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, T as TypeChannelRules, L as ListNotificationsResult, k as NotificationsCopySource, c as NotificationWireMessages } from './wire-Bn6aA2nL.js';
3
- import { c as NotificationChannel, D as DeliveryStatus, g as NotificationLogger, j as NotificationTransport, d as NotificationContent, b as NotificationCategory, e as NotificationEvent, N as NotificationGenerator } from './types-H_aFzLA0.js';
4
- import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-C6U-5JCV.js';
1
+ import { N as NotificationGeneratorRegistry } from './generators-GUF-Kml-.js';
2
+ import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, T as TypeChannelRules, L as ListNotificationsResult, k as NotificationsCopySource, c as NotificationWireMessages } from './wire-CJka1AvM.js';
3
+ import { c as NotificationChannel, D as DeliveryStatus, g as NotificationLogger, j as NotificationTransport, d as NotificationContent, b as NotificationCategory, e as NotificationEvent, N as NotificationGenerator } from './types-DPiePHJD.js';
4
+ import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-DUn_d_gj.js';
5
5
  import { d as EmailChromeCopy, f as EmailTheme } from './template-Dlt5S54z.js';
6
6
 
7
7
  /**
@@ -51,25 +51,45 @@ interface NotificationPageAfter {
51
51
  createdAt: Date;
52
52
  id: string;
53
53
  }
54
- /** The inbox read filter. `deletedAt: null` is on every read, always. */
55
- interface NotificationWhere {
54
+ /**
55
+ * One BRANCH of an inbox filter — a member of `AND`/`OR`, never a whole read.
56
+ *
57
+ * Split out when the store scope arrived, and the split is what keeps `deletedAt: null`
58
+ * REQUIRED on the read itself (see {@link NotificationWhere}). Widening `OR`
59
+ * from the old fixed keyset tuple to a general array needed its members to be
60
+ * valid filters, and the tempting move — relaxing `deletedAt` on the one type —
61
+ * would have removed the only thing that makes a Prisma-backed host exclude
62
+ * soft-deleted rows. Both in-package doubles hard-code that filter regardless,
63
+ * so Prisma is precisely the implementation the requirement was protecting.
64
+ *
65
+ * `createdAt` and `id: { lt }` are here because the keyset boundary is built
66
+ * from them (`../inbox.ts`'s `pageWhere`); without them the branch type cannot
67
+ * express the very thing the widening exists to preserve.
68
+ */
69
+ interface NotificationWhereBranch {
56
70
  userId?: string;
57
71
  id?: string | {
58
72
  in: string[];
73
+ } | {
74
+ lt: string;
59
75
  };
60
- deletedAt: null;
76
+ createdAt?: Date | {
77
+ lt: Date;
78
+ };
79
+ deletedAt?: null;
61
80
  readAt?: null;
62
- /** The keyset half of `(createdAt, id) < (anchor.createdAt, anchor.id)`. */
63
- OR?: [{
64
- createdAt: {
65
- lt: Date;
66
- };
67
- }, {
68
- createdAt: Date;
69
- id: {
70
- lt: string;
71
- };
72
- }];
81
+ /** `null` matches the platform-wide rows that travel with every store scope. */
82
+ clientId?: string | null;
83
+ OR?: NotificationWhereBranch[];
84
+ AND?: NotificationWhereBranch[];
85
+ }
86
+ /**
87
+ * The inbox read filter. `deletedAt: null` is on every read, always — and it
88
+ * stays REQUIRED here, which is the whole reason {@link NotificationWhereBranch}
89
+ * is a separate type.
90
+ */
91
+ interface NotificationWhere extends NotificationWhereBranch {
92
+ deletedAt: null;
73
93
  }
74
94
  interface NotificationDelegate {
75
95
  create(args: {
@@ -227,13 +247,28 @@ interface PushSubscriptionRow {
227
247
  endpoint: string;
228
248
  p256dh: string;
229
249
  auth: string;
250
+ /** The origin this browser registered on; `null` = the platform origin. */
251
+ clientId: string | null;
230
252
  userAgent: string | null;
231
253
  }
254
+ /**
255
+ * Which of a user's subscriptions one send may reach.
256
+ *
257
+ * The disjunction is load-bearing and `clientId: { in: [x, null] }` is NOT a
258
+ * shortcut for it: SQL `IN` never matches NULL, so the platform-wide rows that
259
+ * must travel with every store scope would silently vanish.
260
+ */
261
+ interface PushSubscriptionWhere {
262
+ userId: string;
263
+ OR?: [{
264
+ clientId: null;
265
+ }, {
266
+ clientId: string;
267
+ }];
268
+ }
232
269
  interface PushSubscriptionDelegate {
233
270
  count(args: {
234
- where: {
235
- userId: string;
236
- };
271
+ where: PushSubscriptionWhere;
237
272
  }): Promise<number>;
238
273
  /**
239
274
  * The row holding one endpoint, whoever owns it. Read BEFORE an upsert so a
@@ -247,9 +282,7 @@ interface PushSubscriptionDelegate {
247
282
  };
248
283
  }): Promise<PushSubscriptionRow | null>;
249
284
  findMany(args: {
250
- where: {
251
- userId: string;
252
- };
285
+ where: PushSubscriptionWhere;
253
286
  }): Promise<PushSubscriptionRow[]>;
254
287
  upsert(args: {
255
288
  where: {
@@ -260,12 +293,14 @@ interface PushSubscriptionDelegate {
260
293
  endpoint: string;
261
294
  p256dh: string;
262
295
  auth: string;
296
+ clientId: string | null;
263
297
  userAgent: string | null;
264
298
  };
265
299
  update: {
266
300
  userId: string;
267
301
  p256dh: string;
268
302
  auth: string;
303
+ clientId: string | null;
269
304
  userAgent: string | null;
270
305
  };
271
306
  }): Promise<PushSubscriptionRow>;
@@ -338,6 +373,11 @@ interface PushSubscriptionInput {
338
373
  };
339
374
  /** Optional browser/device hint for a device list. */
340
375
  userAgent?: string;
376
+ /**
377
+ * Which ORIGIN this browser subscribed on — the HOST's resolved
378
+ * value, never anything the caller sent. Absent/null = the platform origin.
379
+ */
380
+ clientId?: string | null;
341
381
  }
342
382
  interface PushSubscriptionStore extends WebPushSubscriptionSource {
343
383
  /**
@@ -355,8 +395,19 @@ interface PushSubscriptionStore extends WebPushSubscriptionSource {
355
395
  save(userId: string, input: PushSubscriptionInput): Promise<void>;
356
396
  /** Remove one browser's subscription (owner-scoped; unknown = no-op). */
357
397
  remove(userId: string, endpoint: string): Promise<void>;
358
- /** How many devices the user has registered (settings UI hint). */
359
- count(userId: string): Promise<number>;
398
+ /**
399
+ * How many devices the user has registered.
400
+ *
401
+ * TWO callers with different questions. The settings screen asks unscoped —
402
+ * "you have 2 devices" is a fact about the PERSON — and passes nothing. The
403
+ * dispatch path passes the notification's tenant, because this is what
404
+ * `supports()` gates on: left unscoped there, the router would enqueue a
405
+ * WEB_PUSH delivery for a notification no reachable subscription exists for,
406
+ * `send` would find an empty list and throw, and the sweep would burn every
407
+ * attempt before writing DEAD — a FAILED row for something that was never
408
+ * undeliverable, only out of scope.
409
+ */
410
+ count(userId: string, notificationClientId?: string | null): Promise<number>;
360
411
  /**
361
412
  * Whether THIS endpoint is currently registered to THIS user.
362
413
  *
@@ -792,6 +843,21 @@ type NotifyByPermission = <TPayload>(clientId: string, permissions: readonly str
792
843
  */
793
844
  interface NotificationsActor {
794
845
  userId: string;
846
+ /**
847
+ * The store whose ORIGIN this request arrived on, resolved by the
848
+ * HOST from the hostname — never read from the body or the query.
849
+ *
850
+ * The paragraph above still holds: this is not a tenant the surface
851
+ * authorizes against, it is a NARROWING of a read that is already the
852
+ * caller's own, and it can never widen one. A host with a single origin never
853
+ * sets it and every endpoint answers exactly what it answered before.
854
+ *
855
+ * It exists because a multi-tenant host installs one storefront per store as
856
+ * its own PWA, and a PWA's identity is its ORIGIN — so an inbox answering a
857
+ * store's own app with a neighbour's notifications is one store reporting on
858
+ * another.
859
+ */
860
+ scopeClientId?: string;
795
861
  }
796
862
  /** One request, already authenticated and routed by the host. */
797
863
  interface NotificationsRequest {
@@ -844,6 +910,18 @@ declare function foldApiError(error: unknown): NotificationsResponse;
844
910
  * Soft-deleted rows (`deletedAt` set) are excluded from every read and can
845
911
  * never be resurrected by mark-read.
846
912
  */
913
+ /**
914
+ * The store whose ORIGIN the caller is reading from, or absent for the platform
915
+ * origin.
916
+ *
917
+ * A host that installs one storefront per store as its own PWA reads this from
918
+ * the request's hostname; a host with one origin never sets it and every read
919
+ * below is exactly what it was. Set, it narrows to that store's rows PLUS the
920
+ * platform-wide ones (`clientId IS NULL`) — a password reset or a security
921
+ * notice is about the person and not about a store, so hiding it inside the
922
+ * only app a customer opens would be a worse failure than the leak this fixes.
923
+ */
924
+ type NotificationScope = string | undefined;
847
925
  interface ListNotificationsInput {
848
926
  /** `unread` narrows to unread rows; default lists all non-deleted. */
849
927
  filter?: 'all' | 'unread';
@@ -859,10 +937,23 @@ interface ListNotificationsInput {
859
937
  limit?: number;
860
938
  }
861
939
  interface NotificationInboxStore {
862
- list(userId: string, input?: ListNotificationsInput): Promise<ListNotificationsResult>;
863
- unreadCount(userId: string): Promise<number>;
940
+ list(userId: string, input?: ListNotificationsInput, scope?: NotificationScope): Promise<ListNotificationsResult>;
941
+ /** Scoped with `list`, or the badge and the list it sits over disagree. */
942
+ unreadCount(userId: string, scope?: NotificationScope): Promise<number>;
864
943
  markRead(userId: string, ids: readonly string[]): Promise<number>;
865
- markAllRead(userId: string): Promise<number>;
944
+ /**
945
+ * Scoped too, and this one is a WRITE.
946
+ *
947
+ * Unscoped, "mark all as read" pressed inside store A's app clears store B's
948
+ * unread rows everywhere — a cross-store write from the app that exists to be
949
+ * isolated, and strictly worse than the read leak. `markRead(ids)` and
950
+ * `softDelete(ids)` need nothing: their ids come from the already-scoped list.
951
+ *
952
+ * The platform-wide rows ARE cleared from any origin, and that follows from
953
+ * the scope rule rather than contradicting it — those rows are one person's,
954
+ * not one store's.
955
+ */
956
+ markAllRead(userId: string, scope?: NotificationScope): Promise<number>;
866
957
  softDelete(userId: string, ids: readonly string[]): Promise<number>;
867
958
  }
868
959
 
@@ -977,4 +1068,4 @@ interface ApiNotifications {
977
1068
  }
978
1069
  declare function createApiNotifications(config: NotificationsServerConfig): ApiNotifications;
979
1070
 
980
- export { createApiNotifications as $, type ApiNotifications as A, type NotificationsDbProvider as B, type CommittedNotification as C, type NotificationsRequest as D, EMAIL_DRIVERS as E, type NotificationsResponse as F, type NotifyByPermission as G, type NotifyOptions as H, type NotifyResult as I, type PermissionNotificationSkip as J, type PushSubscriptionDelegate as K, type ListNotificationsInput as L, type PushSubscriptionInput as M, type NotificationsServerConfig as N, type PushSubscriptionRow as O, type PermissionNotificationResult as P, type PushSubscriptionStore as Q, type SmsDriver as R, SMS_DRIVERS as S, type SmsDriverDeclaration as T, type SmsMessage as U, type TransportDeclaration as V, type TransportRegistry as W, WHATSAPP_DRIVERS as X, type WhatsAppDriver as Y, type WhatsAppDriverDeclaration as Z, type WhatsAppMessage as _, type NotificationsActor as a, createTransportRegistry as a0, emailTransport as a1, foldApiError as a2, formatEmail as a3, formatSms as a4, formatWhatsApp as a5, ok as a6, smsTransport as a7, whatsAppTransport as a8, type NotificationsRoute as b, type EmailDriver as c, type EmailDriverDeclaration as d, type EmailMessage as e, type ExtraDrivers as f, type NotificationAudienceDirectory as g, type NotificationChannelPolicy as h, type NotificationCommittedListener as i, type NotificationContactDirectory as j, type NotificationCreateData as k, type NotificationDelegate as l, type NotificationDeliveryDelegate as m, type NotificationDeliveryRow as n, type NotificationDeliveryWhere as o, type NotificationDispatchScheduler as p, type NotificationInboxStore as q, type NotificationPageAfter as r, type NotificationPreferenceDelegate as s, type NotificationPreferenceRow as t, type NotificationPreferenceStore as u, type NotificationRouter as v, type NotificationWhere as w, NotificationsApiError as x, type NotificationsDb as y, type NotificationsDbClient as z };
1071
+ export { type WhatsAppDriverDeclaration as $, type ApiNotifications as A, type NotificationsDbClient as B, type CommittedNotification as C, type NotificationsDbProvider as D, EMAIL_DRIVERS as E, type NotificationsRequest as F, type NotificationsResponse as G, type NotifyByPermission as H, type NotifyOptions as I, type NotifyResult as J, type PermissionNotificationSkip as K, type ListNotificationsInput as L, type PushSubscriptionDelegate as M, type NotificationsServerConfig as N, type PushSubscriptionInput as O, type PermissionNotificationResult as P, type PushSubscriptionRow as Q, type PushSubscriptionStore as R, type PushSubscriptionWhere as S, SMS_DRIVERS as T, type SmsDriver as U, type SmsDriverDeclaration as V, type SmsMessage as W, type TransportDeclaration as X, type TransportRegistry as Y, WHATSAPP_DRIVERS as Z, type WhatsAppDriver as _, type NotificationsActor as a, type WhatsAppMessage as a0, createApiNotifications as a1, createTransportRegistry as a2, emailTransport as a3, foldApiError as a4, formatEmail as a5, formatSms as a6, formatWhatsApp as a7, ok as a8, smsTransport as a9, whatsAppTransport as aa, type NotificationsRoute as b, type EmailDriver as c, type EmailDriverDeclaration as d, type EmailMessage as e, type ExtraDrivers as f, type NotificationAudienceDirectory as g, type NotificationChannelPolicy as h, type NotificationCommittedListener as i, type NotificationContactDirectory as j, type NotificationCreateData as k, type NotificationDelegate as l, type NotificationDeliveryDelegate as m, type NotificationDeliveryRow as n, type NotificationDeliveryWhere as o, type NotificationDispatchScheduler as p, type NotificationInboxStore as q, type NotificationPageAfter as r, type NotificationPreferenceDelegate as s, type NotificationPreferenceRow as t, type NotificationPreferenceStore as u, type NotificationRouter as v, type NotificationWhere as w, type NotificationWhereBranch as x, NotificationsApiError as y, type NotificationsDb as z };
@@ -1,6 +1,6 @@
1
1
  import { ReactNode, JSX, ComponentType } from 'react';
2
- import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-Bn6aA2nL.js';
3
- import { c as NotificationChannel } from './types-H_aFzLA0.js';
2
+ import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-CJka1AvM.js';
3
+ import { c as NotificationChannel } from './types-DPiePHJD.js';
4
4
  import { b as LiveActivity } from './live-DYxEFO49.js';
5
5
 
6
6
  /**
@@ -201,6 +201,23 @@ interface BadgeSyncOptions {
201
201
  enabled?: boolean;
202
202
  subscribe?: NotificationsSubscribe;
203
203
  useSignal?: NotificationsSignalHook;
204
+ /**
205
+ * The host's own word that its realtime channel is up RIGHT NOW.
206
+ *
207
+ * `subscribe` relaxes the poll because a live subscription is implied by its
208
+ * presence. `useSignal` cannot say the same — a hook handed a callback knows
209
+ * nothing about whether its connection opened — and neither can a host that
210
+ * wires the invalidate itself, outside this package, because its connection
211
+ * is session-scoped and must stay behind `enabled` (which `useSignal` is
212
+ * called in front of). Such a host had no way to relax the badge at all, and
213
+ * every signed-in reader polled once a minute on top of a working stream.
214
+ *
215
+ * `true` relaxes the poll to {@link BADGE_RECONCILE_MS}, exactly as a
216
+ * `subscribe` does; anything else keeps {@link BADGE_POLL_MS}. It is a STATUS,
217
+ * so pass it live — a stream that drops takes the badge straight back to the
218
+ * fast poll on the next render.
219
+ */
220
+ live?: boolean;
204
221
  }
205
222
  /**
206
223
  * The bell badge number, for a host with its own trigger chrome.
@@ -464,9 +481,7 @@ interface WebNotifications {
464
481
  * right now, and `useBellBadge` below is the door. Still the right hook for
465
482
  * anything that genuinely wants "how many unread rows".
466
483
  */
467
- useUnreadCount: (options?: {
468
- enabled?: boolean;
469
- }) => number;
484
+ useUnreadCount: (options?: BadgeHookOptions) => number;
470
485
  /**
471
486
  * The badge's NUMBER AND TONE, for a host with its own trigger chrome.
472
487
  *
@@ -482,9 +497,7 @@ interface WebNotifications {
482
497
  * hook that bell uses. Without live activities configured it is
483
498
  * `useUnreadCount` plus `hasNew: count > 0`.
484
499
  */
485
- useBellBadge: (options?: {
486
- enabled?: boolean;
487
- }) => BellBadge;
500
+ useBellBadge: (options?: BadgeHookOptions) => BellBadge;
488
501
  /** The shared client state, for host glue. */
489
502
  store: InboxStore;
490
503
  /** The bound wire client. */
@@ -492,6 +505,15 @@ interface WebNotifications {
492
505
  /** The copy in force, so a host's own chrome can reuse a sentence. */
493
506
  messages: NotificationMessages;
494
507
  }
508
+ /**
509
+ * What a host passes either badge hook: whether to run at all, and — for a host
510
+ * that holds its own realtime connection — whether that connection is up
511
+ * (`BadgeSyncOptions.live`).
512
+ */
513
+ interface BadgeHookOptions {
514
+ enabled?: boolean;
515
+ live?: boolean;
516
+ }
495
517
  declare function createWebNotifications(config: NotificationsWebConfig): WebNotifications;
496
518
 
497
- export { useUnreadCount as A, BADGE_POLL_MS as B, type InboxListStatus as I, type LiveActivitiesConfig as L, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BadgeSyncOptions as b, createWebNotifications as c, type BellBadge as d, type BellButtonProps as e, type InboxState as f, type InboxStore as g, type LiveActivitiesHook as h, type LiveActivityMessages as i, NotificationsHttpError as j, type NotificationsPanelProps as k, type NotificationsResult as l, type NotificationsSignalHook as m, type NotificationsSubscribe as n, type NotificationsTransport as o, type NotificationsWebConfig as p, type PreferencesPayload as q, type PreferencesScreenProps as r, type PushRegistrationPayload as s, type WebPushPlatformHint as t, type WebPushSetupConfig as u, createInboxStore as v, createNotificationsApiClient as w, httpNotificationsTransport as x, useInboxList as y, useInboxState as z };
519
+ export { useInboxState as A, BADGE_POLL_MS as B, useUnreadCount as C, type InboxListStatus as I, type LiveActivitiesConfig as L, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BadgeHookOptions as b, createWebNotifications as c, type BadgeSyncOptions as d, type BellBadge as e, type BellButtonProps as f, type InboxState as g, type InboxStore as h, type LiveActivitiesHook as i, type LiveActivityMessages as j, NotificationsHttpError as k, type NotificationsPanelProps as l, type NotificationsResult as m, type NotificationsSignalHook as n, type NotificationsSubscribe as o, type NotificationsTransport as p, type NotificationsWebConfig as q, type PreferencesPayload as r, type PreferencesScreenProps as s, type PushRegistrationPayload as t, type WebPushPlatformHint as u, type WebPushSetupConfig as v, createInboxStore as w, createNotificationsApiClient as x, httpNotificationsTransport as y, useInboxList as z };
@@ -1,4 +1,4 @@
1
- import { N as NotificationGenerator } from './types-H_aFzLA0.js';
1
+ import { N as NotificationGenerator } from './types-DPiePHJD.js';
2
2
 
3
3
  /**
4
4
  * Generator registry: one {@link NotificationGenerator} per event `type`.
@@ -1,9 +1,9 @@
1
1
  import { Hono, Context } from 'hono';
2
- import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-BTudlaSC.js';
3
- import '../generators-CQYdJfB5.js';
4
- import '../types-H_aFzLA0.js';
5
- import '../wire-Bn6aA2nL.js';
6
- import '../web-push-C6U-5JCV.js';
2
+ import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-cIEbFqoZ.js';
3
+ import '../generators-GUF-Kml-.js';
4
+ import '../types-DPiePHJD.js';
5
+ import '../wire-CJka1AvM.js';
6
+ import '../web-push-DUn_d_gj.js';
7
7
  import '../template-Dlt5S54z.js';
8
8
 
9
9
  /**
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  createApiNotifications
3
- } from "../chunk-WZBX7YCE.js";
4
- import "../chunk-GK6GSC2J.js";
3
+ } from "../chunk-53SH5ABN.js";
4
+ import "../chunk-XFODKRRB.js";
5
5
  import {
6
6
  messagesOf
7
7
  } from "../chunk-M2TVBVH2.js";
8
8
  import "../chunk-RTURLH5U.js";
9
- import "../chunk-CPQKKLPS.js";
9
+ import "../chunk-CSMFJJXY.js";
10
10
  import "../chunk-EKUSNUBT.js";
11
11
  import {
12
12
  __name
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, f as NotificationGenerateContext, N as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-H_aFzLA0.js';
2
- export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-CQYdJfB5.js';
3
- export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, T as TypeChannelRules, d as availableChannelsOf, e as capToAvailable, f as defaultChannelMatrix, g as enabledChannelsOf, i as inboxWire, m as mergeChoices, h as mergeStoredRow, j as messagesOf, r as resolveTypeChannels } from './wire-Bn6aA2nL.js';
1
+ export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, f as NotificationGenerateContext, N as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-DPiePHJD.js';
2
+ export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-GUF-Kml-.js';
3
+ export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, T as TypeChannelRules, d as availableChannelsOf, e as capToAvailable, f as defaultChannelMatrix, g as enabledChannelsOf, i as inboxWire, m as mergeChoices, h as mergeStoredRow, j as messagesOf, r as resolveTypeChannels } from './wire-CJka1AvM.js';
4
4
  export { L as LIVE_PUSH_TAG_PREFIX, a as LIVE_SUBJECT_KEY, b as LiveActivity, c as LiveActivityLane, d as LiveActivityStep, l as liveActivityLane, e as livePushTag } from './live-DYxEFO49.js';
5
5
 
6
6
  /** Thrown by `notify` when no generator is registered for the event type. */
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  mergeStoredRow,
13
13
  normalizePhoneE164,
14
14
  resolveTypeChannels
15
- } from "./chunk-GK6GSC2J.js";
15
+ } from "./chunk-XFODKRRB.js";
16
16
  import {
17
17
  messagesOf
18
18
  } from "./chunk-M2TVBVH2.js";
@@ -25,7 +25,7 @@ import {
25
25
  import {
26
26
  NOTIFICATION_CHANNELS,
27
27
  taxonomyOf
28
- } from "./chunk-CPQKKLPS.js";
28
+ } from "./chunk-CSMFJJXY.js";
29
29
  import "./chunk-7QVYU63E.js";
30
30
  export {
31
31
  DEFAULT_CHANNEL_ROW,
@@ -1,5 +1,5 @@
1
1
  import { WireJobBlueprint } from '@12-apps/wiring';
2
- import { A as ApiNotifications } from './create-api-notifications-BTudlaSC.js';
2
+ import { A as ApiNotifications } from './create-api-notifications-cIEbFqoZ.js';
3
3
 
4
4
  /**
5
5
  * The two background jobs getting a message out actually needs.
@@ -1,13 +1,13 @@
1
1
  import { EmailPreviewRoute } from '../email/previews/index.js';
2
2
  import * as _12_apps_wiring from '@12-apps/wiring';
3
3
  import { WireRequest } from '@12-apps/wiring';
4
- import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-BTudlaSC.js';
5
- import { N as NotificationsJobDeps } from '../jobs-CaovU4GM.js';
4
+ import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-cIEbFqoZ.js';
5
+ import { N as NotificationsJobDeps } from '../jobs-wKoOz91Q.js';
6
6
  import { E as EmailPreviewsConfig } from '../catalog-BOVZ2b-b.js';
7
- import '../generators-CQYdJfB5.js';
8
- import '../types-H_aFzLA0.js';
9
- import '../wire-Bn6aA2nL.js';
10
- import '../web-push-C6U-5JCV.js';
7
+ import '../generators-GUF-Kml-.js';
8
+ import '../types-DPiePHJD.js';
9
+ import '../wire-CJka1AvM.js';
10
+ import '../web-push-DUn_d_gj.js';
11
11
  import '../template-Dlt5S54z.js';
12
12
 
13
13
  /** One `NotificationsRoute` as the wiring contract reads it. */
@@ -3,14 +3,14 @@ import {
3
3
  } from "../chunk-FTFLU6KH.js";
4
4
  import {
5
5
  NOTIFICATIONS_JOBS
6
- } from "../chunk-FBBPS2LT.js";
6
+ } from "../chunk-QPO6NSRR.js";
7
7
  import {
8
8
  createApiNotifications
9
- } from "../chunk-WZBX7YCE.js";
10
- import "../chunk-GK6GSC2J.js";
9
+ } from "../chunk-53SH5ABN.js";
10
+ import "../chunk-XFODKRRB.js";
11
11
  import "../chunk-M2TVBVH2.js";
12
12
  import "../chunk-RTURLH5U.js";
13
- import "../chunk-CPQKKLPS.js";
13
+ import "../chunk-CSMFJJXY.js";
14
14
  import "../chunk-EKUSNUBT.js";
15
15
  import {
16
16
  __name
@@ -1,8 +1,8 @@
1
1
  import { c as createEmailPreviewScreen } from '../preview-screen-DYJRAnAY.js';
2
- import { c as createWebNotifications } from '../create-web-notifications-2xxbKnrW.js';
2
+ import { c as createWebNotifications } from '../create-web-notifications-32Myo-ww.js';
3
3
  import 'react';
4
- import '../wire-Bn6aA2nL.js';
5
- import '../types-H_aFzLA0.js';
4
+ import '../wire-CJka1AvM.js';
5
+ import '../types-DPiePHJD.js';
6
6
  import '../live-DYxEFO49.js';
7
7
 
8
8
  /**
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createWebNotifications
3
- } from "../chunk-PNY6S6WH.js";
4
- import "../chunk-2IAHFIXS.js";
3
+ } from "../chunk-643HNPQJ.js";
4
+ import "../chunk-AN6IX4AE.js";
5
5
  import {
6
6
  createEmailPreviewScreen
7
7
  } from "../chunk-KEYRE245.js";
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  BellIcon,
7
7
  useInboxList
8
- } from "./chunk-2IAHFIXS.js";
8
+ } from "./chunk-AN6IX4AE.js";
9
9
  import "./chunk-RTURLH5U.js";
10
10
  import {
11
11
  __name
@@ -295,4 +295,4 @@ __name(NotificationsPanel, "NotificationsPanel");
295
295
  export {
296
296
  NotificationsPanel
297
297
  };
298
- //# sourceMappingURL=panel-MKI4PTNZ.js.map
298
+ //# sourceMappingURL=panel-JL5RDEHY.js.map