@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.
- package/dist/{chunk-WZBX7YCE.js → chunk-53SH5ABN.js} +82 -34
- package/dist/chunk-53SH5ABN.js.map +1 -0
- package/dist/{chunk-PNY6S6WH.js → chunk-643HNPQJ.js} +4 -4
- package/dist/chunk-643HNPQJ.js.map +1 -0
- package/dist/{chunk-2IAHFIXS.js → chunk-AN6IX4AE.js} +4 -3
- package/dist/chunk-AN6IX4AE.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-32Myo-ww.d.ts} +31 -9
- 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 +2 -2
- package/dist/{panel-MKI4PTNZ.js → panel-JL5RDEHY.js} +2 -2
- 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 +2 -2
- 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/react/create-web-notifications.tsx +12 -2
- package/src/react/hooks.ts +24 -4
- package/src/react/index.ts +1 -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-2IAHFIXS.js.map +0 -1
- package/dist/chunk-PNY6S6WH.js.map +0 -1
- package/dist/chunk-WZBX7YCE.js.map +0 -1
- /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/{panel-MKI4PTNZ.js.map → panel-JL5RDEHY.js.map} +0 -0
- /package/dist/{preferences-screen-S3ZHX5LB.js.map → preferences-screen-YRPNUZS5.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/context.ts","../src/server/transports/drivers.ts","../src/server/transports/email.ts","../src/server/transports/sms.ts","../src/server/transports/web-push.ts","../src/server/transports/whatsapp.ts","../src/server/transports/registry.ts","../src/server/by-permission.ts","../src/server/dispatch.ts","../src/server/inbox.ts","../src/server/preferences.ts","../src/server/push-subscriptions.ts","../src/server/routes.ts","../src/server/router.ts","../src/server/create-api-notifications.ts"],"sourcesContent":["import type { NotificationWireMessages } from '../messages';\nimport { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';\n\n/**\n * What every route in this surface shares (12-15): the actor, the request, the\n * response envelope and the body parsing. Mirrors the entity-lifecycle /\n * report-builder shape — framework-neutral descriptors a forty-line adapter\n * mounts.\n */\n\n/**\n * What a host must resolve before a request reaches these handlers: WHO is\n * calling. That is the whole seam.\n *\n * There is no tenant here and no permission list, and both absences are the\n * design. Every endpoint in this surface is SELF-scoped — a user reads and\n * writes their own inbox and their own preferences — so the only authorization\n * question is \"who is signed in\", and the answer is applied by scoping every\n * query to `userId` rather than by a guard that could be forgotten. A\n * permission-gated ADMIN view of someone else's inbox would be a different\n * surface, and would need a different actor.\n */\nexport interface NotificationsActor {\n userId: string;\n /**\n * The store whose ORIGIN this request arrived on, resolved by the\n * HOST from the hostname — never read from the body or the query.\n *\n * The paragraph above still holds: this is not a tenant the surface\n * authorizes against, it is a NARROWING of a read that is already the\n * caller's own, and it can never widen one. A host with a single origin never\n * sets it and every endpoint answers exactly what it answered before.\n *\n * It exists because a multi-tenant host installs one storefront per store as\n * its own PWA, and a PWA's identity is its ORIGIN — so an inbox answering a\n * store's own app with a neighbour's notifications is one store reporting on\n * another.\n */\n scopeClientId?: string;\n}\n\n/** One request, already authenticated and routed by the host. */\nexport interface NotificationsRequest {\n actor: NotificationsActor;\n params: Record<string, string | undefined>;\n query: Record<string, string | undefined>;\n body?: unknown;\n /** Headers the surface reads (`user-agent`, for the device hint). */\n headers?: Record<string, string | undefined>;\n /**\n * The language to answer this caller in, as a language tag (`pt-BR`,\n * `en-US`) — the same field `@12-apps/wiring`'s `WireRequest` carries.\n *\n * Populated by the host's adapter, which is the only layer that can\n * negotiate one. Absent is meaningful and not an error: a host with one\n * audience never sets it, and this surface must then answer with the words\n * it was configured with rather than invent a language.\n */\n locale?: string;\n}\n\n/** What a handler answers with; the adapter maps it onto its response type. */\nexport interface NotificationsResponse {\n status: number;\n /** `undefined` means NO body at all (204) — not the same as `null`. */\n body: unknown;\n}\n\nexport interface NotificationsRoute {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /**\n * Path relative to the host's account mount, in `:param` form. The SHAPE is\n * fixed because the packaged client builds these URLs.\n */\n path: string;\n handle(request: NotificationsRequest): Promise<NotificationsResponse>;\n}\n\n/** A user-safe API error carrying the HTTP status the wire promises. */\nexport class NotificationsApiError extends Error {\n readonly status: number;\n constructor(status: number, message: string) {\n super(message);\n this.name = 'NotificationsApiError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationsApiError.prototype);\n }\n}\n\n/** Success is `{ data }`; a denial is `{ error }`, unwrapped. */\nexport const ok = (data: unknown, status = 200): NotificationsResponse => ({\n status,\n body: { data },\n});\n\nconst fail = (status: number, error: string): NotificationsResponse => ({\n status,\n body: { error },\n});\n\n/** Fold a thrown {@link NotificationsApiError} into a response; rethrow the rest. */\nexport function foldApiError(error: unknown): NotificationsResponse {\n if (error instanceof NotificationsApiError) return fail(error.status, error.message);\n throw error;\n}\n\n/** Wrap a handler so its thrown api errors become the wire's `{ error }`. */\nexport function guarded(\n handle: (request: NotificationsRequest) => Promise<NotificationsResponse>,\n): (request: NotificationsRequest) => Promise<NotificationsResponse> {\n return async (request) => {\n try {\n return await handle(request);\n } catch (error) {\n return foldApiError(error);\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// Body / query parsing — the request contract, in the package\n// ---------------------------------------------------------------------------\n\nfunction asRecord(body: unknown, messages: NotificationWireMessages): Record<string, unknown> {\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return body as Record<string, unknown>;\n}\n\n/** 1..100 non-empty string ids. */\nfunction parseIds(value: unknown, messages: NotificationWireMessages): string[] {\n if (!Array.isArray(value) || value.length === 0 || value.length > 100) {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return value.map((id) => {\n if (typeof id !== 'string' || id.length === 0) {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return id;\n });\n}\n\n/**\n * Query strings arrive as strings; the store clamps, but a non-number is a\n * client bug and must not silently read as \"the default page\".\n */\nfunction parseLimit(raw: string | undefined, messages: NotificationWireMessages): number | undefined {\n if (raw === undefined || raw === '') return undefined;\n const limit = Number(raw);\n if (!Number.isInteger(limit) || limit < 1 || limit > 100) {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return limit;\n}\n\nfunction parseFilter(\n raw: string | undefined,\n messages: NotificationWireMessages,\n): 'all' | 'unread' | undefined {\n if (raw === undefined) return undefined;\n if (raw !== 'all' && raw !== 'unread') {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return raw;\n}\n\n/** `GET <mount>/notifications` — `filter`, `cursor`, `limit`. */\nexport function parseListQuery(\n query: Record<string, string | undefined>,\n messages: NotificationWireMessages,\n): { filter?: 'all' | 'unread'; cursor?: string; limit?: number } {\n const filter = parseFilter(query.filter, messages);\n const limit = parseLimit(query.limit, messages);\n return {\n ...(filter !== undefined ? { filter } : {}),\n ...(query.cursor ? { cursor: query.cursor } : {}),\n ...(limit !== undefined ? { limit } : {}),\n };\n}\n\n/** `POST <mount>/notifications/mark-read` — explicit ids, or `all: true`. */\nexport function parseMarkReadBody(\n body: unknown,\n messages: NotificationWireMessages,\n): { all: true } | { ids: string[] } {\n const record = asRecord(body, messages);\n const wantsAll = record.all === true;\n const hasIds = record.ids !== undefined;\n // Exactly one of the two. Both, or neither, is ambiguous in the one direction\n // that matters: \"mark everything read\" is not a thing to guess at.\n if (wantsAll === hasIds) {\n throw new NotificationsApiError(400, messages.markReadTargetRequired);\n }\n return wantsAll ? { all: true } : { ids: parseIds(record.ids, messages) };\n}\n\n/** `POST <mount>/notifications/delete` — 1..100 ids. */\nexport function parseDeleteBody(body: unknown, messages: NotificationWireMessages): string[] {\n return parseIds(asRecord(body, messages).ids, messages);\n}\n\n/**\n * `PUT <mount>/notification-preferences` — any subset of categories, each with\n * any subset of channel toggles. Categories left out stay untouched.\n */\nexport function parsePreferencesBody(\n body: unknown,\n messages: NotificationWireMessages,\n): Record<string, Partial<Record<NotificationChannel, boolean>>> {\n const record = asRecord(body, messages);\n const parsed: Record<string, Partial<Record<NotificationChannel, boolean>>> = {};\n for (const [category, value] of Object.entries(record)) {\n parsed[category] = parseToggles(value, messages);\n }\n return parsed;\n}\n\n/** One category's toggles, narrowed onto the closed channel set. */\nfunction parseToggles(\n value: unknown,\n messages: NotificationWireMessages,\n): Partial<Record<NotificationChannel, boolean>> {\n const toggles = asRecord(value, messages);\n const row: Partial<Record<NotificationChannel, boolean>> = {};\n for (const channel of NOTIFICATION_CHANNELS) {\n const flag = toggles[channel];\n if (flag === undefined) continue;\n if (typeof flag !== 'boolean') throw new NotificationsApiError(400, messages.invalidBody);\n row[channel] = flag;\n }\n return row;\n}\n\nconst MAX_ENDPOINT_CHARS = 2000;\nconst MAX_KEY_CHARS = 500;\n\nfunction parseEndpoint(value: unknown, messages: NotificationWireMessages): string {\n if (typeof value !== 'string' || value.length === 0 || value.length > MAX_ENDPOINT_CHARS) {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n // A push endpoint is a URL the server will POST to, so an absolute http(s)\n // URL is a hard requirement rather than a formality.\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return value;\n}\n\n/** `POST <mount>/push-subscriptions` — `PushSubscription.toJSON()`. */\nexport function parsePushSubscriptionBody(\n body: unknown,\n messages: NotificationWireMessages,\n): { endpoint: string; keys: { p256dh: string; auth: string } } {\n const record = asRecord(body, messages);\n const keys = asRecord(record.keys, messages);\n const key = (value: unknown): string => {\n if (typeof value !== 'string' || value.length === 0 || value.length > MAX_KEY_CHARS) {\n throw new NotificationsApiError(400, messages.invalidBody);\n }\n return value;\n };\n return {\n endpoint: parseEndpoint(record.endpoint, messages),\n keys: { p256dh: key(keys.p256dh), auth: key(keys.auth) },\n };\n}\n\n/** `DELETE <mount>/push-subscriptions` — by endpoint. */\nexport function parsePushEndpointBody(body: unknown, messages: NotificationWireMessages): string {\n return parseEndpoint(asRecord(body, messages).endpoint, messages);\n}\n\n/**\n * `GET <mount>/push-subscriptions?endpoint=…` — the browser asking \"is the\n * subscription I am holding still MINE on the server?\".\n *\n * Optional: with no `endpoint` the route answers the key and the count as it\n * always has. Validated by the same rules as the write, so a junk value is a 400\n * rather than a lookup.\n */\nexport function parsePushEndpointQuery(\n query: Record<string, string | undefined>,\n messages: NotificationWireMessages,\n): string | undefined {\n if (query.endpoint === undefined || query.endpoint === '') return undefined;\n return parseEndpoint(query.endpoint, messages);\n}\n","/**\n * The DRIVER port — the reason a host adds a vendor with a config entry and no\n * code (12-15).\n *\n * the origin's transports each read `process.env` directly:\n * `NOTIFICATIONS_EMAIL_PROVIDER=resend` plus `RESEND_API_KEY`, and a second\n * vendor meant editing the package. That is exactly backwards for a published\n * package — it cannot know a host's variable names, and it must not be the\n * thing that decides whether a channel is on. So a channel is configured by\n * DECLARATION:\n *\n * transports: [\n * { channel: 'EMAIL', driver: 'resend', apiKey, from },\n * { channel: 'SMS', driver: 'log' },\n * ]\n *\n * A channel with no declaration reports `supports() === false` and the router\n * skips it: no delivery row, nothing fake-sent. A second vendor is one more\n * entry in the built-in driver table (or `drivers` on the config, for a host's\n * own), and the router, the registry and the other transports are untouched.\n */\n\nimport { normalizePhoneE164 } from '../../phone';\nimport type {\n NotificationContent,\n NotificationTransport,\n TransportRecipient,\n} from '../../types';\n\n/** Every HTTP call a built-in driver makes goes through this, so a test can. */\nexport type FetchImpl = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;\n\n/** Shared by every declaration: which channel, which vendor. */\nexport interface DriverDeclarationBase {\n /** Vendor key: `resend` / `twilio` / `meta` / `log`, or a host's own. */\n driver: string;\n /**\n * The HTTP client the vendor call uses. Defaults to the global `fetch`.\n * Supplied by tests and by hosts that need a proxy or a retry policy.\n */\n fetchImpl?: FetchImpl;\n}\n\n/** A vendor rejection, carrying what the provider actually said. */\nexport class NotificationProviderError extends Error {\n readonly status: number;\n constructor(vendor: string, status: number, detail: string) {\n super(`${vendor} rejected the message (${status} ${detail}).`);\n this.name = 'NotificationProviderError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationProviderError.prototype);\n }\n}\n\n/** `fetch` and throw {@link NotificationProviderError} on a non-2xx. */\nexport async function postOrThrow(\n vendor: string,\n fetchImpl: FetchImpl | undefined,\n url: string,\n init: { headers: Record<string, string>; body: string },\n): Promise<void> {\n const call = fetchImpl ?? (globalThis.fetch as unknown as FetchImpl);\n const response = await call(url, { method: 'POST', ...init });\n if (!response.ok) {\n throw new NotificationProviderError(vendor, response.status, await response.text());\n }\n}\n\n/**\n * Resolve one declaration against a driver table.\n *\n * Returning `null` — an unknown driver name — is a CONFIGURATION error, not a\n * runtime one, so the caller (the transport factory) throws with the names it\n * does know. A typo'd vendor that silently disabled a channel is the failure\n * mode this whole seam exists to remove.\n */\nexport function resolveDriver<TDeclaration extends DriverDeclarationBase, TDriver>(\n channel: string,\n declaration: TDeclaration,\n table: Record<string, (declaration: TDeclaration) => TDriver>,\n): TDriver {\n const factory = table[declaration.driver];\n if (!factory) {\n throw new Error(\n `@12-apps/notifications: unknown ${channel} driver \"${declaration.driver}\". ` +\n `Known drivers: ${Object.keys(table).sort().join(', ')}.`,\n );\n }\n return factory(declaration);\n}\n\n/**\n * Absolutize an in-app link for a channel that leaves the app.\n *\n * A relative path is useless in an inbox and a fabricated localhost link is\n * worse than none, so with no `appUrl` configured the link is simply dropped —\n * The origin's rule, kept.\n */\nexport function absoluteLink(link: string | undefined, appUrl: string | undefined): string | null {\n if (!link || !appUrl) return null;\n try {\n return new URL(link, appUrl).toString();\n } catch {\n return null;\n }\n}\n\n/**\n * The two PHONE channels' shared skeleton.\n *\n * SMS and WhatsApp differ in their vendor, their message shape and their\n * formatter — and in nothing else: both are unavailable for a recipient whose\n * number will not normalize, both normalize before the vendor ever sees the\n * destination, and both refuse rather than send when it cannot be. Stating that\n * rule twice is how the two drift, which is exactly what happened to the\n * transports' `supports()` gates before they funnelled through one helper.\n */\nexport function phoneChannel<TMessage>(\n channel: 'SMS' | 'WHATSAPP',\n options: {\n /** Country calling code for a bare local number. Required — see `phone.ts`. */\n defaultCountryCode: string;\n format: (content: NotificationContent) => TMessage;\n send: (toE164: string, message: TMessage) => Promise<void>;\n },\n): NotificationTransport<TMessage> {\n const toE164 = (recipient: TransportRecipient): string | null =>\n normalizePhoneE164(recipient.phone, {\n defaultCountryCode: options.defaultCountryCode,\n });\n return {\n channel,\n supports: (recipient) => toE164(recipient) !== null,\n format: options.format,\n async send(message, recipient) {\n const to = toE164(recipient);\n if (!to) throw new Error('Recipient has no usable phone number.');\n await options.send(to, message);\n },\n };\n}\n","import type {\n NotificationContent,\n NotificationLogger,\n NotificationTransport,\n TransportRecipient,\n} from '../../types';\n\nimport { renderEmail } from '../../email/template';\nimport type { EmailChromeCopy } from '../../email/template';\nimport type { EmailTheme } from '../../email/theme';\n\nimport {\n absoluteLink,\n postOrThrow,\n resolveDriver,\n type DriverDeclarationBase,\n} from './drivers';\n\n/**\n * EMAIL transport: formatter + sender behind the driver port.\n *\n * - `driver: 'resend'` — Resend's HTTP API (plain JSON POST, no SDK):\n * `apiKey` + `from`.\n * - `driver: 'log'` — dev/e2e driver: logs the message instead of sending\n * (explicit opt-in, never a silent default).\n * - no EMAIL declaration at all — `supports() === false`, router skips it.\n *\n * A different vendor (SES, an SMTP relay…) is one more entry in\n * {@link EMAIL_DRIVERS} — this transport, the router and the registry stay\n * untouched.\n */\n\n/** The channel message an email formatter produces. */\nexport interface EmailMessage {\n subject: string;\n text: string;\n html: string;\n}\n\n/** The vendor seam: deliver one already-formatted email. Throws on failure. */\nexport interface EmailDriver {\n send(to: string, message: EmailMessage): Promise<void>;\n}\n\nexport interface EmailDriverDeclaration extends DriverDeclarationBase {\n channel: 'EMAIL';\n /** Resend: the API key. */\n apiKey?: string;\n /** Resend: the verified `From` address. */\n from?: string;\n /** Where the CTA link points; without it a link is dropped. */\n appUrl?: string;\n /**\n * The CTA link's label. REQUIRED — this used to default to `'Ver detalhes'`,\n * so a host that declared EMAIL and nothing else mailed this product's\n * Portuguese to its own users, in their inbox, signed with the host's own\n * `from` address.\n */\n linkLabel: string;\n /**\n * Render through `../../email` — the ONE layout — instead of the three bare\n * `<p>` tags below.\n *\n * OPTIONAL, and its absence is the pre-layout behaviour verbatim. That is\n * deliberate rather than timid: `brand` and `chrome` are REQUIRED with no\n * default anywhere in the layout (a package that defaulted them would sign\n * another company's mail, in a language nobody chose), so a required field\n * here would break every host that already declares EMAIL — at runtime, on\n * the first send, which is the worst place to find out.\n *\n * So the seam is opt-in and the ad-hoc path is what remains for a host that\n * has not taken it. `layout.theme` may still be omitted; `../../email/theme`\n * argues why that one asymmetry is allowed.\n */\n layout?: {\n /** The product name in the header and the footer. */\n brand: string;\n /** The layout's own words. `@12-apps/notifications/email/locales` ships packs. */\n chrome: EmailChromeCopy;\n /** The recipient's language, for the document's `lang` attribute. */\n locale: string;\n /** Defaults to the layout's neutral palette. */\n theme?: EmailTheme;\n };\n logger?: NotificationLogger;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"');\n}\n\nconst resendDriver = (declaration: EmailDriverDeclaration): EmailDriver => ({\n async send(to, message) {\n if (!declaration.apiKey || !declaration.from) {\n throw new Error('The resend email driver needs both `apiKey` and `from`.');\n }\n await postOrThrow('Resend', declaration.fetchImpl, 'https://api.resend.com/emails', {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${declaration.apiKey}`,\n },\n body: JSON.stringify({\n from: declaration.from,\n to: [to],\n subject: message.subject,\n text: message.text,\n html: message.html,\n }),\n });\n },\n});\n\nconst logEmailDriver = (declaration: EmailDriverDeclaration): EmailDriver => ({\n // Deliberately logs NO destination address — a recipient e-mail is PII and\n // must never reach logs; the subject alone is enough for local debugging.\n send(_to, message) {\n declaration.logger?.info(\n `[notifications:email] log driver suppressed a real send (subject=\"${message.subject}\")`,\n );\n return Promise.resolve();\n },\n});\n\n/** The built-in email vendors. A host adds one by extending this table. */\nexport const EMAIL_DRIVERS: Record<\n string,\n (declaration: EmailDriverDeclaration) => EmailDriver\n> = {\n resend: resendDriver,\n log: logEmailDriver,\n};\n\n/**\n * Agnostic content → subject/text/html. The link becomes a trailing CTA, only\n * when an app base URL is configured.\n *\n * Two renderings, and which one runs is the host's choice: with `layout`\n * declared this is the shared document (`../../email`), and without it the\n * three bare `<p>` tags that predate it. See `EmailDriverDeclaration.layout`\n * for why the new path could not simply replace the old one.\n */\nexport function formatEmail(\n content: NotificationContent,\n declaration: EmailDriverDeclaration,\n): EmailMessage {\n const href = absoluteLink(content.link, declaration.appUrl);\n const label = declaration.linkLabel;\n if (declaration.layout) return layoutEmail(content, declaration.layout, href, label);\n return {\n subject: content.title,\n text: href ? `${content.body}\\n\\n${href}` : content.body,\n html: [\n `<p><strong>${escapeHtml(content.title)}</strong></p>`,\n `<p>${escapeHtml(content.body)}</p>`,\n ...(href ? [`<p><a href=\"${escapeHtml(href)}\">${escapeHtml(label)}</a></p>`] : []),\n ].join('\\n'),\n };\n}\n\n/**\n * The same notification as a laid-out document.\n *\n * The layout escapes and builds the plain-text twin from the SAME object, which\n * is the pair of defects the branch above still carries by construction: its\n * text half is assembled separately, and its `href` reaches the anchor through\n * `escapeHtml` rather than through a scheme check.\n */\nfunction layoutEmail(\n content: NotificationContent,\n layout: NonNullable<EmailDriverDeclaration['layout']>,\n href: string | null,\n label: string,\n): EmailMessage {\n const rendered = renderEmail({\n subject: content.title,\n heading: content.title,\n paragraphs: [content.body],\n action: href ? { label, href } : undefined,\n chrome: layout.chrome,\n brand: layout.brand,\n locale: layout.locale,\n theme: layout.theme,\n });\n return { subject: rendered.subject, text: rendered.text, html: rendered.html };\n}\n\nexport function emailTransport(\n declaration: EmailDriverDeclaration,\n extraDrivers: Record<string, (d: EmailDriverDeclaration) => EmailDriver> = {},\n): NotificationTransport<EmailMessage> {\n const driver = resolveDriver('EMAIL', declaration, { ...EMAIL_DRIVERS, ...extraDrivers });\n return {\n channel: 'EMAIL',\n // Truthiness, not `!== null`: an EMPTY STRING is a very ordinary DB value\n // for a nullable column, and it used to pass this gate, earn a delivery row\n // and then fail forever against `send`'s own `!recipient.email` check.\n supports: (recipient: TransportRecipient) => Boolean(recipient.email),\n format: (content) => formatEmail(content, declaration),\n async send(message, recipient) {\n if (!recipient.email) throw new Error('Recipient has no email address.');\n await driver.send(recipient.email, message);\n },\n };\n}\n","import type {\n NotificationContent,\n NotificationLogger,\n NotificationTransport,\n} from '../../types';\n\nimport {\n absoluteLink,\n phoneChannel,\n postOrThrow,\n resolveDriver,\n type DriverDeclarationBase,\n} from './drivers';\n\n/**\n * SMS transport — Twilio behind the same driver port as email.\n *\n * - `driver: 'twilio'` — the Messages API (form-encoded POST with basic\n * auth, no SDK): `accountSid`, `authToken`, `from` (an E.164 sender or a\n * Messaging Service SID).\n * - `driver: 'log'` — dev driver: logs instead of sending.\n * - no SMS declaration — channel unavailable, router skips it.\n *\n * A recipient without a normalizable phone is unavailable on this channel\n * regardless of the driver (see `../../phone.ts` for the verification caveat).\n */\n\n/** The channel message the SMS formatter produces: one plain text body. */\nexport interface SmsMessage {\n body: string;\n}\n\nexport interface SmsDriver {\n send(toE164: string, message: SmsMessage): Promise<void>;\n}\n\nexport interface SmsDriverDeclaration extends DriverDeclarationBase {\n channel: 'SMS';\n accountSid?: string;\n authToken?: string;\n from?: string;\n appUrl?: string;\n /**\n * Country calling code for a bare local number, digits only (`'55'`, `'1'`).\n * REQUIRED: this package assumes no country, because the one it used to\n * assume turned a US number into a plausible Brazilian mobile and texted a\n * stranger the customer's order (see `../../phone.ts`).\n */\n defaultCountryCode: string;\n logger?: NotificationLogger;\n}\n\n/** SMS bodies are billed per 160-char segment — keep one message ≤ 3 segments. */\nconst MAX_SMS_CHARS = 480;\n\nconst twilioDriver = (declaration: SmsDriverDeclaration): SmsDriver => ({\n async send(toE164, message) {\n const sid = declaration.accountSid;\n if (!sid || !declaration.authToken || !declaration.from) {\n throw new Error('The twilio sms driver needs `accountSid`, `authToken` and `from`.');\n }\n const auth = Buffer.from(`${sid}:${declaration.authToken}`).toString('base64');\n await postOrThrow(\n 'Twilio',\n declaration.fetchImpl,\n `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(sid)}/Messages.json`,\n {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${auth}`,\n },\n body: new URLSearchParams({\n To: toE164,\n From: declaration.from,\n Body: message.body,\n }).toString(),\n },\n );\n },\n});\n\nconst logSmsDriver = (declaration: SmsDriverDeclaration): SmsDriver => ({\n // Deliberately logs NO destination — a phone number is PII and must never\n // reach logs; the message body alone is enough for local debugging.\n send(_toE164, message) {\n declaration.logger?.info(\n `[notifications:sms] log driver suppressed a real send (body=\"${message.body}\")`,\n );\n return Promise.resolve();\n },\n});\n\nexport const SMS_DRIVERS: Record<string, (declaration: SmsDriverDeclaration) => SmsDriver> = {\n twilio: twilioDriver,\n log: logSmsDriver,\n};\n\n/** Agnostic content → one plain SMS: \"title: body (link)\", length-capped. */\nexport function formatSms(\n content: NotificationContent,\n declaration: SmsDriverDeclaration,\n): SmsMessage {\n const parts = [`${content.title}: ${content.body}`];\n const href = absoluteLink(content.link, declaration.appUrl);\n if (href) parts.push(href);\n const body = parts.join(' ');\n return { body: body.length > MAX_SMS_CHARS ? `${body.slice(0, MAX_SMS_CHARS - 1)}…` : body };\n}\n\nexport function smsTransport(\n declaration: SmsDriverDeclaration,\n extraDrivers: Record<string, (d: SmsDriverDeclaration) => SmsDriver> = {},\n): NotificationTransport<SmsMessage> {\n const driver = resolveDriver('SMS', declaration, { ...SMS_DRIVERS, ...extraDrivers });\n return phoneChannel<SmsMessage>('SMS', {\n defaultCountryCode: declaration.defaultCountryCode,\n format: (content) => formatSms(content, declaration),\n send: (toE164, message) => driver.send(toE164, message),\n });\n}\n","import { livePushTag } from '../../live';\nimport type {\n NotificationContent,\n NotificationLogger,\n NotificationTransport,\n TransportRecipient,\n} from '../../types';\n\nimport { resolveDriver, type DriverDeclarationBase } from './drivers';\n\n/**\n * WEB_PUSH transport — browser alerts over the Web Push protocol.\n *\n * What is the PACKAGE's, and stays here: the payload the service worker\n * renders, the fan-out to every one of the user's browsers, the 404/410 PRUNE\n * that makes `push_subscriptions` self-heal, and the rule that a send succeeds\n * when at least one subscription accepted it and fails only when all errored.\n *\n * What is the VENDOR's, and crosses as a port: VAPID signing and RFC 8291\n * payload encryption. Those need the `web-push` package, a node-only\n * dependency this package must not force on a host that never enables the\n * channel — so `@12-apps/notifications/web-push` exports the sender behind its\n * own subpath and an optional peer, exactly as `./hono` does for the adapter.\n *\n * - `driver: 'vapid'` — `sender: vapidPushSender({ subject, publicKey,\n * privateKey })` from that subpath (or any other signer).\n * - `driver: 'log'` — dev driver: logs instead of sending.\n * - no WEB_PUSH declaration — channel unavailable, router skips it.\n */\n\n/** One browser subscription, as `PushManager.subscribe()` yields it. */\nexport interface WebPushSubscription {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n}\n\n/**\n * The vendor seam: sign + encrypt + POST one payload to one push service.\n *\n * MUST reject with an error carrying a numeric `statusCode` when the push\n * service answers one — that is how a gone subscription (404/410) is told\n * apart from a transient failure, and therefore what makes the prune correct\n * rather than destructive.\n */\nexport type WebPushSender = (\n subscription: WebPushSubscription,\n payload: string,\n) => Promise<void>;\n\n/** The channel message the Web Push formatter produces (the SW's payload). */\nexport interface WebPushMessage {\n title: string;\n body: string;\n link: string | null;\n data: Record<string, unknown>;\n /**\n * The OS tray tag, or `null` for an ordinary event.\n *\n * Present for a notification about a LIVE SUBJECT (see `../../live`), and\n * this is the whole of the live feature's reach onto a locked phone: a tag\n * makes the next push about the same subject REPLACE the one already in the\n * tray — silently — instead of stacking under it. Four stages then cost one\n * entry and one buzz, and the entry that remains is the current one.\n *\n * {@link formatWebPush} always sets it, `null` included, so on the WIRE the\n * key is always there and a service worker reads one shape. A worker that\n * ignores it keeps today's behaviour exactly, which is what makes this safe\n * to ship ahead of the workers.\n *\n * OPTIONAL in the type, and only in the type: a host that hand-writes its own\n * `NotificationTransport<WebPushMessage>` — a fixture, a second driver —\n * would otherwise stop compiling on a field it has no way to know about and\n * no reason to set.\n */\n tag?: string | null;\n}\n\nexport interface WebPushDriverDeclaration extends DriverDeclarationBase {\n channel: 'WEB_PUSH';\n /** Required by the `vapid` driver: the signer. */\n sender?: WebPushSender;\n /**\n * The VAPID PUBLIC key, served to the browser by\n * `GET <mount>/push-subscriptions` so the client can subscribe. Public by\n * definition — the private key never crosses into this declaration.\n */\n publicKey?: string;\n logger?: NotificationLogger;\n}\n\n/** The subscriptions the transport reads and prunes (db-backed by the mount). */\nexport interface WebPushSubscriptionSource {\n /**\n * The subscriptions one notification may reach.\n *\n * `notificationClientId` is the notification's tenant — `null` for a\n * platform-wide one, which every subscription receives. An ADOPTER with a\n * hand-written source must honour it: a function declared with fewer\n * parameters still type-checks, so an un-updated implementation silently\n * ignores the scope and keeps fanning out to every origin, with nothing\n * failing to compile to say so.\n */\n list(\n userId: string,\n notificationClientId?: string | null,\n ): Promise<{ id: string; endpoint: string; p256dh: string; auth: string }[]>;\n prune(id: string): Promise<void>;\n}\n\nconst vapidDriver = (declaration: WebPushDriverDeclaration): WebPushSender => {\n const sender = declaration.sender;\n if (!sender) {\n throw new Error(\n 'The vapid web-push driver needs a `sender` — import `vapidPushSender` from ' +\n '@12-apps/notifications/web-push.',\n );\n }\n return sender;\n};\n\nconst logWebPushDriver =\n (declaration: WebPushDriverDeclaration): WebPushSender =>\n (_subscription, payload) => {\n // No endpoint logged: a push endpoint is a bearer capability for that\n // browser, so it is a secret in exactly the way an e-mail address is PII.\n declaration.logger?.info(\n `[notifications:web-push] log driver suppressed a real send (${payload.length} bytes)`,\n );\n return Promise.resolve();\n };\n\nexport const WEB_PUSH_DRIVERS: Record<\n string,\n (declaration: WebPushDriverDeclaration) => WebPushSender\n> = {\n vapid: vapidDriver,\n log: logWebPushDriver,\n};\n\n/** Status codes that mean \"this subscription no longer exists — prune it\". */\nconst GONE_STATUSES = new Set([404, 410]);\n\nfunction statusCodeOf(error: unknown): number | null {\n if (error && typeof error === 'object' && 'statusCode' in error) {\n const code = (error as { statusCode: unknown }).statusCode;\n return typeof code === 'number' ? code : null;\n }\n return null;\n}\n\nexport function formatWebPush(content: NotificationContent): WebPushMessage {\n return {\n title: content.title,\n body: content.body,\n link: content.link ?? null,\n data: content.data ?? {},\n tag: livePushTag(content.data),\n };\n}\n\nexport function webPushTransport(\n declaration: WebPushDriverDeclaration,\n subscriptions: WebPushSubscriptionSource,\n extraDrivers: Record<string, (d: WebPushDriverDeclaration) => WebPushSender> = {},\n): NotificationTransport<WebPushMessage> {\n const send = resolveDriver('WEB_PUSH', declaration, {\n ...WEB_PUSH_DRIVERS,\n ...extraDrivers,\n });\n return {\n channel: 'WEB_PUSH',\n supports: (recipient: TransportRecipient) => recipient.pushSubscriptionCount > 0,\n format: formatWebPush,\n async send(message, recipient) {\n const rows = await subscriptions.list(recipient.userId, recipient.clientId);\n if (rows.length === 0) throw new Error('Recipient no longer has push subscriptions.');\n const payload = JSON.stringify(message);\n let delivered = 0;\n let lastError: unknown = null;\n for (const row of rows) {\n try {\n await send({ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } }, payload);\n delivered += 1;\n } catch (error) {\n const status = statusCodeOf(error);\n if (status !== null && GONE_STATUSES.has(status)) {\n // Expired/unsubscribed browser — prune so future sends skip it.\n await subscriptions.prune(row.id).catch(() => {\n /* already pruned by a concurrent send */\n });\n } else {\n lastError = error;\n }\n }\n }\n if (delivered === 0) {\n throw lastError instanceof Error\n ? lastError\n : new Error('No push subscription accepted the payload.');\n }\n },\n };\n}\n","import type {\n NotificationContent,\n NotificationLogger,\n NotificationTransport,\n} from '../../types';\n\nimport {\n absoluteLink,\n phoneChannel,\n postOrThrow,\n resolveDriver,\n type DriverDeclarationBase,\n} from './drivers';\n\n/**\n * WHATSAPP transport — Meta's WhatsApp Cloud API behind the driver port.\n *\n * - `driver: 'meta'` — the Cloud API (JSON POST with a bearer token, no\n * SDK): `accessToken` + `phoneNumberId`.\n * - `driver: 'log'` — dev driver: logs instead of sending.\n * - no WHATSAPP declaration — channel unavailable, router skips it.\n *\n * Template/session-window rule: WhatsApp only accepts FREE-FORM text inside a\n * 24h customer-service window; business-initiated messages outside it require\n * a pre-approved TEMPLATE. With `templateName` set the transport sends that\n * template with two body parameters — {{1}} = title, {{2}} = body (language\n * `templateLanguage`, default `pt_BR`). Without it the transport sends\n * free-form text, and a send outside the session window FAILS with the\n * provider's error recorded on the delivery row — the documented fallback\n * behaviour, visible instead of silent.\n *\n * The window cannot be TRACKED from here (only Meta knows when the customer\n * last wrote), so a host that declares WHATSAPP with no `templateName` and then\n * emits business-initiated notifications has every send rejected. That is\n * visible on the delivery rows, but only once they exist — so the mount warns\n * about the combination the moment the declaration is read, which is the one\n * moment a misconfiguration is cheap to notice.\n */\n\n/** The channel message the WhatsApp formatter produces. */\nexport interface WhatsAppMessage {\n /** Free-form text used inside the session window / without a template. */\n text: string;\n /** Template body parameters ({{1}} title, {{2}} body) when templated. */\n templateParameters: [title: string, body: string];\n}\n\nexport interface WhatsAppDriver {\n send(toE164: string, message: WhatsAppMessage): Promise<void>;\n}\n\nexport interface WhatsAppDriverDeclaration extends DriverDeclarationBase {\n channel: 'WHATSAPP';\n accessToken?: string;\n phoneNumberId?: string;\n templateName?: string;\n /**\n * The WhatsApp template's language code, e.g. `pt_BR`, `en_US`.\n *\n * REQUIRED: a template is registered with Meta under one language, and\n * sending it with the wrong code is rejected by the Graph API. This defaulted\n * to `pt_BR` — one market's answer — so a host that forgot it did not get a\n * sensible fallback, it got somebody else's template language and a delivery\n * failure it had no reason to expect.\n */\n templateLanguage: string;\n /** Graph API base, so a host can pin a version. */\n graphApiBase?: string;\n appUrl?: string;\n /**\n * Country calling code for a bare local number, digits only (`'55'`, `'1'`).\n * REQUIRED for the same reason SMS requires it — see `../../phone.ts`.\n */\n defaultCountryCode: string;\n logger?: NotificationLogger;\n}\n\nconst DEFAULT_GRAPH_API_BASE = 'https://graph.facebook.com/v20.0';\n\nfunction templatePayload(\n toDigits: string,\n message: WhatsAppMessage,\n declaration: WhatsAppDriverDeclaration,\n): object {\n return {\n messaging_product: 'whatsapp',\n to: toDigits,\n type: 'template',\n template: {\n name: declaration.templateName,\n language: { code: declaration.templateLanguage },\n components: [\n {\n type: 'body',\n parameters: message.templateParameters.map((text) => ({ type: 'text', text })),\n },\n ],\n },\n };\n}\n\nfunction textPayload(toDigits: string, message: WhatsAppMessage): object {\n return {\n messaging_product: 'whatsapp',\n to: toDigits,\n type: 'text',\n text: { body: message.text },\n };\n}\n\nconst metaDriver = (declaration: WhatsAppDriverDeclaration): WhatsAppDriver => ({\n async send(toE164, message) {\n if (!declaration.accessToken || !declaration.phoneNumberId) {\n throw new Error('The meta whatsapp driver needs `accessToken` and `phoneNumberId`.');\n }\n // The Cloud API addresses recipients by bare digits (no `+`).\n const toDigits = toE164.replace('+', '');\n const payload = declaration.templateName\n ? templatePayload(toDigits, message, declaration)\n : textPayload(toDigits, message);\n const base = declaration.graphApiBase ?? DEFAULT_GRAPH_API_BASE;\n await postOrThrow(\n 'WhatsApp Cloud API',\n declaration.fetchImpl,\n `${base}/${encodeURIComponent(declaration.phoneNumberId)}/messages`,\n {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${declaration.accessToken}`,\n },\n body: JSON.stringify(payload),\n },\n );\n },\n});\n\nconst logWhatsAppDriver = (declaration: WhatsAppDriverDeclaration): WhatsAppDriver => ({\n // Deliberately logs NO destination — a phone number is PII and must never\n // reach logs; the message text alone is enough for local debugging.\n send(_toE164, message) {\n declaration.logger?.info(\n `[notifications:whatsapp] log driver suppressed a real send (text=\"${message.text}\")`,\n );\n return Promise.resolve();\n },\n});\n\nexport const WHATSAPP_DRIVERS: Record<\n string,\n (declaration: WhatsAppDriverDeclaration) => WhatsAppDriver\n> = {\n meta: metaDriver,\n log: logWhatsAppDriver,\n};\n\n/** Agnostic content → WhatsApp text + template parameters. */\nexport function formatWhatsApp(\n content: NotificationContent,\n declaration: WhatsAppDriverDeclaration,\n): WhatsAppMessage {\n const lines = [`*${content.title}*`, '', content.body];\n const href = absoluteLink(content.link, declaration.appUrl);\n if (href) lines.push('', href);\n return { text: lines.join('\\n'), templateParameters: [content.title, content.body] };\n}\n\nexport function whatsAppTransport(\n declaration: WhatsAppDriverDeclaration,\n extraDrivers: Record<string, (d: WhatsAppDriverDeclaration) => WhatsAppDriver> = {},\n logger?: NotificationLogger,\n): NotificationTransport<WhatsAppMessage> {\n const driver = resolveDriver('WHATSAPP', declaration, {\n ...WHATSAPP_DRIVERS,\n ...extraDrivers,\n });\n if (!declaration.templateName) {\n // Not a throw: free-form IS correct for a host that only replies inside the\n // 24h window. It is a warning because the other reading — business-initiated\n // alerts with no template — is a channel that never delivers anything.\n logger?.error(\n '[notifications] WHATSAPP is declared with no `templateName`: only free-form ' +\n 'replies inside the 24h customer-service window will be accepted, and every ' +\n 'business-initiated send will be rejected by the Graph API.',\n );\n }\n return phoneChannel<WhatsAppMessage>('WHATSAPP', {\n defaultCountryCode: declaration.defaultCountryCode,\n format: (content) => formatWhatsApp(content, declaration),\n send: (toE164, message) => driver.send(toE164, message),\n });\n}\n","import type {\n NotificationChannel,\n NotificationLogger,\n NotificationTransport,\n} from '../../types';\n\nimport type { EmailDriver, EmailDriverDeclaration } from './email';\nimport { emailTransport } from './email';\nimport type { SmsDriver, SmsDriverDeclaration } from './sms';\nimport { smsTransport } from './sms';\nimport type { WebPushDriverDeclaration, WebPushSender, WebPushSubscriptionSource } from './web-push';\nimport { webPushTransport } from './web-push';\nimport type { WhatsAppDriver, WhatsAppDriverDeclaration } from './whatsapp';\nimport { whatsAppTransport } from './whatsapp';\n\n/**\n * The transport registry: the router dispatches through this, so adding a\n * channel = registering one adapter and the router, generators and existing\n * transports are untouched (open/closed).\n *\n * A mount declares its channels and gets a registry; nothing is process-wide.\n * The origin registered its four transports as an IMPORT SIDE EFFECT of the\n * package's root entry, which made \"which channels are on\" a property of the\n * module graph rather than of any configuration — importing the inbox helpers\n * in a unit test silently armed four transports.\n */\n\n/** One channel's declaration. The union is closed; the drivers are not. */\nexport type TransportDeclaration =\n | EmailDriverDeclaration\n | SmsDriverDeclaration\n | WhatsAppDriverDeclaration\n | WebPushDriverDeclaration;\n\n/** A host's own vendors, added per channel without touching the package. */\nexport interface ExtraDrivers {\n email?: Record<string, (declaration: EmailDriverDeclaration) => EmailDriver>;\n sms?: Record<string, (declaration: SmsDriverDeclaration) => SmsDriver>;\n whatsapp?: Record<string, (declaration: WhatsAppDriverDeclaration) => WhatsAppDriver>;\n webPush?: Record<string, (declaration: WebPushDriverDeclaration) => WebPushSender>;\n}\n\nexport interface TransportRegistry {\n /** The adapter for `channel`, or null when the host declared none. */\n get(channel: NotificationChannel): NotificationTransport<never> | null;\n /** Every declared adapter, in declaration order. */\n list(): NotificationTransport<never>[];\n /** Register (or replace, last-wins) an adapter built by the host itself. */\n register<TMessage>(transport: NotificationTransport<TMessage>): void;\n /** The VAPID public key, when the WEB_PUSH channel declared one. */\n webPushPublicKey(): string | null;\n}\n\nfunction build(\n declaration: TransportDeclaration,\n subscriptions: WebPushSubscriptionSource,\n extra: ExtraDrivers,\n logger?: NotificationLogger,\n): NotificationTransport<never> {\n switch (declaration.channel) {\n case 'EMAIL':\n return emailTransport(declaration, extra.email ?? {}) as NotificationTransport<never>;\n case 'SMS':\n return smsTransport(declaration, extra.sms ?? {}) as NotificationTransport<never>;\n case 'WHATSAPP':\n return whatsAppTransport(\n declaration,\n extra.whatsapp ?? {},\n logger,\n ) as NotificationTransport<never>;\n case 'WEB_PUSH':\n return webPushTransport(\n declaration,\n subscriptions,\n extra.webPush ?? {},\n ) as NotificationTransport<never>;\n }\n}\n\nexport function createTransportRegistry(\n declarations: readonly TransportDeclaration[],\n subscriptions: WebPushSubscriptionSource,\n extra: ExtraDrivers = {},\n /** The mount's logger, for a declaration that is legal but probably wrong. */\n logger?: NotificationLogger,\n): TransportRegistry {\n const transports = new Map<NotificationChannel, NotificationTransport<never>>();\n let publicKey: string | null = null;\n for (const declaration of declarations) {\n if (transports.has(declaration.channel)) {\n throw new Error(\n `@12-apps/notifications: the ${declaration.channel} channel is declared twice.`,\n );\n }\n if (declaration.channel === 'WEB_PUSH') publicKey = declaration.publicKey ?? null;\n transports.set(declaration.channel, build(declaration, subscriptions, extra, logger));\n }\n return {\n get: (channel) => transports.get(channel) ?? null,\n list: () => [...transports.values()],\n register(transport) {\n transports.set(transport.channel, transport as NotificationTransport<never>);\n },\n webPushPublicKey: () => publicKey,\n };\n}\n","import type { NotificationEvent, NotificationLogger } from '../types';\n\nimport type { NotificationRouter } from './router';\n\n/**\n * Permission-addressed notifications: \"tell whoever can act on this\", resolved\n * against the host's REAL authorization engine.\n *\n * Naming an audience by ROLE reads a coarse mirror column, so a tenant who\n * moved a capability onto a custom role — or granted it additively — gets a\n * notification list that disagrees with what the app actually authorizes. The\n * two answers drift silently, and the direction they drift in is \"the person\n * who can fix it never hears about it\".\n *\n * So this addresses by CAPABILITY: name the permissions the recipient must\n * hold, and the audience is derived from the same evaluation the guards use.\n *\n * ## What the package owns, and what the host answers\n *\n * The FOLD is the package's: the AND, the deduplication, the refusal of an\n * empty permission list, the per-recipient isolation, and the log line that\n * distinguishes \"nobody holds it\" from \"everybody's dispatch failed\". Those are\n * the parts that are the same in every host and that are easy to get subtly\n * wrong.\n *\n * The two QUERIES are the host's, through {@link NotificationAudienceDirectory}\n * — because an authorization engine is host machinery. In the extraction origin this\n * module could not live in a package at all: it needed `notify()` AND the RBAC\n * engine, and neither package could see the other. Inverting the dependency\n * (the host answers, the package asks) is what makes it portable.\n */\n\n/**\n * The host's authorization engine, as this fan-out needs it.\n *\n * `listCandidates` must be BOUNDED to people who actually hold a role at the\n * tenant. that host's implementation requires a role grant, which is what\n * keeps a store's storefront BUYERS — who all carry a default membership — out\n * of a loop that resolves permissions one user at a time.\n *\n * `getPermissions` must be scoped to `tenantId`. Unioning a user's grants\n * across tenants — the obvious way to \"simplify\" it — notifies someone about a\n * store whose money they have no authority over, and no `where` clause upstream\n * can save it because that user is already a candidate.\n */\nexport interface NotificationAudienceDirectory {\n listCandidates(tenantId: string): Promise<readonly string[]>;\n getPermissions(\n userId: string,\n tenantId: string,\n ): Promise<ReadonlySet<string> | readonly string[]>;\n}\n\n/**\n * One candidate that did not receive it, and why.\n *\n * `audience-error` is deliberately its own reason rather than folded into\n * `missing-permission`: \"this user does not hold the pair\" is a configuration\n * fact, while \"we could not find out whether they hold it\" is an outage, and\n * they need opposite responses. Collapsing them would report a database timeout\n * as a tenant that simply has nobody to tell.\n */\nexport interface PermissionNotificationSkip {\n userId: string;\n reason: 'missing-permission' | 'dispatch-failed' | 'audience-error';\n}\n\n/**\n * What one fan-out actually did. Returned rather than logged-and-forgotten so a\n * caller (and a test) can assert on the OUTCOME — who was reached and who was\n * not — without reaching into transport mocks to infer it.\n */\nexport interface PermissionNotificationResult {\n /** User ids whose notification committed, in candidate order. */\n notified: string[];\n /** Candidates that did not receive it, with the reason. */\n skipped: PermissionNotificationSkip[];\n}\n\n/** What {@link reportOutcome} needs to describe one fan-out. */\ninterface OutcomeReport {\n type: string;\n clientId: string;\n permissions: readonly string[];\n candidateCount: number;\n result: PermissionNotificationResult;\n}\n\n/**\n * Log what the fan-out amounted to, keyed on WHY nobody was reached rather\n * than on the fact that nobody was.\n *\n * The distinction is the whole point: \"this tenant has nobody holding the pair\"\n * is a configuration fact and belongs at info, while \"three people hold it and\n * every dispatch failed\" is an outage in which nobody was told about missing\n * money. Both leave `notified` empty, so keying on that alone would file the\n * second under the first and make a dead transport look like an unconfigured\n * store.\n */\nfunction reportOutcome(logger: NotificationLogger, report: OutcomeReport): void {\n const { type, clientId, permissions, candidateCount, result } = report;\n // Both non-configuration reasons count as \"not reached\": a candidate whose\n // authorization query threw is as un-notified as one whose dispatch threw.\n const failed = result.skipped.filter((skip) => skip.reason !== 'missing-permission').length;\n if (failed > 0) {\n logger.error(\n `[notifications] ${type}: ${failed} of ${failed + result.notified.length} matching ` +\n `recipient(s) at client ${clientId} could not be reached; ` +\n `${result.notified.length} delivered`,\n );\n return;\n }\n if (result.notified.length > 0) return;\n // Quiet, and deliberately at info: \"nobody at this tenant holds\n // `orders:refund` + `reports:financial:read`\" is worth being able to look up\n // when someone asks why they heard nothing, but it is not a fault.\n logger.info(\n `[notifications] ${type}: no recipient at client ${clientId} holds ` +\n `[${permissions.join(', ')}] (${candidateCount} candidate(s) evaluated)`,\n );\n}\n\nexport type NotifyByPermission = <TPayload>(\n clientId: string,\n permissions: readonly string[],\n event: Omit<NotificationEvent<TPayload>, 'recipient'>,\n) => Promise<PermissionNotificationResult>;\n\n/**\n * Send one notification to every user of `clientId` holding ALL of\n * `permissions` (AND, not OR).\n *\n * AND is the semantics that makes an addressed notification openable: pair the\n * permission that HANDLES the thing with the permission that gates the SURFACE\n * it links to, and every addressee is, by construction, someone the surface's\n * own guard will serve. OR would happily notify someone into a 403.\n *\n * Contract:\n * - `permissions` must be non-empty. An empty list is a PROGRAMMING ERROR and\n * throws: \"holds every permission in []\" is vacuously true for everyone, so\n * the quiet reading of an empty list is \"fan out to the entire tenant\" —\n * the one outcome a caller can never have meant. It is the only throw here.\n * - TENANT-SCOPED, unconditionally. Permissions are evaluated at `clientId`\n * and the inbox row is written with that same tenant.\n * - Recipients are DEDUPLICATED: candidates are a set, and holding the\n * permissions through two roles is one notification.\n * - Zero matching users is a NORMAL outcome — logged, never thrown.\n * - Recipients are ISOLATED, in BOTH host queries: the authorization lookup\n * and the dispatch each get their own try/catch, so neither a candidate the\n * engine cannot answer for nor a recipient `notify` throws on can cost the\n * others their notification — or cost the caller the outcome log.\n *\n * Cost: one `getPermissions` per candidate, run sequentially. That is only\n * affordable because the candidate set is bounded (see the directory docs).\n * Callers run this fire-and-forget, off whatever path produced the event.\n */\n/** What the two isolated per-candidate steps need. */\ninterface FanOutDeps {\n router: NotificationRouter;\n directory: NotificationAudienceDirectory;\n logger: NotificationLogger;\n}\n\n/**\n * One recipient's dispatch, isolated. Resolves `true` when the notification\n * committed and `false` when it did not — the failure is contained here so the\n * loop can carry on, which is the whole point of per-recipient isolation.\n *\n * The log names the USER ID and never an address: an e-mail in a log is PII.\n */\nasync function dispatchTo<TPayload>(\n deps: FanOutDeps,\n clientId: string,\n userId: string,\n event: Omit<NotificationEvent<TPayload>, 'recipient'>,\n): Promise<boolean> {\n try {\n await deps.router.notify<TPayload>({ ...event, recipient: { userId, clientId } });\n return true;\n } catch (error) {\n deps.logger.error(\n `[notifications] ${event.type} dispatch failed for user ${userId} at client ${clientId}:`,\n error,\n );\n return false;\n }\n}\n\n/**\n * One candidate's permissions, isolated. `null` means the host's engine could\n * not answer.\n *\n * This await used to sit bare in the loop while only `notify` was guarded, which\n * made the documented per-recipient isolation half true: an ordinary\n * connection-pool timeout on candidate #3 of 30 propagated out of\n * `notifyByPermission`, so #4..#30 were never evaluated, `reportOutcome` never\n * ran, and the only trace was a rejection in whatever `.catch` the\n * fire-and-forget caller happened to attach. For a short-payment alert that is\n * nobody being told that money is missing, with nothing in the log saying so.\n */\nasync function heldBy(\n deps: FanOutDeps,\n clientId: string,\n userId: string,\n): Promise<ReadonlySet<string> | null> {\n try {\n const granted = await deps.directory.getPermissions(userId, clientId);\n return granted instanceof Set ? granted : new Set(granted);\n } catch (error) {\n deps.logger.error(\n `[notifications] audience lookup failed for user ${userId} at client ${clientId}:`,\n error,\n );\n return null;\n }\n}\n\nexport function createNotifyByPermission(deps: FanOutDeps): NotifyByPermission {\n return async function notifyByPermission(clientId, permissions, event) {\n if (permissions.length === 0) {\n throw new Error(\n 'notifyByPermission(): `permissions` must be non-empty — ' +\n 'an empty list matches every user of the tenant.',\n );\n }\n const candidates = [...new Set(await deps.directory.listCandidates(clientId))];\n const result: PermissionNotificationResult = { notified: [], skipped: [] };\n\n for (const userId of candidates) {\n const held = await heldBy(deps, clientId, userId);\n if (!held) {\n result.skipped.push({ userId, reason: 'audience-error' });\n continue;\n }\n // THE AND. `held` folds every role the user has at this tenant, so this\n // one line is the whole authorization question — and a single-element\n // list needs no special case.\n if (!permissions.every((permission) => held.has(permission))) {\n result.skipped.push({ userId, reason: 'missing-permission' });\n continue;\n }\n if (await dispatchTo(deps, clientId, userId, event)) result.notified.push(userId);\n else result.skipped.push({ userId, reason: 'dispatch-failed' });\n }\n\n reportOutcome(deps.logger, {\n type: event.type,\n clientId,\n permissions,\n candidateCount: candidates.length,\n result,\n });\n return result;\n };\n}\n","import type {\n DeliveryStatus,\n NotificationChannel,\n NotificationContent,\n NotificationLogger,\n TransportRecipient,\n} from '../types';\n\nimport type {\n NotificationContactDirectory,\n NotificationDeliveryRow,\n NotificationsDb,\n NotificationsDbProvider,\n} from './db';\nimport type { PushSubscriptionStore } from './push-subscriptions';\nimport type { TransportRegistry } from './transports/registry';\n\n/**\n * Handing a delivery to its transport, and the retry sweep that re-hands the\n * ones that did not make it (12-15).\n *\n * ## Every send is CLAIMED first\n *\n * A dispatcher never sends a row it merely READ. It moves the row `QUEUED →\n * SENDING` with one conditional `updateMany` whose `where` carries the\n * precondition, and sends only when that statement reports `count === 1`:\n *\n * updateMany({ where: { id, status: 'QUEUED' }, data: { status: 'SENDING' } })\n *\n * The database decides the winner, in one statement, so two dispatchers racing\n * the same delivery produce exactly one provider call. The version this replaced\n * read the QUEUED rows, called `transport.send`, and only then wrote `SENT` —\n * with nothing marking the row taken in between, which is a read-validate-write\n * with a network round trip in the window. It needed no crash to double-send: a\n * sweep whose backlog outlived its own cron interval overlapped itself, and\n * every row the first run had not yet reached was sent twice. On SMS and\n * WhatsApp that is a second billed message to a real phone.\n *\n * A single-threaded fake cannot tell the two implementations apart, which is why\n * the concurrency contracts are pinned against real SQL in\n * `harness/backend/tests/notifications-pipeline.test.ts` as well as against the\n * in-memory seam.\n *\n * ## Nothing is retried forever\n *\n * Each claim increments `attempts` — at CLAIM time, so a dispatcher that dies\n * mid-send still spends one — and the `maxAttempts`-th failure writes `DEAD`\n * instead of `FAILED`. A `DEAD` row is terminal: no sweep selects it again.\n * Without a ceiling a permanently invalid destination is a billed provider call\n * on every sweep for the life of the row, and the sweep's working set only ever\n * grows.\n *\n * ## The sweep is bounded and cannot un-send\n *\n * It selects on `updatedAt` (never `createdAt`), takes at most `take` rows, and\n * re-queues each one with the same conditional-update shape — status pinned to\n * what was read, plus the staleness predicate. A row another dispatcher has\n * already moved fails that predicate, so a committed `SENT` can never be dragged\n * back to `QUEUED`.\n */\n\n/** Claims a delivery gets before it is DEAD. Overridable per mount. */\nexport const DEFAULT_MAX_DELIVERY_ATTEMPTS = 5;\n\n/** Rows one sweep may take. Bounds a run to well inside a cron interval. */\nexport const DEFAULT_SWEEP_TAKE = 200;\n\n/** Default staleness cutoff: a row that has not moved in five minutes. */\nexport const DEFAULT_SWEEP_CUTOFF_MS = 5 * 60_000;\n\n/**\n * The statuses a sweep may return to QUEUED, once stale.\n *\n * `SENDING` is in the list because that is what a dispatcher that died mid-send\n * leaves behind, and it is safe precisely because of the cutoff: a SENDING row\n * younger than the cutoff belongs to a dispatcher that is still working.\n * `SENT` and `DEAD` are absent, and that is the whole point of them.\n */\nconst RETRYABLE: DeliveryStatus[] = ['FAILED', 'QUEUED', 'SENDING'];\n\n/** What dispatch needs from the mount. */\nexport interface NotificationDispatchDeps {\n db: NotificationsDbProvider;\n transports: TransportRegistry;\n pushSubscriptions: PushSubscriptionStore;\n contacts: NotificationContactDirectory;\n logger: NotificationLogger;\n maxAttempts: number;\n}\n\n/** Load the recipient's destinations once, for every transport's gate. */\nexport async function loadRecipient(\n deps: NotificationDispatchDeps,\n userId: string,\n clientId: string | null,\n): Promise<TransportRecipient | null> {\n const contact = await deps.contacts.getContact(userId);\n if (!contact) return null;\n return {\n userId,\n email: contact.email,\n phone: contact.phone,\n // Carried through UNRESOLVED and only where the host supplied one: the\n // absent case has to stay distinguishable from a stated language, because\n // that is what lets a generator apply its own default in one place.\n ...(contact.locale === undefined ? {} : { locale: contact.locale }),\n clientId,\n // SCOPED, and this is what keeps `supports()` honest: it gates on this\n // number, so an unscoped count would enqueue a WEB_PUSH delivery for a\n // notification no reachable subscription exists for.\n pushSubscriptionCount: await deps.pushSubscriptions.count(userId, clientId),\n };\n}\n\n/** The stored inbox row, back as the agnostic content a formatter takes. */\nfunction contentOf(notification: {\n title: string;\n body: string;\n link: string | null;\n data: unknown;\n}): NotificationContent {\n return {\n title: notification.title,\n body: notification.body,\n ...(notification.link !== null ? { link: notification.link } : {}),\n data: (notification.data ?? {}) as Record<string, unknown>,\n };\n}\n\nconst messageOf = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\n/** Record the outcome of a claimed send — terminal once the ceiling is hit. */\nasync function settle(\n deps: NotificationDispatchDeps,\n client: NotificationsDb,\n delivery: NotificationDeliveryRow,\n error: unknown,\n): Promise<void> {\n // `attempts` was read BEFORE this claim incremented it, so `+ 1` is the\n // attempt that just failed.\n const spent = delivery.attempts + 1;\n const terminal = spent >= deps.maxAttempts;\n await client.notificationDelivery.update({\n where: { id: delivery.id },\n data: {\n status: terminal ? 'DEAD' : 'FAILED',\n error: terminal ? `${messageOf(error)} (gave up after ${spent} attempts)` : messageOf(error),\n },\n });\n}\n\n/**\n * Claim one delivery and, if we won it, send it.\n *\n * Resolves whether a provider call was made, which is what the sweep counts —\n * \"dispatched\" must mean sends attempted, not rows looked at.\n */\nasync function sendClaimed(\n deps: NotificationDispatchDeps,\n client: NotificationsDb,\n delivery: NotificationDeliveryRow,\n content: NotificationContent,\n recipient: TransportRecipient,\n): Promise<boolean> {\n const claimed = await client.notificationDelivery.updateMany({\n where: { id: delivery.id, status: 'QUEUED' },\n data: { status: 'SENDING', attempts: { increment: 1 } },\n });\n // Someone else moved it between our read and here. They own the send now.\n if (claimed.count !== 1) return false;\n\n const transport = deps.transports.get(delivery.channel as NotificationChannel);\n try {\n if (!transport) throw new Error(`No transport declared for ${delivery.channel}.`);\n await transport.send(transport.format(content) as never, recipient);\n await client.notificationDelivery.update({\n where: { id: delivery.id },\n data: { status: 'SENT', sentAt: new Date(), error: null },\n });\n } catch (error) {\n await settle(deps, client, delivery, error);\n }\n return true;\n}\n\n/**\n * The recipient no longer exists, so no channel will ever reach them.\n *\n * The rows are marked DEAD rather than left QUEUED: `getContact` returning null\n * is the host saying \"no such person\" (the same answer `notify` throws on), and\n * a QUEUED row for a deleted account is a row every sweep, forever, picks up and\n * cannot deliver.\n */\nasync function abandonUnreachable(\n deps: NotificationDispatchDeps,\n client: NotificationsDb,\n queued: readonly NotificationDeliveryRow[],\n userId: string,\n): Promise<void> {\n deps.logger.error(\n `[notifications] no contact for user ${userId}: ${queued.length} delivery row(s) marked DEAD`,\n );\n for (const delivery of queued) {\n await client.notificationDelivery.update({\n where: { id: delivery.id },\n data: { status: 'DEAD', error: 'The contact directory no longer knows this recipient.' },\n });\n }\n}\n\n/**\n * Send every still-QUEUED delivery of one notification, claiming each one first.\n *\n * Safe to call repeatedly and safe to call concurrently: SENT, SENDING and DEAD\n * rows are not selected, and of two callers that both read the same QUEUED row\n * only one wins the claim.\n */\nexport async function dispatchOne(\n deps: NotificationDispatchDeps,\n notificationId: string,\n): Promise<number> {\n const client = await deps.db();\n const notification = await client.notification.findUnique({ where: { id: notificationId } });\n if (!notification) return 0;\n const queued = await client.notificationDelivery.findMany({\n where: { notificationId, status: 'QUEUED' },\n });\n if (queued.length === 0) return 0;\n\n // The STORED column, so the retry sweep scopes identically to the first\n // attempt — anything less would let a retry leak what the first send withheld.\n const recipient = await loadRecipient(deps, notification.userId, notification.clientId);\n if (!recipient) {\n await abandonUnreachable(deps, client, queued, notification.userId);\n return 0;\n }\n\n const content = contentOf(notification);\n // Sequential on purpose: one recipient's channels (2–4 sends) gain little\n // from parallelism, and providers rate-limit per sender anyway.\n let sent = 0;\n for (const delivery of queued) {\n if (await sendClaimed(deps, client, delivery, content, recipient)) sent += 1;\n }\n return sent;\n}\n\n/**\n * Return one stale row to QUEUED, or report that somebody else got there first.\n *\n * The `where` is the guard, and every clause of it is load-bearing: `status`\n * pinned to the value we READ means a row that has since been SENT is not\n * dragged back (the unguarded version matched on `id` alone and reverted\n * committed sends, leaving a row claiming QUEUED with a `sent_at`), and\n * `updatedAt < cutoff` means a row a concurrent sweep already re-queued — whose\n * `updated_at` is now — is not re-queued a second time.\n */\nasync function requeue(\n client: NotificationsDb,\n row: NotificationDeliveryRow,\n cutoff: Date,\n): Promise<boolean> {\n const moved = await client.notificationDelivery.updateMany({\n where: { id: row.id, status: row.status as DeliveryStatus, updatedAt: { lt: cutoff } },\n data: { status: 'QUEUED' },\n });\n return moved.count === 1;\n}\n\n/**\n * Retry sweep for a cron/admin trigger: re-dispatch deliveries that have not\n * moved in `olderThanMs`.\n *\n * BOUNDED by `take`, deliberately. Unbounded, a 2 000-row outage backlog is a\n * single run that takes minutes of sequential provider round trips — outliving\n * its own cron interval, so the next tick starts while it is still working. The\n * claim makes that overlap harmless; the bound makes it rare.\n */\nexport async function drainPending(\n deps: NotificationDispatchDeps,\n olderThanMs: number,\n take: number,\n): Promise<{ dispatched: number }> {\n const client = await deps.db();\n const cutoff = new Date(Date.now() - olderThanMs);\n const stale = await client.notificationDelivery.findMany({\n where: { status: { in: RETRYABLE }, updatedAt: { lt: cutoff } },\n orderBy: { updatedAt: 'asc' },\n take,\n });\n\n const notificationIds = new Set<string>();\n for (const row of stale) {\n if (await requeue(client, row, cutoff)) notificationIds.add(row.notificationId);\n }\n\n let dispatched = 0;\n for (const id of notificationIds) {\n // Isolated per notification: an unreachable host seam on one of them must\n // not cost every later row in the batch its retry.\n try {\n dispatched += await dispatchOne(deps, id);\n } catch (error) {\n deps.logger.error(`[notifications] sweep failed to dispatch ${id}:`, error);\n }\n }\n return { dispatched };\n}\n","import { inboxWire, type ListNotificationsResult } from '../wire';\n\nimport type {\n NotificationDelegate,\n NotificationPageAfter,\n NotificationsDbProvider,\n NotificationWhere,\n NotificationWhereBranch,\n} from './db';\n\n/**\n * Notification-centre inbox reads/writes. Every function is scoped to the\n * OWNER's `userId` — a caller can only ever see or touch their own rows (the\n * route layer supplies the authenticated user's id, never a client value).\n * Soft-deleted rows (`deletedAt` set) are excluded from every read and can\n * never be resurrected by mark-read.\n */\n\n/**\n * The store whose ORIGIN the caller is reading from, or absent for the platform\n * origin.\n *\n * A host that installs one storefront per store as its own PWA reads this from\n * the request's hostname; a host with one origin never sets it and every read\n * below is exactly what it was. Set, it narrows to that store's rows PLUS the\n * platform-wide ones (`clientId IS NULL`) — a password reset or a security\n * notice is about the person and not about a store, so hiding it inside the\n * only app a customer opens would be a worse failure than the leak this fixes.\n */\nexport type NotificationScope = string | undefined;\n\n/**\n * `clientId IN (<scope>, NULL)`, as a filter branch — or nothing at all.\n *\n * A disjunction rather than an `in`, because SQL `IN` never matches NULL and\n * the NULL rows are precisely the ones that must survive every scope.\n */\nfunction scopeBranch(scope: NotificationScope): NotificationWhereBranch[] {\n return scope === undefined ? [] : [{ OR: [{ clientId: scope }, { clientId: null }] }];\n}\n\nexport interface ListNotificationsInput {\n /** `unread` narrows to unread rows; default lists all non-deleted. */\n filter?: 'all' | 'unread';\n /**\n * Cursor = the `id` of the last item of the previous page. Resolved to a\n * KEYSET position, so a row the user soft-deleted between the two requests —\n * routinely the bottom one, since that is the row with the delete button —\n * still anchors the next page instead of costing it a row. Owner-checked: an\n * id that is not the caller's names no position and answers an empty page.\n */\n cursor?: string;\n /** Page size (server-clamped 1..100, default 20). */\n limit?: number;\n}\n\nconst DEFAULT_PAGE = 20;\nconst MAX_PAGE = 100;\n\nexport interface NotificationInboxStore {\n list(\n userId: string,\n input?: ListNotificationsInput,\n scope?: NotificationScope,\n ): Promise<ListNotificationsResult>;\n /** Scoped with `list`, or the badge and the list it sits over disagree. */\n unreadCount(userId: string, scope?: NotificationScope): Promise<number>;\n markRead(userId: string, ids: readonly string[]): Promise<number>;\n /**\n * Scoped too, and this one is a WRITE.\n *\n * Unscoped, \"mark all as read\" pressed inside store A's app clears store B's\n * unread rows everywhere — a cross-store write from the app that exists to be\n * isolated, and strictly worse than the read leak. `markRead(ids)` and\n * `softDelete(ids)` need nothing: their ids come from the already-scoped list.\n *\n * The platform-wide rows ARE cleared from any origin, and that follows from\n * the scope rule rather than contradicting it — those rows are one person's,\n * not one store's.\n */\n markAllRead(userId: string, scope?: NotificationScope): Promise<number>;\n softDelete(userId: string, ids: readonly string[]): Promise<number>;\n}\n\n/**\n * Resolve a cursor into a keyset anchor, or refuse it.\n *\n * OWNERSHIP-CHECKED, which the positional cursor never was: `cursor` is a raw\n * client value, and while the `where` kept the ROWS the caller's own, the\n * anchor's position leaked the `created_at` of whatever row the id named.\n * `undefined` here means \"this cursor names no position in your list\" and the\n * caller answers an empty page — the anchor is not `deletedAt`-filtered, so the\n * only way to reach that is a foreign or invented id.\n */\nasync function resolveAnchor(\n notifications: NotificationDelegate,\n userId: string,\n cursor: string,\n): Promise<NotificationPageAfter | undefined> {\n const anchor = await notifications.findUnique({ where: { id: cursor } });\n if (!anchor || anchor.userId !== userId) return undefined;\n return { createdAt: anchor.createdAt, id: anchor.id };\n}\n\n/** The whole read filter for one page: owner, live, filter, page boundary. */\nfunction pageWhere(\n userId: string,\n filter: ListNotificationsInput['filter'],\n anchor: NotificationPageAfter | undefined,\n scope: NotificationScope,\n): NotificationWhere {\n // Two DISJUNCTIONS have to hold at once — the page boundary and the store\n // scope — so they are AND-ed rather than merged. A second `OR` key on this\n // object literal would overwrite the first, dropping either the scope or the\n // anchor; the anchor only from page TWO onward, which is the case a cursor\n // exists for and the case a single-page test never reaches.\n const clauses: NotificationWhereBranch[] = [\n // `(createdAt, id) < (anchor.createdAt, anchor.id)`, as a portable `where`.\n ...(anchor\n ? [\n {\n OR: [\n { createdAt: { lt: anchor.createdAt } },\n { createdAt: anchor.createdAt, id: { lt: anchor.id } },\n ],\n },\n ]\n : []),\n ...scopeBranch(scope),\n ];\n return {\n userId,\n deletedAt: null,\n ...(filter === 'unread' ? { readAt: null } : {}),\n ...(clauses.length > 0 ? { AND: clauses } : {}),\n };\n}\n\n/**\n * The unread rows this scope can see — READ by the badge and WRITTEN by \"mark\n * all\". One filter for both on purpose: the count and the write have to agree\n * about which rows are in scope, and two copies of the same object literal is\n * how a badge ends up saying 3 over a list of 2.\n */\nfunction unreadWhere(userId: string, scope: NotificationScope): NotificationWhere {\n const scoped = scopeBranch(scope);\n return {\n userId,\n deletedAt: null,\n readAt: null,\n ...(scoped.length > 0 ? { AND: scoped } : {}),\n };\n}\n\nexport function createInboxStore(db: NotificationsDbProvider): NotificationInboxStore {\n return {\n /** The owner's inbox, newest first, keyset-paginated, deleted excluded. */\n async list(userId, input = {}, scope) {\n const client = await db();\n const limit = Math.min(Math.max(input.limit ?? DEFAULT_PAGE, 1), MAX_PAGE);\n const anchor = input.cursor\n ? await resolveAnchor(client.notification, userId, input.cursor)\n : undefined;\n if (input.cursor && !anchor) return { items: [], nextCursor: null };\n const rows = await client.notification.findMany({\n where: pageWhere(userId, input.filter, anchor, scope),\n // `id` tie-breaks equal timestamps so pages never skip/repeat.\n orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],\n take: limit + 1,\n });\n const page = rows.slice(0, limit);\n return {\n items: page.map(inboxWire),\n nextCursor: rows.length > limit ? (page[page.length - 1]?.id ?? null) : null,\n };\n },\n\n /** Unread badge count (non-deleted, unread). */\n async unreadCount(userId, scope) {\n const client = await db();\n return client.notification.count({ where: unreadWhere(userId, scope) });\n },\n\n /**\n * Mark specific notifications read. Only the owner's own, still-unread,\n * non-deleted rows are touched — foreign or already-read ids are silently\n * ignored (idempotent). Returns how many rows flipped.\n */\n async markRead(userId, ids) {\n if (ids.length === 0) return 0;\n const client = await db();\n const result = await client.notification.updateMany({\n where: { id: { in: [...ids] }, userId, deletedAt: null, readAt: null },\n data: { readAt: new Date() },\n });\n return result.count;\n },\n\n /** Mark every unread notification of the owner read (\"mark all\"). */\n async markAllRead(userId, scope) {\n const client = await db();\n const result = await client.notification.updateMany({\n where: unreadWhere(userId, scope),\n data: { readAt: new Date() },\n });\n return result.count;\n },\n\n /**\n * Soft-delete notifications (single or bulk): stamps `deletedAt` so the\n * rows drop out of every list/count forever, while the delivery audit\n * trail under them survives. Owner-scoped and idempotent like mark-read.\n */\n async softDelete(userId, ids) {\n if (ids.length === 0) return 0;\n const client = await db();\n const result = await client.notification.updateMany({\n where: { id: { in: [...ids] }, userId, deletedAt: null },\n data: { deletedAt: new Date() },\n });\n return result.count;\n },\n };\n}\n","import {\n DEFAULT_CHANNEL_ROW,\n explicitChoicesOf,\n mergeStoredRow,\n resolveTypeChannels,\n type ChannelMatrix,\n type ChannelRow,\n type TypeChannelRules,\n} from '../preferences-core';\nimport type {\n NotificationCategory,\n NotificationChannel,\n NotificationTaxonomy,\n} from '../types';\n\nimport type { NotificationsDbProvider } from './db';\n\n/**\n * Per-user channel preferences: which transport channels may carry each\n * notification category to a user. The inbox is NOT gated here — it is always\n * on.\n *\n * The POLICY (defaults, coercion, merge) lives in `../preferences-core.ts`;\n * this is the storage over it, and the split is what lets the react half render\n * the same defaults before the first read lands.\n */\n\nexport interface NotificationPreferenceStore {\n /** The user's full matrix, defaults merged in. */\n get(userId: string): Promise<ChannelMatrix>;\n /** Persist explicit choices for any subset of categories/toggles. */\n save(\n userId: string,\n input: Partial<Record<NotificationCategory, Partial<ChannelRow>>>,\n ): Promise<void>;\n /**\n * The channels enabled for one (user, category) — the router's gate.\n *\n * `rules` carries the emitting TYPE's own declarations (availability and\n * per-type defaults). It is OPTIONAL, and that is what keeps a host store\n * written before it working: a two-parameter implementation is assignable to\n * this signature unchanged, and omitting the argument asks the same question\n * the store has always answered. The router does not rely on a store\n * honouring it — it caps the result by availability itself.\n */\n enabledChannels(\n userId: string,\n category: NotificationCategory,\n rules?: TypeChannelRules,\n ): Promise<NotificationChannel[]>;\n}\n\nexport function createPreferenceStore(\n db: NotificationsDbProvider,\n taxonomy: NotificationTaxonomy,\n channelDefaults: Partial<ChannelRow> = {},\n): NotificationPreferenceStore {\n const defaultRow: ChannelRow = { ...DEFAULT_CHANNEL_ROW, ...channelDefaults };\n const known = new Set(taxonomy.categories);\n\n return {\n async get(userId) {\n const client = await db();\n const rows = await client.notificationPreference.findMany({ where: { userId } });\n const stored = new Map(rows.map((row) => [row.category, row.channels]));\n return Object.fromEntries(\n taxonomy.categories.map((category) => [\n category,\n // A category with no row, or a row missing a channel key, falls back\n // to the defaults — so a NEW channel ships without a data migration.\n mergeStoredRow(stored.get(category), defaultRow),\n ]),\n ) as ChannelMatrix;\n },\n\n /**\n * Only the categories present on `input` are written; within a category,\n * the toggles are merged over the user's CURRENT effective row (their\n * stored choices, or the defaults when none) — so a single-toggle save\n * (how the settings UI writes) never resets the category's other channels\n * back to their defaults.\n *\n * A category outside the taxonomy is IGNORED rather than stored: the DB\n * CHECK would reject it anyway, and a 500 from a stale client's extra key\n * would fail the whole save including the toggle the user did flip.\n *\n * What is written is the user's EXPLICIT choices only, never their\n * effective row. Merging onto the effective row wrote a boolean for all\n * four channels the moment anyone touched any switch, so the row could\n * never again say \"no opinion\" about a channel — which silently disabled\n * every per-type and per-host default for that user, and defeated the\n * missing-key fallback that lets a new channel ship without a data\n * migration.\n */\n async save(userId, input) {\n const client = await db();\n for (const [category, choices] of Object.entries(input)) {\n if (!choices || !known.has(category)) continue;\n const existing = await client.notificationPreference.findUnique({\n where: { userId_category: { userId, category } },\n });\n const channels = { ...explicitChoicesOf(existing?.channels), ...choices };\n await client.notificationPreference.upsert({\n where: { userId_category: { userId, category } },\n create: { userId, category, channels },\n update: { channels },\n });\n }\n },\n\n async enabledChannels(userId, category, rules) {\n const client = await db();\n const row = await client.notificationPreference.findUnique({\n where: { userId_category: { userId, category } },\n });\n return resolveTypeChannels({\n stored: row?.channels,\n categoryDefaults: defaultRow,\n rules,\n });\n },\n };\n}\n","import type { NotificationLogger } from '../types';\n\nimport type { NotificationsDbProvider, PushSubscriptionWhere } from './db';\nimport type { WebPushSubscriptionSource } from './transports/web-push';\n\n/**\n * Browser push subscription registry — the write side of the Web Push\n * destination. The client obtains a `PushSubscription` from\n * `PushManager.subscribe()` (using the VAPID public key) and posts it here;\n * unsubscribe removes it by endpoint. All owner-scoped.\n */\n\n/** What `PushSubscription.toJSON()` yields in the browser. */\nexport interface PushSubscriptionInput {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n /** Optional browser/device hint for a device list. */\n userAgent?: string;\n /**\n * Which ORIGIN this browser subscribed on — the HOST's resolved\n * value, never anything the caller sent. Absent/null = the platform origin.\n */\n clientId?: string | null;\n}\n\n/**\n * Which of a user's subscriptions one notification may reach.\n *\n * Stated as a rule on the SUBSCRIPTION rather than as a fallback, because the\n * fallback form (\"the platform's rows only when the store's are absent\")\n * silently changes the platform origin's behaviour, and a marketplace host must\n * be unaffected by this change:\n *\n * | the subscription was registered on | it receives |\n * |---|---|\n * | the PLATFORM origin (`client_id IS NULL`) | every notification — as today |\n * | store X's origin | store X's, and platform-wide ones (`clientId IS NULL`) |\n *\n * Nothing ever reaches a subscription registered on a DIFFERENT store's origin.\n * It follows without a special case that a store-B notification reaches a\n * customer who installed only store A through their PLATFORM subscription, and\n * through neither app.\n *\n * `notificationClientId` is the NOTIFICATION's tenant. `undefined` (nobody\n * asked to narrow) and `null` (a platform-wide notification) both mean every\n * row, which is why they share a branch.\n */\nfunction reachableBy(\n userId: string,\n notificationClientId?: string | null,\n): PushSubscriptionWhere {\n if (notificationClientId === undefined || notificationClientId === null) return { userId };\n return { userId, OR: [{ clientId: null }, { clientId: notificationClientId }] };\n}\n\nexport interface PushSubscriptionStore extends WebPushSubscriptionSource {\n /**\n * Register (or refresh) one browser's subscription. Upserts on the globally\n * unique endpoint, so re-subscribing the same browser never duplicates — and\n * an endpoint recycled to a different signed-in user is re-owned by them.\n *\n * Re-owning is the right call and the alternative is worse: `PushManager`\n * returns the SAME endpoint for the same browser profile, so one row per\n * `(userId, endpoint)` would push user A's notifications to a browser now used\n * by B with B's own keys — which decrypt. Re-owning costs A their channel;\n * keeping both rows costs A their privacy. What re-owning must NOT do is\n * happen unrecorded, hence the warning.\n */\n save(userId: string, input: PushSubscriptionInput): Promise<void>;\n /** Remove one browser's subscription (owner-scoped; unknown = no-op). */\n remove(userId: string, endpoint: string): Promise<void>;\n /**\n * How many devices the user has registered.\n *\n * TWO callers with different questions. The settings screen asks unscoped —\n * \"you have 2 devices\" is a fact about the PERSON — and passes nothing. The\n * dispatch path passes the notification's tenant, because this is what\n * `supports()` gates on: left unscoped there, the router would enqueue a\n * WEB_PUSH delivery for a notification no reachable subscription exists for,\n * `send` would find an empty list and throw, and the sweep would burn every\n * attempt before writing DEAD — a FAILED row for something that was never\n * undeliverable, only out of scope.\n */\n count(userId: string, notificationClientId?: string | null): Promise<number>;\n /**\n * Whether THIS endpoint is currently registered to THIS user.\n *\n * The settings screen needs it because a browser's own subscription object is\n * not evidence that the server still has the row: a re-own or a 404/410 prune\n * removes the row while the browser keeps the subscription, and a screen that\n * reads only the browser then tells a user they are receiving alerts they will\n * never get again. `false` covers both \"no such row\" and \"somebody else's\n * row\", so an endpoint the caller does not own reveals nothing about who does.\n */\n isRegisteredTo(userId: string, endpoint: string): Promise<boolean>;\n}\n\nexport function createPushSubscriptionStore(\n db: NotificationsDbProvider,\n logger?: NotificationLogger,\n): PushSubscriptionStore {\n return {\n async save(userId, input) {\n const client = await db();\n const existing = await client.pushSubscription.findUnique({\n where: { endpoint: input.endpoint },\n });\n if (existing && existing.userId !== userId) {\n // No endpoint in the message: a push endpoint is a bearer capability for\n // that browser and must not reach logs. The two user ids are what makes\n // \"user X stopped getting web push on a shared machine\" answerable.\n logger?.error(\n `[notifications] push endpoint re-owned: user ${existing.userId} lost this ` +\n `browser's subscription to user ${userId}`,\n );\n }\n await client.pushSubscription.upsert({\n where: { endpoint: input.endpoint },\n create: {\n userId,\n endpoint: input.endpoint,\n p256dh: input.keys.p256dh,\n auth: input.keys.auth,\n clientId: input.clientId ?? null,\n userAgent: input.userAgent ?? null,\n },\n // Re-stamped on every save, so the same browser moving between a store's\n // app and the platform corrects its own scope rather than keeping the\n // first origin it ever subscribed from. A scope change on the SAME user\n // is not a re-own and must not log one.\n update: {\n userId,\n p256dh: input.keys.p256dh,\n auth: input.keys.auth,\n clientId: input.clientId ?? null,\n userAgent: input.userAgent ?? null,\n },\n });\n },\n\n async remove(userId, endpoint) {\n const client = await db();\n await client.pushSubscription.deleteMany({ where: { userId, endpoint } });\n },\n\n async count(userId, notificationClientId) {\n const client = await db();\n return client.pushSubscription.count({ where: reachableBy(userId, notificationClientId) });\n },\n\n async isRegisteredTo(userId, endpoint) {\n const client = await db();\n const row = await client.pushSubscription.findUnique({ where: { endpoint } });\n return row?.userId === userId;\n },\n\n async list(userId, notificationClientId) {\n const client = await db();\n const rows = await client.pushSubscription.findMany({\n where: reachableBy(userId, notificationClientId),\n });\n return rows.map((row) => ({\n id: row.id,\n endpoint: row.endpoint,\n p256dh: row.p256dh,\n auth: row.auth,\n }));\n },\n\n async prune(id) {\n const client = await db();\n await client.pushSubscription.delete({ where: { id } });\n },\n };\n}\n","import {\n messagesOf,\n type NotificationsCopySource,\n type NotificationWireMessages,\n} from '../messages';\nimport { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';\n\nimport {\n guarded,\n ok,\n parseDeleteBody,\n parseListQuery,\n parseMarkReadBody,\n parsePreferencesBody,\n parsePushEndpointBody,\n parsePushEndpointQuery,\n parsePushSubscriptionBody,\n type NotificationsRoute,\n} from './context';\nimport type { NotificationContactDirectory } from './db';\nimport type { NotificationInboxStore } from './inbox';\nimport type { NotificationPreferenceStore } from './preferences';\nimport type { PushSubscriptionStore } from './push-subscriptions';\nimport type { TransportRegistry } from './transports/registry';\n\n/**\n * The endpoints, as framework-neutral descriptors (12-15).\n *\n * Nine routes, and the paths are the PACKAGE's: the shipped react client\n * builds these URLs, so a host that renamed one would be a host whose own bell\n * stopped working. The host names only where the whole block is mounted\n * (the origin mounts it at `/api/account`).\n *\n * Route ORDER is preserved by every adapter. Nothing here is shaped `/:id`, so\n * no sibling can capture a literal — but the order is still the contract,\n * because that is what a host mounting an `/:id` route of its own under the\n * same prefix has to reason about.\n */\n\ninterface NotificationRoutesDeps {\n inbox: NotificationInboxStore;\n preferences: NotificationPreferenceStore;\n pushSubscriptions: PushSubscriptionStore;\n transports: TransportRegistry;\n contacts: NotificationContactDirectory;\n categories: readonly string[];\n /**\n * The SOURCE, not a resolved pack — the route table is built once per\n * process and every handler below runs per request.\n */\n messages: NotificationsCopySource<NotificationWireMessages>;\n /** Told when a write actually changed something (for a realtime hint). */\n onInboxChanged?: (userId: string) => void;\n}\n\n/**\n * Whether each channel CAN reach this user right now — destination on file and\n * the channel declared — so the settings UI can disable dead toggles with a\n * hint (no phone → SMS/WhatsApp off).\n *\n * Web push is probed with a HYPOTHETICAL subscription: for the SETTINGS screen\n * the channel is \"available\" when the platform can send at all, because the\n * browser subscribe step happens right from that toggle. Requiring an existing\n * subscription here would deadlock the UX.\n */\nasync function channelAvailability(\n deps: NotificationRoutesDeps,\n userId: string,\n scopeClientId: string | null,\n): Promise<Record<NotificationChannel, boolean>> {\n const contact = await deps.contacts.getContact(userId);\n const recipient = {\n userId,\n email: contact?.email ?? null,\n phone: contact?.phone ?? null,\n clientId: scopeClientId,\n pushSubscriptionCount: 1,\n };\n return Object.fromEntries(\n NOTIFICATION_CHANNELS.map((channel) => [\n channel,\n deps.transports.get(channel)?.supports(recipient) ?? false,\n ]),\n ) as Record<NotificationChannel, boolean>;\n}\n\n/** The `{ preferences, availability, categories }` payload both prefs routes answer. */\nasync function preferencesPayload(\n deps: NotificationRoutesDeps,\n userId: string,\n scopeClientId: string | null,\n): Promise<{\n preferences: Record<string, Record<string, boolean>>;\n availability: Record<NotificationChannel, boolean>;\n categories: string[];\n}> {\n const [preferences, availability] = await Promise.all([\n deps.preferences.get(userId),\n channelAvailability(deps, userId, scopeClientId),\n ]);\n // `categories` travels with the matrix so the settings screen renders the\n // HOST's taxonomy without being told it twice (once in the api config, once\n // in the web config) — the two could then disagree.\n return { preferences, availability, categories: [...deps.categories] };\n}\n\nfunction inboxRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {\n return [\n {\n method: 'GET',\n path: '/notifications',\n handle: guarded(async ({ actor, query, locale }) =>\n ok(\n await deps.inbox.list(\n actor.userId,\n parseListQuery(query, messagesOf(deps, locale)),\n actor.scopeClientId,\n ),\n ),\n ),\n },\n {\n method: 'GET',\n path: '/notifications/unread-count',\n handle: guarded(async ({ actor }) =>\n // Polled by the SPAs, so it stays a single indexed COUNT.\n ok({ count: await deps.inbox.unreadCount(actor.userId, actor.scopeClientId) }),\n ),\n },\n {\n method: 'POST',\n path: '/notifications/mark-read',\n handle: guarded(async ({ actor, body, locale }) => {\n const target = parseMarkReadBody(body, messagesOf(deps, locale));\n const updated =\n 'all' in target\n ? await deps.inbox.markAllRead(actor.userId, actor.scopeClientId)\n : await deps.inbox.markRead(actor.userId, target.ids);\n // Only when something actually flipped. This endpoint is idempotent, so\n // a re-send of an already-read id reports `updated: 0` and has changed\n // nothing — hinting on that would wake every one of this user's devices\n // to re-read a badge that did not move.\n if (updated > 0) deps.onInboxChanged?.(actor.userId);\n return ok({ updated });\n }),\n },\n {\n method: 'POST',\n // POST, not DELETE, because the ids travel in a JSON body.\n path: '/notifications/delete',\n handle: guarded(async ({ actor, body, locale }) => {\n const deleted = await deps.inbox.softDelete(\n actor.userId,\n parseDeleteBody(body, messagesOf(deps, locale)),\n );\n // Same rule as mark-read. A delete can move the badge too — an UNREAD\n // row that is removed takes its place in the count with it.\n if (deleted > 0) deps.onInboxChanged?.(actor.userId);\n return ok({ deleted });\n }),\n },\n ];\n}\n\nfunction preferenceRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {\n return [\n {\n method: 'GET',\n path: '/notification-preferences',\n handle: guarded(async ({ actor }) =>\n ok(await preferencesPayload(deps, actor.userId, actor.scopeClientId ?? null)),\n ),\n },\n {\n method: 'PUT',\n path: '/notification-preferences',\n handle: guarded(async ({ actor, body, locale }) => {\n // The dispatch pipeline reads these on every emit, so a save takes\n // effect immediately — no cache to invalidate.\n await deps.preferences.save(\n actor.userId,\n parsePreferencesBody(body, messagesOf(deps, locale)),\n );\n return ok(await preferencesPayload(deps, actor.userId, actor.scopeClientId ?? null));\n }),\n },\n ];\n}\n\nfunction pushRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {\n return [\n {\n method: 'GET',\n path: '/push-subscriptions',\n handle: guarded(async ({ actor, query, locale }) => {\n const endpoint = parsePushEndpointQuery(query, messagesOf(deps, locale));\n return ok({\n // null = web push is not configured on this deployment.\n vapidPublicKey: deps.transports.webPushPublicKey(),\n count: await deps.pushSubscriptions.count(actor.userId),\n // Only when asked. `registered` is what lets the settings screen stop\n // trusting the browser alone: a re-owned or pruned row answers false,\n // so the screen offers *Ativar* again instead of claiming all is well.\n ...(endpoint !== undefined\n ? { registered: await deps.pushSubscriptions.isRegisteredTo(actor.userId, endpoint) }\n : {}),\n });\n }),\n },\n {\n method: 'POST',\n path: '/push-subscriptions',\n handle: guarded(async ({ actor, body, headers, locale }) => {\n const input = parsePushSubscriptionBody(body, messagesOf(deps, locale));\n const userAgent = headers?.['user-agent'];\n await deps.pushSubscriptions.save(actor.userId, {\n // The HOST's resolved origin, never the body's — a caller cannot\n // choose which store's app their browser counts as.\n clientId: actor.scopeClientId ?? null,\n ...input,\n ...(userAgent ? { userAgent } : {}),\n });\n return ok({ count: await deps.pushSubscriptions.count(actor.userId) });\n }),\n },\n {\n method: 'DELETE',\n // The endpoint is a long opaque URL, unusable as a path param.\n path: '/push-subscriptions',\n handle: guarded(async ({ actor, body, locale }) => {\n await deps.pushSubscriptions.remove(\n actor.userId,\n parsePushEndpointBody(body, messagesOf(deps, locale)),\n );\n return ok({ count: await deps.pushSubscriptions.count(actor.userId) });\n }),\n },\n ];\n}\n\nexport function notificationRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {\n return [...inboxRoutes(deps), ...preferenceRoutes(deps), ...pushRoutes(deps)];\n}\n","import { UnknownNotificationRecipientError } from '../errors';\nimport type { NotificationGeneratorRegistry } from '../generators';\nimport { capToAvailable, DEFAULT_CHANNEL_ROW, enabledChannelsOf } from '../preferences-core';\nimport type {\n NotificationChannel,\n NotificationEvent,\n NotificationGenerator,\n TransportRecipient,\n} from '../types';\n\nimport {\n dispatchOne,\n drainPending,\n loadRecipient,\n DEFAULT_SWEEP_CUTOFF_MS,\n DEFAULT_SWEEP_TAKE,\n type NotificationDispatchDeps,\n} from './dispatch';\nimport type { NotificationPreferenceStore } from './preferences';\n\n/**\n * The channel router + the `notify` emit API — the single front door into the\n * pipeline. Any server-side caller (route handler, background worker, agent\n * tool) emits with one typed call and zero knowledge of channels, formatting,\n * or preferences:\n *\n * await notifications.notify({ type: 'order.paid', recipient: { userId }, payload });\n *\n * What one emit does:\n * 1. Resolves the registered generator for `type` → agnostic content.\n * 2. ALWAYS writes the inbox record (the always-on channel), atomically\n * with…\n * 3. …one QUEUED delivery per channel that is (a) enabled by the recipient's\n * preferences for the generator's category and (b) supported by its\n * transport for this recipient.\n * 4. Hands the deliveries to the transports ASYNCHRONOUSLY (fire-and-forget\n * by default) so emit sites never block on provider I/O.\n *\n * The TRANSACTION IS THIS PACKAGE'S OWN, and a host cannot enlist in it: step 2\n * opens `client.$transaction` itself, and a Prisma `TransactionClient` has no\n * `$transaction` to nest. So `notify` must be called AFTER the caller's own\n * transaction commits — called from inside one, it commits an inbox row and\n * dispatches an e-mail for a payment that then rolls back.\n *\n * Failure isolation: each delivery is sent in its own try/catch — one channel\n * failing marks only its row FAILED (error recorded) and never blocks the\n * inbox record or the other channels. Delivery is at-least-once: the unique\n * (notification, channel) row makes fan-out idempotent, and every send is\n * CLAIMED before it happens (`./dispatch.ts`), so the remaining re-send window\n * is the unavoidable one — a crash between the provider call and the SENT flip.\n * Transports are required to tolerate that.\n *\n * Queueing: in-process async dispatch by default, or a real queue when the\n * host passes `scheduleDispatch`. The QUEUED status + the drain sweep are what\n * make either safe — the delivery rows are the durable record, so a queue that\n * is unavailable (or absent) costs latency, never a notification.\n */\n\n/**\n * Which channels a TENANT may use, decided per emit — the host's plan gate.\n *\n * A `null`/absent clientId is a PLATFORM notification (password resets,\n * operator alerts) and is never policy-filtered. With no policy installed\n * every channel passes.\n */\nexport type NotificationChannelPolicy = (\n clientId: string,\n channels: readonly NotificationChannel[],\n) => Promise<NotificationChannel[]> | NotificationChannel[];\n\n/** How the host defers dispatch of one already-committed notification. */\nexport type NotificationDispatchScheduler = (notificationId: string) => Promise<void>;\n\n/** One committed inbox record, as the commit observer sees it. */\nexport interface CommittedNotification {\n notificationId: string;\n /** The owner — the only field a user-scoped fan-out needs. */\n userId: string;\n /** The tenant the row was stamped with, or null for a platform emit. */\n clientId: string | null;\n}\n\n/**\n * Told about each inbox record the moment it commits. Synchronous and\n * `void`-returning by contract: an observer may not make an emit site wait,\n * and may not fail one.\n *\n * It exists because the inbox record is written HERE, in the package, while the\n * thing that usually wants to know — a realtime bus — is a dependency this\n * package does not have and should not gain. Placing it at the funnel rather\n * than at the emit sites is the point: `notify` is the single front door, so\n * every sender is covered by construction, including ones written later.\n */\nexport type NotificationCommittedListener = (notification: CommittedNotification) => void;\n\n/** Options for `notify`. */\nexport interface NotifyOptions {\n /**\n * Await transport dispatch instead of fire-and-forget. For tests and\n * worker/cron contexts where the process may exit right after emitting.\n */\n sync?: boolean;\n}\n\n/** What `notify` resolves with (dispatch may still be in flight). */\nexport interface NotifyResult {\n notificationId: string;\n /** Channels a delivery row was enqueued for (preference ∩ transport gate). */\n channels: NotificationChannel[];\n}\n\ninterface NotificationRouterDeps extends NotificationDispatchDeps {\n generators: NotificationGeneratorRegistry;\n preferences: NotificationPreferenceStore;\n channelPolicy?: NotificationChannelPolicy;\n scheduleDispatch?: NotificationDispatchScheduler;\n onCommitted?: NotificationCommittedListener;\n}\n\nexport interface NotificationRouter {\n notify<TPayload>(\n event: NotificationEvent<TPayload>,\n options?: NotifyOptions,\n ): Promise<NotifyResult>;\n dispatchDeliveries(notificationId: string): Promise<void>;\n drainPending(olderThanMs?: number, take?: number): Promise<{ dispatched: number }>;\n}\n\n/**\n * The channels that survive a plan gate this package could not consult.\n *\n * A policy error degrades to the FREE defaults (`DEFAULT_CHANNEL_ROW`, i.e.\n * e-mail + web push) intersected with what the other gates allowed — not to\n * everything. Failing fully open was the original reading, and the rationale\n * given for it only ever argued for the free channels: the dunning e-mail this\n * system carries is how payment gets collected, so a transient entitlements\n * error must not silence it. That argument says nothing about SMS and WhatsApp,\n * which are billed per message and are exactly what the host's own gate was\n * about to refuse. So the free channels stay (an extra notification beats none)\n * and the paid ones do not (the host is not billed for a channel it did not\n * authorize).\n */\nfunction policyFallback(channels: NotificationChannel[]): NotificationChannel[] {\n const free = new Set(enabledChannelsOf(DEFAULT_CHANNEL_ROW));\n return channels.filter((channel) => free.has(channel));\n}\n\n/** Apply the host's policy, degrading to the free channels on an error. */\nasync function applyPolicy(\n deps: NotificationRouterDeps,\n clientId: string | null | undefined,\n channels: NotificationChannel[],\n): Promise<NotificationChannel[]> {\n if (!deps.channelPolicy || clientId === null || clientId === undefined) return channels;\n try {\n return await deps.channelPolicy(clientId, channels);\n } catch (error) {\n deps.logger.error(\n `[notifications] channelPolicy failed for client ${clientId}; ` +\n 'degrading to the free channels:',\n error,\n );\n return policyFallback(channels);\n }\n}\n\n/** Run the commit observer without ever letting it reach the caller. */\nfunction announce(deps: NotificationRouterDeps, notification: CommittedNotification): void {\n if (!deps.onCommitted) return;\n try {\n deps.onCommitted(notification);\n } catch (error) {\n // An observer is an accelerator, never a step. The row is committed; a\n // listener that throws must not turn a delivered notification into a 500.\n deps.logger.error(\n `[notifications] commit listener failed for ${notification.notificationId}:`,\n error,\n );\n }\n}\n\n/**\n * The channels one emit will actually enqueue:\n * preference ∩ transport ∩ plan ∩ the TYPE's own availability.\n *\n * Availability is applied LAST rather than left to the preference store,\n * because {@link NotificationChannelPolicy} is a HOST function returning an\n * array and nothing constrains it to a subset of what it was handed. A policy\n * that returns a channel it was not given — a host reading the plan's own\n * entitlement list rather than filtering the argument — would otherwise put\n * back a channel this type had just declared it does not offer. The final\n * filter makes availability independent of what any host code returns.\n *\n * {@link policyFallback}, by contrast, cannot reintroduce anything: it filters\n * its own already-capped input. It is not a reason for this filter, and the\n * cost of the filter is one pass over at most four strings.\n *\n * The inbox record is NOT gated by any of this — `commit` writes it whatever\n * this returns, including the empty array — because the inbox is the record of\n * what happened rather than a channel a user opts out of.\n */\nasync function resolveChannels(\n deps: NotificationRouterDeps,\n event: NotificationEvent<unknown>,\n generator: Pick<NotificationGenerator<never>, 'category' | 'channels' | 'channelDefaults'>,\n recipient: TransportRecipient,\n): Promise<NotificationChannel[]> {\n const rules = { channels: generator.channels, channelDefaults: generator.channelDefaults };\n const enabled = await deps.preferences.enabledChannels(\n event.recipient.userId,\n generator.category,\n rules,\n );\n const supported = enabled.filter((channel) => {\n const transport = deps.transports.get(channel);\n return transport !== null && transport.supports(recipient);\n });\n // The host's plan gate: a tenant-scoped emit keeps only the channels the\n // tenant's plan covers, so a revoked transport DEGRADES to the remaining ones\n // rather than dropping the notification silently.\n const permitted = await applyPolicy(deps, event.recipient.clientId, supported);\n return capToAvailable(permitted, generator.channels);\n}\n\n/** Commit the inbox record and its delivery rows together, in one transaction. */\nasync function commit(\n deps: NotificationRouterDeps,\n event: NotificationEvent<unknown>,\n category: string,\n content: { title: string; body: string; link?: string; data?: Record<string, unknown> },\n channels: NotificationChannel[],\n): Promise<{ id: string; userId: string; clientId: string | null }> {\n const client = await deps.db();\n return client.$transaction(async (tx) => {\n const created = await tx.notification.create({\n data: {\n userId: event.recipient.userId,\n clientId: event.recipient.clientId ?? null,\n type: event.type,\n category,\n title: content.title,\n body: content.body,\n link: content.link ?? null,\n data: content.data ?? {},\n },\n });\n if (channels.length > 0) {\n await tx.notificationDelivery.createMany({\n data: channels.map((channel) => ({ notificationId: created.id, channel })),\n // Idempotence backstop: the unique (notification, channel) key.\n skipDuplicates: true,\n });\n }\n return created;\n });\n}\n\nexport function createNotificationRouter(deps: NotificationRouterDeps): NotificationRouter {\n return {\n dispatchDeliveries: async (notificationId) => {\n await dispatchOne(deps, notificationId);\n },\n drainPending: (olderThanMs = DEFAULT_SWEEP_CUTOFF_MS, take = DEFAULT_SWEEP_TAKE) =>\n drainPending(deps, olderThanMs, take),\n\n async notify(event, options = {}) {\n const generator = deps.generators.resolve(event.type);\n\n /*\n The recipient is loaded BEFORE the content is rendered, and the order is\n the whole of the fix.\n\n `notify` is already per-person — `event.recipient.userId` is one user,\n and a permission fan-out resolves its audience and calls this once each\n — so the reader was knowable here all along. Rendering first simply\n threw that away: the row was written in whatever language the generator\n had been bound to at boot, for everyone.\n\n Loading first also means a notification addressed to nobody now throws\n before any content is built, which is the cheaper order anyway.\n */\n const recipient = await loadRecipient(\n deps,\n event.recipient.userId,\n event.recipient.clientId ?? null,\n );\n if (!recipient) throw new UnknownNotificationRecipientError(event.recipient.userId);\n\n // Forwarded exactly as the directory stated it, `undefined` included: the\n // generator owns the fallback, in one place a reader can find.\n const content = generator.generate(event.payload as never, { locale: recipient.locale });\n\n const channels = await resolveChannels(deps, event, generator, recipient);\n const notification = await commit(deps, event, generator.category, content, channels);\n\n // AFTER the transaction, never inside it: a subscriber woken by an event\n // published mid-transaction would re-read and not find the row it was told\n // about — the classic read-your-own-hint race.\n announce(deps, {\n notificationId: notification.id,\n userId: notification.userId,\n clientId: notification.clientId,\n });\n\n // `sync` always means \"send it here, now\" — a test or a worker about to\n // exit must not have its send handed to a queue it will never drain.\n if (options.sync) {\n await dispatchOne(deps, notification.id);\n return { notificationId: notification.id, channels };\n }\n\n void (\n deps.scheduleDispatch\n ? deps.scheduleDispatch(notification.id)\n : dispatchOne(deps, notification.id)\n ).catch((error: unknown) => {\n // Detached-path backstop only: per-delivery failures are already\n // recorded on their rows; this catches infrastructure errors.\n deps.logger.error(`[notifications] dispatch failed for ${notification.id}:`, error);\n });\n\n return { notificationId: notification.id, channels };\n },\n };\n}\n","import { createGeneratorRegistry, type NotificationGeneratorRegistry } from '../generators';\nimport type {\n NotificationsCopySource,\n NotificationWireMessages,\n} from '../messages';\nimport type { ChannelRow } from '../preferences-core';\nimport {\n taxonomyOf,\n type NotificationCategory,\n type NotificationGenerator,\n type NotificationLogger,\n} from '../types';\n\nimport {\n createNotifyByPermission,\n type NotificationAudienceDirectory,\n type NotifyByPermission,\n} from './by-permission';\nimport type { NotificationsRoute } from './context';\nimport type { NotificationContactDirectory, NotificationsDbProvider } from './db';\nimport { DEFAULT_MAX_DELIVERY_ATTEMPTS } from './dispatch';\nimport { createInboxStore, type NotificationInboxStore } from './inbox';\nimport { createPreferenceStore, type NotificationPreferenceStore } from './preferences';\nimport {\n createPushSubscriptionStore,\n type PushSubscriptionStore,\n} from './push-subscriptions';\nimport { notificationRoutes } from './routes';\nimport {\n createNotificationRouter,\n type NotificationChannelPolicy,\n type NotificationCommittedListener,\n type NotificationDispatchScheduler,\n type NotificationRouter,\n} from './router';\nimport {\n createTransportRegistry,\n type ExtraDrivers,\n type TransportDeclaration,\n type TransportRegistry,\n} from './transports/registry';\n\n/**\n * The one thing this package exposes to a BACKEND host (12-15).\n *\n * The pipeline used to be a private workspace package plus six hand-written\n * route files: each one resolving the session, calling a loose helper, and\n * shaping a response, with the transports reading their own credentials out of\n * `process.env` and registering themselves as an import side effect. Only \"who\n * is calling, where the rows live, how a channel reaches a person\" was ever the\n * host's business; the rest — the routing, the delivery rows, the retries, the\n * request contract, the envelope, the pt-BR copy — is this surface's.\n *\n * Routes are FRAMEWORK-NEUTRAL descriptors, not a Hono/Express router (the\n * report-builder doctrine). `@12-apps/notifications/hono` adapts them.\n *\n * What stays the HOST's, and is passed in rather than guessed at:\n *\n * - **Authentication** — the adapter's `resolveActor` hands over a user id.\n * Every endpoint is self-scoped, so that is the entire authorization seam.\n * - **Where the four owned tables live** — the structural `db` seam.\n * - **How to reach a person** — `contacts`, because a package cannot know the\n * shape of a host's identity table (nor whether its phones are verified).\n * - **Which vendors carry which channel** — `transports`, one declaration per\n * channel. An undeclared channel is off; a second vendor is a config entry.\n * - **Billing** — `channelPolicy`, the plan gate answered per emit.\n * - **Its authorization engine** — `audience`, for the permission fan-out.\n * - **Its domain events** — `generators`, registered from the outside.\n */\n\nexport interface NotificationsServerConfig {\n /** Prisma-shaped client for the four owned models, through the seam. */\n db: NotificationsDbProvider;\n /** How a transport reaches a person (the host's identity table). */\n contacts: NotificationContactDirectory;\n /** One declaration per channel the host wants on. Default: none, all off. */\n transports?: readonly TransportDeclaration[];\n /** The host's own vendor drivers, per channel. */\n drivers?: ExtraDrivers;\n /** The domain events this mount can emit. */\n generators?: readonly NotificationGenerator<never>[];\n /**\n * Preference categories — the granularity at which a user chooses channels.\n *\n * REQUIRED. This defaulted to one product's four (`orders`, `payments`,\n * `stock`, `system`), which is the host's vocabulary and not this library's:\n * a host that omitted it rendered four rows it never chose, with its own\n * categories absent, and nothing failed — `category` is a free string by\n * design, so there was no layer left to notice.\n */\n categories: readonly NotificationCategory[];\n /** Override which channels a never-touched category defaults to. */\n channelDefaults?: Partial<ChannelRow>;\n /** The tenant plan gate, answered per emit. */\n channelPolicy?: NotificationChannelPolicy;\n /** Hand dispatch to a real queue instead of the in-process detached send. */\n scheduleDispatch?: NotificationDispatchScheduler;\n /**\n * Claims one delivery gets before the sweep gives up on it and writes DEAD.\n * Default 5. There is no \"unlimited\": a permanently invalid destination would\n * be a billed provider call on every sweep for the life of the row.\n */\n maxDeliveryAttempts?: number;\n /** Told the moment an inbox record commits (a realtime bus, typically). */\n onCommitted?: NotificationCommittedListener;\n /** Told when a mark-read/delete actually changed something. */\n onInboxChanged?: (userId: string) => void;\n /** The host's authorization engine, for `notifyByPermission`. */\n audience?: NotificationAudienceDirectory;\n /**\n * Every user-facing sentence this surface can produce — REQUIRED host config.\n *\n * A pack, or a RESOLVER for a host whose callers do not share a language.\n * Passed to the routes UNRESOLVED: this factory runs once per process (and\n * at least one host memoises its call), so resolving here would answer every\n * later request in the language the process started with.\n */\n messages: NotificationsCopySource<NotificationWireMessages>;\n /** The host's logger. Defaults to the console. */\n logger?: NotificationLogger;\n}\n\nexport interface ApiNotifications {\n /** The whole generated surface, in mount order. */\n routes: NotificationsRoute[];\n /** The emit front door. */\n notify: NotificationRouter['notify'];\n /** Send every still-QUEUED delivery of one notification. */\n dispatchDeliveries: NotificationRouter['dispatchDeliveries'];\n /** The retry sweep, for a cron/admin trigger. */\n drainPending: NotificationRouter['drainPending'];\n /**\n * \"Tell whoever can act on this.\" Rejects when the host configured no\n * `audience` — loudly, because the alternative is a money alert nobody gets.\n */\n notifyByPermission: NotifyByPermission;\n /** The stores, for host surfaces that read the same tables. */\n inbox: NotificationInboxStore;\n preferences: NotificationPreferenceStore;\n pushSubscriptions: PushSubscriptionStore;\n /** Register a generator after the mount (a lazily-imported domain module). */\n registerGenerator: NotificationGeneratorRegistry['register'];\n /** The declared transports, for diagnostics and availability probes. */\n transports: TransportRegistry;\n /**\n * The copy source in force, so a host's own screens can reuse a sentence.\n *\n * The SOURCE rather than a resolved pack, for the same reason the routes get\n * one: a host screen serving two readers must be able to ask per reader.\n * Read it with `messagesOf({ messages }, locale)`.\n */\n messages: NotificationsCopySource<NotificationWireMessages>;\n}\n\n/** Drop the keys the host left unset, so an absent seam stays absent. */\nfunction present<T extends object>(entries: T): Partial<T> {\n return Object.fromEntries(\n Object.entries(entries).filter(([, value]) => value !== undefined),\n ) as Partial<T>;\n}\n\n/** Fallback logger — used until the host passes its own. */\nconst consoleLogger: NotificationLogger = {\n info: (message, ...meta) => console.info(message, ...meta),\n error: (message, ...meta) => console.error(message, ...meta),\n};\n\nexport function createApiNotifications(config: NotificationsServerConfig): ApiNotifications {\n /**\n * The SOURCE travels; nothing is resolved here.\n *\n * This factory runs once per process — and at least one host memoises its\n * call behind an `if (assembled) return assembled;` — so a `messagesOf(config)`\n * on this line would word every later request in the language the process\n * started with. The handlers resolve per request instead.\n */\n const messages = config.messages;\n const taxonomy = taxonomyOf(config);\n const logger = config.logger ?? consoleLogger;\n\n const generators = createGeneratorRegistry(config.generators ?? []);\n const inbox = createInboxStore(config.db);\n const preferences = createPreferenceStore(\n config.db,\n taxonomy,\n config.channelDefaults ?? {},\n );\n const pushSubscriptions = createPushSubscriptionStore(config.db, logger);\n const transports = createTransportRegistry(\n config.transports ?? [],\n pushSubscriptions,\n config.drivers ?? {},\n logger,\n );\n\n const router = createNotificationRouter({\n db: config.db,\n generators,\n transports,\n preferences,\n pushSubscriptions,\n contacts: config.contacts,\n logger,\n maxAttempts: config.maxDeliveryAttempts ?? DEFAULT_MAX_DELIVERY_ATTEMPTS,\n // `exactOptionalPropertyTypes` is on, so an absent host seam must be an\n // ABSENT key rather than an explicit `undefined`.\n ...present({\n channelPolicy: config.channelPolicy,\n scheduleDispatch: config.scheduleDispatch,\n onCommitted: config.onCommitted,\n }),\n });\n\n const audience = config.audience;\n const notifyByPermission: NotifyByPermission = audience\n ? createNotifyByPermission({ router, directory: audience, logger })\n : () =>\n Promise.reject(\n new Error(\n 'notifyByPermission() needs an `audience` directory — pass the host authorization ' +\n 'engine to createApiNotifications({ audience }).',\n ),\n );\n\n return {\n routes: notificationRoutes({\n inbox,\n preferences,\n pushSubscriptions,\n transports,\n contacts: config.contacts,\n categories: taxonomy.categories,\n messages,\n ...present({ onInboxChanged: config.onInboxChanged }),\n }),\n notify: router.notify,\n dispatchDeliveries: router.dispatchDeliveries,\n drainPending: router.drainPending,\n notifyByPermission,\n inbox,\n preferences,\n pushSubscriptions,\n registerGenerator: generators.register,\n transports,\n messages,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createInboxStore,
|
|
4
4
|
useBadgeState,
|
|
5
5
|
useUnreadCount
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-AN6IX4AE.js";
|
|
7
7
|
import {
|
|
8
8
|
messagesOf
|
|
9
9
|
} from "./chunk-M2TVBVH2.js";
|
|
@@ -285,7 +285,7 @@ import { Suspense, lazy, useEffect, useState } from "react";
|
|
|
285
285
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
286
286
|
function lazyNotificationsPanel(parts) {
|
|
287
287
|
const Bound = lazy(async () => {
|
|
288
|
-
const { NotificationsPanel } = await import("./panel-
|
|
288
|
+
const { NotificationsPanel } = await import("./panel-JL5RDEHY.js");
|
|
289
289
|
return {
|
|
290
290
|
default: /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx2(NotificationsPanel, { ...props, ...parts }), "default")
|
|
291
291
|
};
|
|
@@ -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-643HNPQJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/api.ts","../src/react/transport.ts","../src/react/create-web-notifications.tsx","../src/react/bell-badge.ts","../src/react/live-seen.ts","../src/react/bell-button.tsx","../src/react/panel-lazy.tsx","../src/react/page-lazy.tsx"],"sourcesContent":["import type { ChannelRow } from '../preferences-core';\nimport type { NotificationChannel } from '../types';\nimport type { ListNotificationsResult } from '../wire';\n\nimport type { NotificationsResult, NotificationsTransport } from './transport';\n\n/**\n * The wire client, bound to one mount (12-15).\n *\n * Every path this package's screens can call, in one place — which is what\n * makes the api half's route table and the web half's URLs one contract instead\n * of two lists that drift.\n */\n\n/** `GET <mount>/notification-preferences` and the PUT's answer. */\nexport interface PreferencesPayload {\n preferences: Record<string, ChannelRow>;\n availability: Record<NotificationChannel, boolean>;\n /** The host's taxonomy, so the screen renders it without being told twice. */\n categories: string[];\n}\n\n/** `GET <mount>/push-subscriptions`. */\nexport interface PushRegistrationPayload {\n /** null = web push is not configured on this deployment. */\n vapidPublicKey: string | null;\n count: number;\n /**\n * Whether the endpoint asked about is still registered to the caller. Present\n * only when one was passed — see {@link NotificationsApiClient.getPushRegistration}.\n */\n registered?: boolean;\n}\n\nexport interface NotificationsApiClient {\n listNotifications(input: {\n cursor?: string | null;\n limit?: number;\n filter?: 'all' | 'unread';\n }): Promise<ListNotificationsResult>;\n unreadCount(): Promise<number>;\n markRead(ids: readonly string[]): Promise<NotificationsResult<{ updated: number }>>;\n markAllRead(): Promise<NotificationsResult<{ updated: number }>>;\n remove(ids: readonly string[]): Promise<NotificationsResult<{ deleted: number }>>;\n getPreferences(): Promise<PreferencesPayload>;\n savePreference(\n category: string,\n channel: NotificationChannel,\n enabled: boolean,\n ): Promise<NotificationsResult<PreferencesPayload>>;\n /**\n * The deployment's VAPID key and the caller's device count — and, when an\n * `endpoint` is passed, whether the SERVER still has that exact subscription\n * under the caller's id. The browser holding a subscription object is not\n * evidence of that: a re-own or a 404/410 prune drops the row and leaves the\n * browser's object in place.\n */\n getPushRegistration(input?: { endpoint?: string }): Promise<PushRegistrationPayload>;\n savePushSubscription(input: {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n }): Promise<NotificationsResult<{ count: number }>>;\n removePushSubscription(endpoint: string): Promise<NotificationsResult<{ count: number }>>;\n}\n\nexport function createNotificationsApiClient(\n apiBase: string,\n transport: NotificationsTransport,\n): NotificationsApiClient {\n const base = apiBase.replace(/\\/$/, '');\n const url = (path: string): string => `${base}${path}`;\n\n return {\n listNotifications({ cursor, limit, filter }) {\n const params = new URLSearchParams();\n if (limit !== undefined) params.set('limit', String(limit));\n if (cursor) params.set('cursor', cursor);\n if (filter) params.set('filter', filter);\n const query = params.toString();\n return transport.get<ListNotificationsResult>(\n url(`/notifications${query ? `?${query}` : ''}`),\n );\n },\n async unreadCount() {\n const { count } = await transport.get<{ count: number }>(\n url('/notifications/unread-count'),\n );\n return count;\n },\n markRead: (ids) =>\n transport.send(url('/notifications/mark-read'), 'POST', { ids: [...ids] }),\n markAllRead: () => transport.send(url('/notifications/mark-read'), 'POST', { all: true }),\n remove: (ids) => transport.send(url('/notifications/delete'), 'POST', { ids: [...ids] }),\n getPreferences: () => transport.get<PreferencesPayload>(url('/notification-preferences')),\n savePreference: (category, channel, enabled) =>\n transport.send(url('/notification-preferences'), 'PUT', {\n [category]: { [channel]: enabled },\n }),\n getPushRegistration: ({ endpoint } = {}) =>\n transport.get<PushRegistrationPayload>(\n url(\n endpoint\n ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}`\n : '/push-subscriptions',\n ),\n ),\n savePushSubscription: (input) => transport.send(url('/push-subscriptions'), 'POST', input),\n removePushSubscription: (endpoint) =>\n transport.send(url('/push-subscriptions'), 'DELETE', { endpoint }),\n };\n}\n","/**\n * How the notification screens reach their data (12-15) — the report-builder\n * transport doctrine: this is the ONLY way the surface performs I/O, so a\n * caller supplying one has substituted the entire backend without stubbing a\n * global. The default is same-origin `fetch` riding the browser's cookies.\n */\n\n/** A write outcome the screens branch on — never a thrown mutation. */\nexport type NotificationsResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\n/** A failed read, carrying the status the screens branch on (401 = signed out). */\nexport class NotificationsHttpError extends Error {\n readonly status: number;\n constructor(status: number, message: string) {\n super(message);\n this.name = 'NotificationsHttpError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationsHttpError.prototype);\n }\n}\n\nexport interface NotificationsTransport {\n /** A read. Returns the payload INSIDE the `{ data }` envelope. */\n get<T>(path: string): Promise<T>;\n /** A write. Returns a {@link NotificationsResult} rather than rejecting. */\n send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>>;\n}\n\n/**\n * @param fallbackError What a failed write says when the server sent no\n * sentence of its own — REQUIRED, the host's words. `createWebNotifications`\n * already passes its (equally required) `messages.operationFailed`; only a\n * host constructing the transport directly writes it here. The old default\n * was one application's Portuguese, and the only string in this package the\n * required-messages port did not cover.\n */\nexport function httpNotificationsTransport(fallbackError: string): NotificationsTransport {\n return {\n async get<T>(path: string): Promise<T> {\n const response = await fetch(path, {\n credentials: 'same-origin',\n headers: { Accept: 'application/json' },\n });\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) {\n throw new NotificationsHttpError(\n response.status,\n payload?.error ?? `HTTP ${response.status} for ${path}`,\n );\n }\n return (payload?.data ?? payload) as T;\n },\n\n async send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>> {\n try {\n const response = await fetch(path, {\n method,\n credentials: 'same-origin',\n headers: {\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (response.status === 204) return { ok: true, data: undefined as T };\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };\n return { ok: true, data: (payload?.data ?? payload) as T };\n } catch {\n return { ok: false, error: fallbackError };\n }\n },\n };\n}\n","import { useState, type ComponentType, type JSX } from 'react';\n\nimport { messagesOf, type NotificationMessages } from '../messages';\n\nimport { createNotificationsApiClient, type NotificationsApiClient } from './api';\nimport { useInboxBellBadge, useLiveBellBadge, type BellBadge } from './bell-badge';\nimport { BellButton, LiveBellButton, type BellButtonProps } from './bell-button';\nimport {\n useUnreadCount,\n type NotificationsSignalHook,\n type NotificationsSubscribe,\n} from './hooks';\nimport { createInboxStore, type InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport { createLiveSeenStore, type LiveSeenStore } from './live-seen';\nimport { lazyNotificationsPanel } from './panel-lazy';\nimport type { NotificationsPanelProps } from './panel';\nimport { lazyPreferencesPage } from './page-lazy';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport { httpNotificationsTransport, type NotificationsTransport } from './transport';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/**\n * The one thing this package exposes to a FRONTEND host (12-15).\n *\n * Everything the notification centre IS — the bell with its live badge, the\n * slide-over inbox with its optimistic mark-read / delete / mark-all and its\n * cursor pager, the preferences matrix with its availability hints and the\n * per-browser push enable step, and every wire call between them — lives inside\n * this package. The host names where the API is mounted, and that is the whole\n * wiring.\n *\n * `page` is the standalone surface (the preferences screen), which is the one\n * thing a host routes to. The bell and the panel are a PAIR a host drops into\n * its own chrome, and they share one store, so a read in the panel moves the\n * badge in the same tick.\n */\n\nexport interface NotificationsWebConfig {\n /** The account mount the routes live under, e.g. `/api/account`. */\n apiBase: string;\n /** How the surface reaches its data. Default: same-origin fetch. */\n transport?: NotificationsTransport;\n /** User-facing copy overrides (pt-BR product copy by default). */\n messages: NotificationMessages;\n /**\n * How the surface learns an inbox changed without asking — the host's message\n * bus. Without it the badge keeps its 60 s poll, which is the standing\n * contract rather than a fallback: a dropped event must cost latency, never\n * correctness.\n */\n subscribe?: NotificationsSubscribe;\n /**\n * The same wiring as a HOOK, for a host whose realtime connection lives in\n * React context — see `NotificationsSignalHook`. `subscribe` is read at\n * factory time, which such a host cannot reach.\n */\n useSignal?: NotificationsSignalHook;\n /** The browser push enable step's host seams (SW path, platform hint). */\n webPush?: WebPushSetupConfig;\n /**\n * LIVE ACTIVITIES — the ongoing-state entries pinned above the inbox list.\n *\n * Opt-in, and absent means absent: a host that passes nothing gets the panel\n * it had, with no section, no heading and no reserved space. See\n * `./live-config` for the two things a host has to supply (where they come\n * from, and what the section says) and `../live` for what one IS.\n */\n liveActivities?: LiveActivitiesConfig;\n}\n\nexport interface WebNotifications {\n /**\n * The routed surface: the preferences screen.\n *\n * Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and\n * the panel never downloads it, and a host that routes to it fetches it while\n * entering that route.\n */\n page: ComponentType<PreferencesScreenProps>;\n /** The bell, already bound to the shared store. */\n BellButton: ComponentType<BellButtonProps>;\n /**\n * The inbox slide-over, sharing that store.\n *\n * Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a\n * host's chrome carries the bell and nothing else.\n */\n Panel: ComponentType<NotificationsPanelProps>;\n /**\n * Bell + panel as ONE element, for a host that just wants the feature in its\n * header and does not want to own the open/closed state.\n */\n BellWithPanel: ComponentType<{\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }>;\n /**\n * The unread INBOX count.\n *\n * For a host with its own trigger chrome only when that host configured no\n * live activities — otherwise it is a bell that ignores everything happening\n * right now, and `useBellBadge` below is the door. Still the right hook for\n * anything that genuinely wants \"how many unread rows\".\n */\n useUnreadCount: (options?: BadgeHookOptions) => number;\n /**\n * The badge's NUMBER AND TONE, for a host with its own trigger chrome.\n *\n * What `useUnreadCount` should have been for a host that also configured live\n * activities, and the reason it is a second door rather than a change to that\n * one: a count alone cannot express a bell, because a live entry is present\n * without being news (see `./bell-badge`). A host that renders\n * `useUnreadCount` in its own chrome gets a badge that ignores everything\n * happening right now — which is not a subtle wrongness, it is the pinned\n * pedido on screen going uncounted.\n *\n * Identical to what this package's own `BellButton` draws, because it is the\n * hook that bell uses. Without live activities configured it is\n * `useUnreadCount` plus `hasNew: count > 0`.\n */\n useBellBadge: (options?: BadgeHookOptions) => BellBadge;\n /** The shared client state, for host glue. */\n store: InboxStore;\n /** The bound wire client. */\n api: NotificationsApiClient;\n /** The copy in force, so a host's own chrome can reuse a sentence. */\n messages: NotificationMessages;\n}\n\n/**\n * What a host passes either badge hook: whether to run at all, and — for a host\n * that holds its own realtime connection — whether that connection is up\n * (`BadgeSyncOptions.live`).\n */\nexport interface BadgeHookOptions {\n enabled?: boolean;\n live?: boolean;\n}\n\n/** What the factory passes both badge hooks: whatever realtime wiring it has. */\ntype SubscribeOption = {\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n};\n\n/**\n * The two badge hooks, bound to this factory's store.\n *\n * `useBellBadge` is chosen ONCE here, the same way `Bell` is below and for the\n * same reason: `live.useActivities` is a hook, so which implementation runs\n * must not be a per-render decision.\n */\nfunction bindBadgeHooks(\n store: InboxStore,\n subscribeOption: SubscribeOption,\n liveSeen: LiveSeenStore,\n live: LiveActivitiesConfig | undefined,\n): Pick<WebNotifications, 'useUnreadCount' | 'useBellBadge'> {\n return {\n useUnreadCount: (options = {}) => useUnreadCount(store, { ...options, ...subscribeOption }),\n useBellBadge: live\n ? (options = {}) => useLiveBellBadge(store, live, liveSeen, { ...options, ...subscribeOption })\n : (options = {}) => useInboxBellBadge(store, { ...options, ...subscribeOption }),\n };\n}\n\nexport function createWebNotifications(config: NotificationsWebConfig): WebNotifications {\n const messages = messagesOf(config);\n const api = createNotificationsApiClient(\n config.apiBase,\n config.transport ?? httpNotificationsTransport(messages.operationFailed),\n );\n const store = createInboxStore(api);\n const webPush = config.webPush ?? {};\n const subscribe = config.subscribe;\n const subscribeOption = {\n ...(subscribe ? { subscribe } : {}),\n ...(config.useSignal ? { useSignal: config.useSignal } : {}),\n };\n\n // One store per factory, shared by the bell that READS it and the panel that\n // WRITES it — the same arrangement as the inbox store above, and for the same\n // reason: two independent copies would disagree about what the reader saw.\n const liveSeen = createLiveSeenStore();\n\n // Chosen ONCE, here, because `useActivities` is a hook and the choice must\n // not be made per render: a bell that read an optional config inside itself\n // would be calling a hook conditionally.\n const live = config.liveActivities;\n const Bell: ComponentType<BellButtonProps> = live\n ? (props) => (\n <LiveBellButton\n {...props}\n store={store}\n messages={messages}\n live={live}\n seen={liveSeen}\n {...subscribeOption}\n />\n )\n : (props) => (\n <BellButton {...props} store={store} messages={messages} {...subscribeOption} />\n );\n const Panel = lazyNotificationsPanel({\n store,\n messages,\n ...(live ? { live, liveSeen } : {}),\n });\n\n const badgeHooks = bindBadgeHooks(store, subscribeOption, liveSeen, live);\n\n function BellWithPanel({\n enabled = true,\n onNavigate,\n }: {\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }): JSX.Element {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Bell enabled={enabled} onClick={() => setOpen(true)} />\n <Panel\n open={open}\n onClose={() => setOpen(false)}\n {...(onNavigate ? { onNavigate } : {})}\n />\n </>\n );\n }\n\n return {\n page: lazyPreferencesPage({ api, messages, webPush }),\n BellButton: Bell,\n Panel,\n BellWithPanel,\n ...badgeHooks,\n store,\n api,\n messages,\n };\n}\n","/**\n * What the bell shows: ONE number, and whether any of it is news.\n *\n * The badge answers two different questions with one glyph, and keeping them\n * apart is the whole design:\n *\n * - the COUNT is how many things the centre is holding for the reader;\n * - the TONE is whether any of them has happened since they last looked.\n *\n * An inbox row makes those the same question — an unread row is by definition\n * both present and unseen — which is why the distinction did not exist before\n * live activities did. A live activity separates them: a pedido that has been\n * `Preparo` for ten minutes is still worth a `1`, and shouting about it every\n * render is how a badge teaches people to stop reading it.\n *\n * ## This is here so a host can DRAW it\n *\n * The numbers were already correct inside this package's own `BellButton`, and\n * unreachable from a host that cannot take that component — a header whose cart\n * and search buttons are one styled icon-button is importing a second trigger\n * style the moment it does. Such a host had `useUnreadCount` and nothing else,\n * so its bell showed NOTHING while a pinned pedido sat inside the panel it\n * opens. Both bells now read these hooks, so a host cannot drift from what this\n * package renders.\n *\n * ## What it does NOT yet do\n *\n * A live subject usually also writes inbox rows as it moves, and this counts\n * both: a pedido with one unread row about it reads `2`. Subtracting the double\n * needs the server to say which unread rows name which subject, and that was\n * built, reviewed and pulled — for reasons about the CONTRACT rather than the\n * arithmetic, and worth recording so the next attempt starts past them:\n *\n * - it added a field to `GET /notifications/unread-count`, and at least one\n * adopter publishes that response as a closed schema to LLM clients. An\n * additive field is a breaking change against `additionalProperties: false`.\n * - the scan is per READER, so every host paid it — including the two SPAs in\n * that adopter that share one factory and configure no live activities at\n * all, and read the count through `useUnreadCount`, which never sees the\n * breakdown.\n * - it narrowed `NotificationsApiClient.unreadCount()` from `Promise<number>`,\n * which is a breaking change on a commit the release rules cut as a minor.\n *\n * The way through is an opt-in the surface asks for — a host with no live\n * activities then sends nothing different and receives nothing different.\n *\n * (An earlier revision of this docblock blamed a missing index instead. That\n * was wrong: `[userId, deletedAt, readAt]` is a full equality prefix over the\n * filter, and the `ORDER BY` the scan carried was not load-bearing, since a\n * tally does not care what order it counts in.)\n */\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useBadgeState, type BadgeSyncOptions } from './hooks';\nimport type { InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport { hasUnseenActivity, type LiveSeenStore } from './live-seen';\n\n/** The bell's whole state — see the file docblock for what each half means. */\nexport interface BellBadge {\n /** What the badge shows. `0` renders no badge at all. */\n count: number;\n /**\n * Whether any of it has arrived or moved since the reader last looked.\n *\n * The trigger paints this as its accent colour; a host with its own chrome\n * decides how to say it, but it should be a difference somebody notices.\n */\n hasNew: boolean;\n}\n\n/**\n * The badge for a host with no live activities: unread rows, and that is all.\n *\n * `hasNew` is `count > 0` here, and not as a simplification — an UNREAD row is\n * one the reader has not seen, so for this host presence and novelty really are\n * the same fact.\n */\nexport function useInboxBellBadge(store: InboxStore, options: BadgeSyncOptions = {}): BellBadge {\n // `useBadgeState` already blanks itself when disabled — the gate lives there,\n // once, rather than at each of the three hooks that layer on it.\n const { unread } = useBadgeState(store, options);\n // MEMOISED, unlike the number `useUnreadCount` returns. `useSyncExternalStore`\n // re-renders on every `patch` and `patch` always allocates, so a poll that\n // comes back with an unchanged count would otherwise hand a host a new object\n // every 60 s — enough to re-fire a `useEffect` keyed on it, or defeat a\n // `React.memo` on the trigger, forever.\n return useMemo(() => ({ count: unread, hasNew: unread > 0 }), [unread]);\n}\n\n/**\n * The badge for a host that configured live activities.\n *\n * A SECOND hook rather than a flag on the one above, for the reason the bell\n * itself is two components: `live.useActivities` is a hook, so a single hook\n * reading an optional config would be calling one conditionally — which React\n * reports as a crash somewhere else entirely. The factory knows statically\n * which host it is building for and binds one.\n *\n * ## `enabled` is enforced HERE, not taken on trust\n *\n * A host is explicitly allowed to ignore the `active` hint and always answer —\n * `./live-config` calls that \"behaving correctly and merely paying for it\" — so\n * a signed-out header, which still MOUNTS the bell, can be handed a list of\n * somebody's pedidos. The guard below is the only thing between that and a\n * badge counting them.\n *\n * Defensive against the CONTRACT, not against an observed adopter: today's one\n * honours the hint on every lever it has. That is exactly why the guard needs\n * saying — nothing about the current tree would fail if it went, and the case\n * that covers it has to build the ignoring host itself.\n *\n * ## What it costs the host, stated plainly\n *\n * The bell is mounted for as long as the app is, so unlike the panel's copy of\n * this hook there is no \"nobody is looking\" state to stand down in — `active`\n * is simply `enabled`. A host that answers by polling therefore polls for every\n * signed-in reader whether or not they ever open the centre. That is the price\n * of a badge that knows about live activities at all, and the reason to answer\n * this hook from a pushed cache rather than from an interval.\n */\nexport function useLiveBellBadge(\n store: InboxStore,\n live: LiveActivitiesConfig,\n seen: LiveSeenStore,\n options: BadgeSyncOptions = {},\n): BellBadge {\n const enabled = options.enabled ?? true;\n const { unread } = useBadgeState(store, options);\n const activities = live.useActivities({ active: enabled });\n const seenAt = useSyncExternalStore(seen.subscribe, seen.read, seen.read);\n // The store's own half is already blanked by `useBadgeState`; the `enabled`\n // guard here is for the ACTIVITIES half, which comes from a host hook that\n // may have ignored the hint.\n //\n // A live entry COUNTS. It is a notification — it is the one the reader most\n // wants to know about — and the panel it opens lists it.\n const count = enabled ? unread + activities.length : 0;\n const hasNew = enabled && (unread > 0 || hasUnseenActivity(activities, seenAt));\n // Memoised on the two RESULTS, not on `activities`. A host's hook returns a\n // fresh array every render — the storefront's maps its query's rows, so\n // structural sharing keeps the DATA identical and the array new — so an\n // `activities` dependency would invalidate on every render and the memo would\n // buy nothing at all. `hasUnseenActivity` runs unmemoised in front of it,\n // which is a `.some()` over the handful of things happening at once.\n return useMemo(() => ({ count, hasNew }), [count, hasNew]);\n}\n","/**\n * What the reader has already been shown, so the bell can say NEW rather than\n * merely PRESENT.\n *\n * A live activity is unlike an inbox row in the one way that matters here: it\n * stays on the panel for as long as the thing is happening, so its presence\n * cannot mean \"you have not seen this\". A pedido that has been `Preparo` for\n * ten minutes is still live and still worth counting, but nothing has happened\n * — and a badge that shouts for a subject the reader has already looked at is a\n * badge people stop reading.\n *\n * So presence and novelty are answered separately: the COUNT comes from how\n * many are live, and the TONE comes from this. The panel writes it — being on\n * screen is what seen means — and the bell reads it.\n *\n * ## Per subject, not one watermark\n *\n * A single \"newest instant already seen\" is smaller and was the first cut, and\n * it is wrong in a way that shows up in normal use: a pedido placed ten minutes\n * ago but only now reaching the client arrives with an `updatedAt` BEHIND the\n * watermark, and would be silently marked as already seen. The reader has never\n * laid eyes on it. Keyed by subject, an id that has not been recorded is new\n * whatever its clock says.\n *\n * Bounded by pruning rather than by expiry: every write keeps only the subjects\n * that are live at that moment, so the record can never outgrow the number of\n * things happening at once. A subject that finishes and later comes back is\n * news again, which is correct — it is a different occurrence.\n */\nimport type { LiveActivity } from '../live';\n\nconst STORAGE_KEY = '12a.notifications.live-seen';\n\n/** id -> the `updatedAt` that was on screen. */\ntype SeenMap = Readonly<Record<string, string>>;\n\nconst EMPTY: SeenMap = {};\n\n/** ms since epoch, or `null` for an absent or unparseable stamp. */\nfunction instant(iso: string | undefined): number | null {\n if (iso === undefined) return null;\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? null : ms;\n}\n\n/**\n * Read/write through `try`, every time.\n *\n * `localStorage` is not merely absent in SSR and in a worker — the ACCESSOR\n * itself throws in a browser set to block site data. A notification bell that\n * cannot render because storage is blocked is a worse failure than one that\n * forgets what was seen, and forgetting degrades in the safe direction: towards\n * saying something is happening.\n */\nfunction readStored(): SeenMap {\n try {\n const raw = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (!raw) return EMPTY;\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return EMPTY;\n // Anything can be in storage — another version of this package, or a person\n // with the devtools open. Keep only what has the shape this reads.\n const clean: Record<string, string> = {};\n for (const [id, value] of Object.entries(parsed)) {\n if (typeof value === 'string') clean[id] = value;\n }\n return clean;\n } catch {\n return EMPTY;\n }\n}\n\nfunction writeStored(value: SeenMap): void {\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(value));\n } catch {\n // Blocked or full. The badge stays new a while longer; nothing else breaks.\n }\n}\n\nexport interface LiveSeenStore {\n /** What has been shown, keyed by subject id. */\n read: () => SeenMap;\n /** Record that exactly these are on screen now, forgetting subjects that are not. */\n mark: (activities: readonly LiveActivity[]) => void;\n subscribe: (listener: () => void) => () => void;\n}\n\nexport function createLiveSeenStore(): LiveSeenStore {\n // Mirrored in memory as well as in storage: `useSyncExternalStore` compares\n // snapshots by IDENTITY and calls `read` on every render, so parsing storage\n // there would hand it a fresh object each time and re-render for ever.\n let current = readStored();\n const listeners = new Set<() => void>();\n\n return {\n read: () => current,\n mark: (activities) => {\n const next: Record<string, string> = {};\n for (const activity of activities) next[activity.id] = activity.updatedAt;\n // Identity is the snapshot, so an unchanged map must not become a new\n // object — see `read` above.\n const ids = Object.keys(next);\n const same =\n ids.length === Object.keys(current).length &&\n ids.every((id) => current[id] === next[id]);\n if (same) return;\n current = next;\n writeStored(next);\n for (const listener of listeners) listener();\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/**\n * Whether any of these has moved, or arrived, since the reader last looked.\n *\n * An id with nothing recorded is new — that is the case the per-subject record\n * exists for. An unparseable stamp is treated as new too: the alternative is\n * silently never alerting for a host whose clock format this does not read.\n */\nexport function hasUnseenActivity(\n activities: readonly LiveActivity[],\n seen: SeenMap,\n): boolean {\n return activities.some((activity) => {\n const shown = instant(seen[activity.id]);\n if (shown === null) return true;\n const now = instant(activity.updatedAt);\n return now === null || now > shown;\n });\n}\n","/**\n * Bare bell trigger with the live unread badge — for hosts that do not already\n * have a styled icon-button slot.\n *\n * A host with its own trigger chrome uses `useBellBadge` + `Panel` directly,\n * and NOT `useUnreadCount`, which is what this sentence used to say. That\n * advice was taken, verbatim and by name, by a storefront whose header needed\n * its own trigger — and it gave that storefront a bell showing nothing at all\n * while a live pedido sat in the panel it opens, because `useUnreadCount`\n * counts inbox rows and knows nothing about what is happening right now.\n */\nimport type { JSX } from 'react';\n\nimport { Badge } from '@12-apps/ui/data-display/Badge';\nimport { Box } from '@12-apps/ui/mui/Box';\n\nimport type { NotificationMessages } from '../messages';\n\nimport { useInboxBellBadge, useLiveBellBadge } from './bell-badge';\nimport { BellIcon } from './bell-icon';\nimport type { NotificationsSignalHook, NotificationsSubscribe } from './hooks';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\nimport type { InboxStore } from './inbox-state';\n\nconst triggerSx = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n p: 0.5,\n border: 'none',\n background: 'none',\n cursor: 'pointer',\n color: 'text.primary',\n lineHeight: 0,\n '& *': { cursor: 'pointer' },\n '&:hover': { color: 'primary.main' },\n '&:focus-visible': {\n outline: '2px solid',\n outlineColor: 'primary.main',\n outlineOffset: '2px',\n borderRadius: '50%',\n },\n} as const;\n\nexport interface BellButtonProps {\n onClick: () => void;\n /** Signed-out hosts still mount the bell; `false` silences it. */\n enabled?: boolean;\n}\n\n/**\n * The trigger itself, given a count and whether any of it is NEW.\n *\n * Presentational, and shared by both bells below, so the two can never drift on\n * what the badge looks like — only on where the number comes from.\n *\n * ## The two tones\n *\n * `primary` says *something happened*; `neutral` says *something is present*. A\n * live activity is the reason that distinction has to exist: it stays on the\n * panel for as long as the thing is happening, so a bell that painted every\n * live entry as new would be permanently red for a pedido the reader already\n * looked at, and a bell that ignored them would say nothing at all while one\n * was running. Grey keeps the count honest without spending attention twice.\n */\nfunction BellTrigger({\n onClick,\n count,\n hasNew,\n messages,\n}: {\n onClick: () => void;\n count: number;\n hasNew: boolean;\n messages: NotificationMessages;\n}): JSX.Element {\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onClick}\n // `openBellWithUnread` rather than a new message, and not for want of\n // precision: `NotificationMessages` is REQUIRED of every host, so adding\n // a field is a breaking change to a package several apps already mount.\n // The sentence a host wrote for \"you have N\" is the sentence this wants.\n aria-label={count > 0 ? messages.openBellWithUnread(count) : messages.openBell}\n data-testid=\"notifications-bell\"\n sx={triggerSx}\n >\n <Badge\n content={count > 0 ? count : undefined}\n color={hasNew ? 'primary' : 'neutral'}\n variant=\"count\"\n max={99}\n data-testid=\"notifications-badge\"\n // The tone is carried by a colour, and a colour is not something a\n // test can read — nor, on its own, a signal every reader can. This is\n // what the tests assert on.\n data-tone={hasNew ? 'new' : 'seen'}\n >\n <BellIcon size={28} />\n </Badge>\n </Box>\n );\n}\n\nexport function BellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n}): JSX.Element {\n const badge = useInboxBellBadge(store, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return <BellTrigger onClick={onClick} {...badge} messages={messages} />;\n}\n\n/**\n * The bell for a host that configured live activities.\n *\n * A SECOND component rather than a flag on the one above, because the host's\n * `useActivities` is a hook: reading an optional config inside one component\n * would mean calling it conditionally, which React reports as a crash in some\n * unrelated component rather than here. The factory knows statically which host\n * it is building for and picks one.\n *\n * What the number MEANS, and what it costs the host, is `bell-badge.ts` — the\n * same hook a host with its own trigger chrome reaches through the factory's\n * `useBellBadge`, so the two bells can never disagree about the count.\n */\nexport function LiveBellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n live,\n seen,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n live: LiveActivitiesConfig;\n seen: LiveSeenStore;\n}): JSX.Element {\n const badge = useLiveBellBadge(store, live, seen, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return <BellTrigger onClick={onClick} {...badge} messages={messages} />;\n}\n","/**\n * The inbox slide-over, fetched the first time somebody opens it.\n *\n * The bell and the panel are a PAIR a host drops into its chrome, and that is\n * still true — but only the BELL is on screen when a page paints. The panel is\n * behind a tap, and a static import made every host pay for it up front: the\n * design-system `Drawer` and, through it, MUI's `SwipeableDrawer`, `Modal`,\n * `Slide` and the focus trap, plus the row, the empty state and the pager. On a\n * storefront that is a slide-over most visits never open, parsed before the\n * first screen can render.\n *\n * ## Why the gate is \"ever opened\" rather than `open`\n *\n * `lazy` fetches when a component first RENDERS, so a boundary that still\n * rendered the panel while closed would fetch immediately and buy nothing. This\n * renders `null` until the panel has been open once, which is what actually\n * defers the download to the tap.\n *\n * And once opened it STAYS mounted. Unmounting on close would throw away the\n * drawer's transition state, so the panel would vanish instead of sliding out,\n * and the entrance animation would re-run on every reopen — which someone\n * working through an inbox does repeatedly. The fetch happens once.\n *\n * The initial state reads `open` rather than starting at `false`, so a host that\n * mounts the panel already open renders it in the same commit instead of a frame\n * later.\n *\n * ## Why `null` for the fallback\n *\n * The only frame this can show anything is the one right after the tap, where a\n * spinner reads as a stall rather than as progress. The chunk is small and\n * same-origin.\n */\nimport { Suspense, lazy, useEffect, useState, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\nimport type { NotificationsPanelProps } from './panel';\n\n/** What the factory binds into the panel, and the host never passes. */\ninterface PanelParts {\n store: InboxStore;\n messages: NotificationMessages;\n /** Absent unless the host turned live activities on — see `./live-config`. */\n live?: LiveActivitiesConfig;\n /** Travels with `live`: where the panel records what the reader has seen. */\n liveSeen?: LiveSeenStore;\n}\n\nexport function lazyNotificationsPanel(\n parts: PanelParts,\n): ComponentType<NotificationsPanelProps> {\n const Bound = lazy(async () => {\n const { NotificationsPanel } = await import('./panel');\n return {\n default: (props: NotificationsPanelProps): JSX.Element => (\n <NotificationsPanel {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPanelSlot(props: NotificationsPanelProps): JSX.Element | null {\n const [everOpened, setEverOpened] = useState(props.open);\n\n useEffect(() => {\n if (props.open) setEverOpened(true);\n }, [props.open]);\n\n if (!everOpened) return null;\n\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n","/**\n * The routed preferences screen, fetched when a host actually routes to it.\n *\n * `createWebNotifications` returns two different KINDS of thing, and its own\n * docstring says so: `page` is \"the standalone surface … the one thing a host\n * routes to\", while the bell and the panel \"are a PAIR a host drops into its own\n * chrome\". Chrome is on screen from the first paint; a routed surface is not.\n *\n * A static import made that distinction invisible to a bundler. Every host that\n * put the bell in its header also shipped the preferences matrix — its channel\n * toggles, the per-browser push enable step, and the design-system `Switch`\n * behind them — in the same chunk as the header. A storefront paid for a\n * settings screen a shopper never opens, before its first screen could render;\n * a host that renders its OWN preferences page paid for this one twice.\n *\n * So `page` now loads on demand. Nothing else moves: the bell, the panel and\n * `BellWithPanel` stay exactly as eager as the chrome they belong to, because\n * that is what they are.\n *\n * NO PREFETCH, deliberately, and this is the opposite call from a surface a\n * host opens from chrome it already has. A routed surface is reached by\n * NAVIGATION, and every host here already code-splits its routes — so the\n * fetch happens while the route is being entered, which is the moment a\n * prefetch would have been trying to anticipate. Warming it at factory time\n * would put the screen back on the boot path of every app, which is the whole\n * cost this removes.\n */\nimport { Suspense, lazy, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { NotificationsApiClient } from './api';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/** What the factory binds into the screen, and the host never passes. */\ninterface PreferencesPageParts {\n api: NotificationsApiClient;\n messages: NotificationMessages;\n webPush: WebPushSetupConfig;\n}\n\n/**\n * The routed screen, bound and loaded on first render.\n *\n * `lazy` memoises its factory, so the binding below happens once however many\n * times a host mounts the page — the same guarantee the direct call gave.\n *\n * The fallback is `null` because a host routes to this: whatever it renders\n * around the route is already on screen, and a second spinner inside it would\n * be one more thing appearing and disappearing during a navigation the host is\n * already indicating.\n */\nexport function lazyPreferencesPage(\n parts: PreferencesPageParts,\n): ComponentType<PreferencesScreenProps> {\n const Bound = lazy(async () => {\n const { PreferencesScreen } = await import('./preferences-screen');\n return {\n default: (props: PreferencesScreenProps): JSX.Element => (\n <PreferencesScreen {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPreferencesPage(props: PreferencesScreenProps): JSX.Element {\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n"],"mappings":"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","names":["useState","jsx","Suspense","lazy","jsx","lazy","Suspense","jsx","useState"]}
|
|
@@ -144,6 +144,7 @@ var NOTHING_TO_SHOW = {
|
|
|
144
144
|
function useBadgeState(store, options = {}) {
|
|
145
145
|
const enabled = options.enabled ?? true;
|
|
146
146
|
const subscribe = options.subscribe;
|
|
147
|
+
const relaxed = subscribe !== void 0 || options.live === true;
|
|
147
148
|
const live = useInboxState(store);
|
|
148
149
|
const state = enabled ? live : NOTHING_TO_SHOW;
|
|
149
150
|
options.useSignal?.(() => {
|
|
@@ -155,7 +156,7 @@ function useBadgeState(store, options = {}) {
|
|
|
155
156
|
const unsubscribe = subscribe?.(() => store.invalidate());
|
|
156
157
|
const interval = setInterval(
|
|
157
158
|
() => store.refreshBadge(),
|
|
158
|
-
|
|
159
|
+
relaxed ? BADGE_RECONCILE_MS : BADGE_POLL_MS
|
|
159
160
|
);
|
|
160
161
|
const onFocus = /* @__PURE__ */ __name(() => store.refreshBadge(), "onFocus");
|
|
161
162
|
globalThis.addEventListener?.("focus", onFocus);
|
|
@@ -164,7 +165,7 @@ function useBadgeState(store, options = {}) {
|
|
|
164
165
|
globalThis.removeEventListener?.("focus", onFocus);
|
|
165
166
|
unsubscribe?.();
|
|
166
167
|
};
|
|
167
|
-
}, [store, enabled, subscribe]);
|
|
168
|
+
}, [store, enabled, subscribe, relaxed]);
|
|
168
169
|
return state;
|
|
169
170
|
}
|
|
170
171
|
__name(useBadgeState, "useBadgeState");
|
|
@@ -224,4 +225,4 @@ export {
|
|
|
224
225
|
useInboxList,
|
|
225
226
|
BellIcon
|
|
226
227
|
};
|
|
227
|
-
//# sourceMappingURL=chunk-
|
|
228
|
+
//# sourceMappingURL=chunk-AN6IX4AE.js.map
|