@12-apps/notifications 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/{chunk-YE24MDS6.js → chunk-6HLHQDKS.js} +2 -2
  2. package/dist/{chunk-AHNRSA6U.js → chunk-HQU4R4SG.js} +2 -2
  3. package/dist/{chunk-WU6QJLSZ.js → chunk-QRAXX3GR.js} +2 -2
  4. package/dist/{chunk-ORXJH3VM.js → chunk-TIGTBSAQ.js} +25 -17
  5. package/dist/chunk-TIGTBSAQ.js.map +1 -0
  6. package/dist/{chunk-4TTYQVPK.js → chunk-Y34FX24X.js} +4 -3
  7. package/dist/{chunk-4TTYQVPK.js.map → chunk-Y34FX24X.js.map} +1 -1
  8. package/dist/{create-api-notifications-CgBdjfyF.d.ts → create-api-notifications-B3u6Kx3x.d.ts} +28 -5
  9. package/dist/{create-web-notifications-Du3hTs7P.d.ts → create-web-notifications-BpNR8qH3.d.ts} +1 -1
  10. package/dist/hono/index.d.ts +2 -2
  11. package/dist/hono/index.js +3 -3
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/{jobs-DhDjrAX5.d.ts → jobs-BDZ7aGHV.d.ts} +1 -1
  15. package/dist/manifest/server.d.ts +3 -3
  16. package/dist/manifest/server.js +4 -4
  17. package/dist/manifest/web.d.ts +2 -2
  18. package/dist/manifest/web.js +2 -2
  19. package/dist/react/index.d.ts +3 -3
  20. package/dist/react/index.js +2 -2
  21. package/dist/server/index.d.ts +4 -4
  22. package/dist/server/index.js +4 -4
  23. package/dist/{wire-5IRin4zH.d.ts → wire-6dzyfDE7.d.ts} +31 -5
  24. package/package.json +2 -2
  25. package/src/messages.ts +37 -4
  26. package/src/server/context.ts +10 -0
  27. package/src/server/create-api-notifications.ts +30 -6
  28. package/src/server/routes.ts +32 -16
  29. package/dist/chunk-ORXJH3VM.js.map +0 -1
  30. /package/dist/{chunk-YE24MDS6.js.map → chunk-6HLHQDKS.js.map} +0 -0
  31. /package/dist/{chunk-AHNRSA6U.js.map → chunk-HQU4R4SG.js.map} +0 -0
  32. /package/dist/{chunk-WU6QJLSZ.js.map → chunk-QRAXX3GR.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { N as NotificationGeneratorRegistry } from './generators-B9xt3sRh.js';
2
- import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, c as NotificationWireMessages } from './wire-5IRin4zH.js';
2
+ import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-6dzyfDE7.js';
3
3
  import { b as NotificationChannel, D as DeliveryStatus, f as NotificationLogger, i as NotificationTransport, c as NotificationContent, a as NotificationCategory, d as NotificationEvent, e as NotificationGenerator } from './types-CXLAG3UU.js';
4
4
  import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-Cs14Wp9u.js';
5
5
 
@@ -753,6 +753,16 @@ interface NotificationsRequest {
753
753
  body?: unknown;
754
754
  /** Headers the surface reads (`user-agent`, for the device hint). */
755
755
  headers?: Record<string, string | undefined>;
756
+ /**
757
+ * The language to answer this caller in, as a language tag (`pt-BR`,
758
+ * `en-US`) — the same field `@12-apps/wiring`'s `WireRequest` carries.
759
+ *
760
+ * Populated by the host's adapter, which is the only layer that can
761
+ * negotiate one. Absent is meaningful and not an error: a host with one
762
+ * audience never sets it, and this surface must then answer with the words
763
+ * it was configured with rather than invent a language.
764
+ */
765
+ locale?: string;
756
766
  }
757
767
  /** What a handler answers with; the adapter maps it onto its response type. */
758
768
  interface NotificationsResponse {
@@ -874,8 +884,15 @@ interface NotificationsServerConfig {
874
884
  onInboxChanged?: (userId: string) => void;
875
885
  /** The host's authorization engine, for `notifyByPermission`. */
876
886
  audience?: NotificationAudienceDirectory;
877
- /** User-facing copy overrides (pt-BR product copy by default). */
878
- messages: NotificationWireMessages;
887
+ /**
888
+ * Every user-facing sentence this surface can produce — REQUIRED host config.
889
+ *
890
+ * A pack, or a RESOLVER for a host whose callers do not share a language.
891
+ * Passed to the routes UNRESOLVED: this factory runs once per process (and
892
+ * at least one host memoises its call), so resolving here would answer every
893
+ * later request in the language the process started with.
894
+ */
895
+ messages: NotificationsCopySource<NotificationWireMessages>;
879
896
  /** The host's logger. Defaults to the console. */
880
897
  logger?: NotificationLogger;
881
898
  }
@@ -901,8 +918,14 @@ interface ApiNotifications {
901
918
  registerGenerator: NotificationGeneratorRegistry['register'];
902
919
  /** The declared transports, for diagnostics and availability probes. */
903
920
  transports: TransportRegistry;
904
- /** The copy in force, so a host's own screens can reuse a sentence. */
905
- messages: NotificationWireMessages;
921
+ /**
922
+ * The copy source in force, so a host's own screens can reuse a sentence.
923
+ *
924
+ * The SOURCE rather than a resolved pack, for the same reason the routes get
925
+ * one: a host screen serving two readers must be able to ask per reader.
926
+ * Read it with `messagesOf({ messages }, locale)`.
927
+ */
928
+ messages: NotificationsCopySource<NotificationWireMessages>;
906
929
  }
907
930
  declare function createApiNotifications(config: NotificationsServerConfig): ApiNotifications;
908
931
 
@@ -1,5 +1,5 @@
1
1
  import { JSX, ComponentType } from 'react';
2
- import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-5IRin4zH.js';
2
+ import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-6dzyfDE7.js';
3
3
  import { b as NotificationChannel } from './types-CXLAG3UU.js';
4
4
 
5
5
  /**
@@ -1,8 +1,8 @@
1
1
  import { Hono, Context } from 'hono';
2
- import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-CgBdjfyF.js';
2
+ import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-B3u6Kx3x.js';
3
3
  import '../generators-B9xt3sRh.js';
4
4
  import '../types-CXLAG3UU.js';
5
- import '../wire-5IRin4zH.js';
5
+ import '../wire-6dzyfDE7.js';
6
6
  import '../web-push-Cs14Wp9u.js';
7
7
 
8
8
  /**
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createApiNotifications
3
- } from "../chunk-ORXJH3VM.js";
4
- import "../chunk-AHNRSA6U.js";
3
+ } from "../chunk-TIGTBSAQ.js";
4
+ import "../chunk-HQU4R4SG.js";
5
5
  import {
6
6
  messagesOf
7
- } from "../chunk-4TTYQVPK.js";
7
+ } from "../chunk-Y34FX24X.js";
8
8
  import {
9
9
  __name
10
10
  } from "../chunk-7QVYU63E.js";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { D as DeliveryStatus, N as NOTIFICATION_CHANNELS, a as NotificationCategory, b as NotificationChannel, c as NotificationContent, d as NotificationEvent, e as NotificationGenerator, f as NotificationLogger, g as NotificationRecipient, h as NotificationTaxonomy, i as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-CXLAG3UU.js';
2
2
  export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-B9xt3sRh.js';
3
- export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, d as defaultChannelMatrix, e as enabledChannelsOf, i as inboxWire, m as mergeChoices, f as mergeStoredRow, g as messagesOf } from './wire-5IRin4zH.js';
3
+ export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, d as defaultChannelMatrix, e as enabledChannelsOf, i as inboxWire, m as mergeChoices, f as mergeStoredRow, g as messagesOf } from './wire-6dzyfDE7.js';
4
4
 
5
5
  /** Thrown by `notify` when no generator is registered for the event type. */
6
6
  declare class UnknownNotificationTypeError extends Error {
package/dist/index.js CHANGED
@@ -9,12 +9,12 @@ import {
9
9
  mergeChoices,
10
10
  mergeStoredRow,
11
11
  normalizePhoneE164
12
- } from "./chunk-AHNRSA6U.js";
12
+ } from "./chunk-HQU4R4SG.js";
13
13
  import {
14
14
  NOTIFICATION_CHANNELS,
15
15
  messagesOf,
16
16
  taxonomyOf
17
- } from "./chunk-4TTYQVPK.js";
17
+ } from "./chunk-Y34FX24X.js";
18
18
  import "./chunk-7QVYU63E.js";
19
19
  export {
20
20
  DEFAULT_CHANNEL_ROW,
@@ -1,5 +1,5 @@
1
1
  import { WireJobBlueprint } from '@12-apps/wiring';
2
- import { A as ApiNotifications } from './create-api-notifications-CgBdjfyF.js';
2
+ import { A as ApiNotifications } from './create-api-notifications-B3u6Kx3x.js';
3
3
 
4
4
  /**
5
5
  * The two background jobs getting a message out actually needs.
@@ -1,10 +1,10 @@
1
1
  import * as _12_apps_wiring from '@12-apps/wiring';
2
2
  import { WireRequest } from '@12-apps/wiring';
3
- import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-CgBdjfyF.js';
4
- import { N as NotificationsJobDeps } from '../jobs-DhDjrAX5.js';
3
+ import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-B3u6Kx3x.js';
4
+ import { N as NotificationsJobDeps } from '../jobs-BDZ7aGHV.js';
5
5
  import '../generators-B9xt3sRh.js';
6
6
  import '../types-CXLAG3UU.js';
7
- import '../wire-5IRin4zH.js';
7
+ import '../wire-6dzyfDE7.js';
8
8
  import '../web-push-Cs14Wp9u.js';
9
9
 
10
10
  /** One `NotificationsRoute` as the wiring contract reads it. */
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  NOTIFICATIONS_JOBS
3
- } from "../chunk-WU6QJLSZ.js";
3
+ } from "../chunk-QRAXX3GR.js";
4
4
  import {
5
5
  createApiNotifications
6
- } from "../chunk-ORXJH3VM.js";
7
- import "../chunk-AHNRSA6U.js";
8
- import "../chunk-4TTYQVPK.js";
6
+ } from "../chunk-TIGTBSAQ.js";
7
+ import "../chunk-HQU4R4SG.js";
8
+ import "../chunk-Y34FX24X.js";
9
9
  import {
10
10
  __name
11
11
  } from "../chunk-7QVYU63E.js";
@@ -1,6 +1,6 @@
1
- import { c as createWebNotifications } from '../create-web-notifications-Du3hTs7P.js';
1
+ import { c as createWebNotifications } from '../create-web-notifications-BpNR8qH3.js';
2
2
  import 'react';
3
- import '../wire-5IRin4zH.js';
3
+ import '../wire-6dzyfDE7.js';
4
4
  import '../types-CXLAG3UU.js';
5
5
 
6
6
  /**
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createWebNotifications
3
- } from "../chunk-YE24MDS6.js";
4
- import "../chunk-4TTYQVPK.js";
3
+ } from "../chunk-6HLHQDKS.js";
4
+ import "../chunk-Y34FX24X.js";
5
5
  import "../chunk-7QVYU63E.js";
6
6
 
7
7
  // src/manifest/web.ts
@@ -1,7 +1,7 @@
1
- import { N as NotificationsApiClient } from '../create-web-notifications-Du3hTs7P.js';
2
- export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BellButtonProps, I as InboxListStatus, d as InboxState, e as InboxStore, f as NotificationsHttpError, g as NotificationsPanelProps, h as NotificationsResult, i as NotificationsSignalHook, j as NotificationsSubscribe, k as NotificationsTransport, l as NotificationsWebConfig, P as PAGE_SIZE, m as PreferencesPayload, n as PreferencesScreenProps, o as PushRegistrationPayload, W as WebNotifications, p as WebPushPlatformHint, q as WebPushSetupConfig, r as createInboxStore, s as createNotificationsApiClient, c as createWebNotifications, t as httpNotificationsTransport, u as useInboxList, v as useInboxState, w as useUnreadCount } from '../create-web-notifications-Du3hTs7P.js';
1
+ import { N as NotificationsApiClient } from '../create-web-notifications-BpNR8qH3.js';
2
+ export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BellButtonProps, I as InboxListStatus, d as InboxState, e as InboxStore, f as NotificationsHttpError, g as NotificationsPanelProps, h as NotificationsResult, i as NotificationsSignalHook, j as NotificationsSubscribe, k as NotificationsTransport, l as NotificationsWebConfig, P as PAGE_SIZE, m as PreferencesPayload, n as PreferencesScreenProps, o as PushRegistrationPayload, W as WebNotifications, p as WebPushPlatformHint, q as WebPushSetupConfig, r as createInboxStore, s as createNotificationsApiClient, c as createWebNotifications, t as httpNotificationsTransport, u as useInboxList, v as useInboxState, w as useUnreadCount } from '../create-web-notifications-BpNR8qH3.js';
3
3
  import { JSX } from 'react';
4
- import { N as NotificationMessages } from '../wire-5IRin4zH.js';
4
+ import { N as NotificationMessages } from '../wire-6dzyfDE7.js';
5
5
  import '../types-CXLAG3UU.js';
6
6
 
7
7
  /** Inline SVG bell (no icon-library dependency in this package). */
@@ -16,8 +16,8 @@ import {
16
16
  useInboxList,
17
17
  useInboxState,
18
18
  useUnreadCount
19
- } from "../chunk-YE24MDS6.js";
20
- import "../chunk-4TTYQVPK.js";
19
+ } from "../chunk-6HLHQDKS.js";
20
+ import "../chunk-Y34FX24X.js";
21
21
  import "../chunk-7QVYU63E.js";
22
22
  export {
23
23
  BADGE_POLL_MS,
@@ -1,11 +1,11 @@
1
- import { A as ApiNotifications } from '../create-api-notifications-CgBdjfyF.js';
2
- export { C as CommittedNotification, E as EMAIL_DRIVERS, c as EmailDriver, d as EmailDriverDeclaration, e as EmailMessage, f as ExtraDrivers, L as ListNotificationsInput, g as NotificationAudienceDirectory, h as NotificationChannelPolicy, i as NotificationCommittedListener, j as NotificationContactDirectory, k as NotificationCreateData, l as NotificationDelegate, m as NotificationDeliveryDelegate, n as NotificationDeliveryRow, o as NotificationDeliveryWhere, p as NotificationDispatchScheduler, q as NotificationInboxStore, r as NotificationPageAfter, s as NotificationPreferenceDelegate, t as NotificationPreferenceRow, u as NotificationPreferenceStore, v as NotificationRouter, w as NotificationWhere, a as NotificationsActor, x as NotificationsApiError, y as NotificationsDb, z as NotificationsDbClient, B as NotificationsDbProvider, D as NotificationsRequest, F as NotificationsResponse, b as NotificationsRoute, N as NotificationsServerConfig, G as NotifyByPermission, H as NotifyOptions, I as NotifyResult, P as PermissionNotificationResult, J as PermissionNotificationSkip, K as PushSubscriptionDelegate, M as PushSubscriptionInput, O as PushSubscriptionRow, Q as PushSubscriptionStore, S as SMS_DRIVERS, R as SmsDriver, T as SmsDriverDeclaration, U as SmsMessage, V as TransportDeclaration, W as TransportRegistry, X as WHATSAPP_DRIVERS, Y as WhatsAppDriver, Z as WhatsAppDriverDeclaration, _ as WhatsAppMessage, $ as createApiNotifications, a0 as createTransportRegistry, a1 as emailTransport, a2 as foldApiError, a3 as formatEmail, a4 as formatSms, a5 as formatWhatsApp, a6 as ok, a7 as smsTransport, a8 as whatsAppTransport } from '../create-api-notifications-CgBdjfyF.js';
1
+ import { A as ApiNotifications } from '../create-api-notifications-B3u6Kx3x.js';
2
+ export { C as CommittedNotification, E as EMAIL_DRIVERS, c as EmailDriver, d as EmailDriverDeclaration, e as EmailMessage, f as ExtraDrivers, L as ListNotificationsInput, g as NotificationAudienceDirectory, h as NotificationChannelPolicy, i as NotificationCommittedListener, j as NotificationContactDirectory, k as NotificationCreateData, l as NotificationDelegate, m as NotificationDeliveryDelegate, n as NotificationDeliveryRow, o as NotificationDeliveryWhere, p as NotificationDispatchScheduler, q as NotificationInboxStore, r as NotificationPageAfter, s as NotificationPreferenceDelegate, t as NotificationPreferenceRow, u as NotificationPreferenceStore, v as NotificationRouter, w as NotificationWhere, a as NotificationsActor, x as NotificationsApiError, y as NotificationsDb, z as NotificationsDbClient, B as NotificationsDbProvider, D as NotificationsRequest, F as NotificationsResponse, b as NotificationsRoute, N as NotificationsServerConfig, G as NotifyByPermission, H as NotifyOptions, I as NotifyResult, P as PermissionNotificationResult, J as PermissionNotificationSkip, K as PushSubscriptionDelegate, M as PushSubscriptionInput, O as PushSubscriptionRow, Q as PushSubscriptionStore, S as SMS_DRIVERS, R as SmsDriver, T as SmsDriverDeclaration, U as SmsMessage, V as TransportDeclaration, W as TransportRegistry, X as WHATSAPP_DRIVERS, Y as WhatsAppDriver, Z as WhatsAppDriverDeclaration, _ as WhatsAppMessage, $ as createApiNotifications, a0 as createTransportRegistry, a1 as emailTransport, a2 as foldApiError, a3 as formatEmail, a4 as formatSms, a5 as formatWhatsApp, a6 as ok, a7 as smsTransport, a8 as whatsAppTransport } from '../create-api-notifications-B3u6Kx3x.js';
3
3
  export { D as DriverDeclarationBase, F as FetchImpl, N as NotificationProviderError, W as WEB_PUSH_DRIVERS, a as WebPushDriverDeclaration, b as WebPushMessage, c as WebPushSender, d as WebPushSubscription, e as WebPushSubscriptionSource, f as absoluteLink, g as formatWebPush, w as webPushTransport } from '../web-push-Cs14Wp9u.js';
4
4
  import { NotifyPort } from '@12-apps/wiring/ports';
5
- export { a as NOTIFICATIONS_DRAIN_CRON, b as NOTIFICATIONS_DRAIN_LEASE_MS, c as NOTIFICATIONS_JOBS, d as NOTIFICATIONS_SWEEP_QUEUE, N as NotificationsJobDeps } from '../jobs-DhDjrAX5.js';
5
+ export { a as NOTIFICATIONS_DRAIN_CRON, b as NOTIFICATIONS_DRAIN_LEASE_MS, c as NOTIFICATIONS_JOBS, d as NOTIFICATIONS_SWEEP_QUEUE, N as NotificationsJobDeps } from '../jobs-BDZ7aGHV.js';
6
6
  import '../generators-B9xt3sRh.js';
7
7
  import '../types-CXLAG3UU.js';
8
- import '../wire-5IRin4zH.js';
8
+ import '../wire-6dzyfDE7.js';
9
9
  import '@12-apps/wiring';
10
10
 
11
11
  /**
@@ -4,7 +4,7 @@ import {
4
4
  NOTIFICATIONS_JOBS,
5
5
  NOTIFICATIONS_SWEEP_QUEUE,
6
6
  wireNotifyPort
7
- } from "../chunk-WU6QJLSZ.js";
7
+ } from "../chunk-QRAXX3GR.js";
8
8
  import {
9
9
  EMAIL_DRIVERS,
10
10
  NotificationProviderError,
@@ -25,9 +25,9 @@ import {
25
25
  smsTransport,
26
26
  webPushTransport,
27
27
  whatsAppTransport
28
- } from "../chunk-ORXJH3VM.js";
29
- import "../chunk-AHNRSA6U.js";
30
- import "../chunk-4TTYQVPK.js";
28
+ } from "../chunk-TIGTBSAQ.js";
29
+ import "../chunk-HQU4R4SG.js";
30
+ import "../chunk-Y34FX24X.js";
31
31
  import "../chunk-7QVYU63E.js";
32
32
  export {
33
33
  EMAIL_DRIVERS,
@@ -83,7 +83,20 @@ interface NotificationMessages extends NotificationWireMessages {
83
83
  devicePushEnabling: string;
84
84
  }
85
85
  /**
86
- * The messages in force.
86
+ * What a copy field takes once its words can follow a reader.
87
+ *
88
+ * Declared here rather than imported from `@12-apps/i18n`: this package must
89
+ * stay liftable into a repo that has never heard of it, so the two agree
90
+ * STRUCTURALLY and nothing forces the dependency. The context is deliberately
91
+ * loose — a raw tag off the wire, unnarrowed — because matching it is the host
92
+ * resolver's job, not this package's.
93
+ */
94
+ type NotificationsCopyResolver<T> = (context: {
95
+ readonly locale?: string | null;
96
+ }) => T;
97
+ type NotificationsCopySource<T> = T | NotificationsCopyResolver<T>;
98
+ /**
99
+ * The messages in force, for ONE reader.
87
100
  *
88
101
  * A pass-through rather than a merge: there is nothing left to merge WITH, and
89
102
  * that is the point of the change. The old version spread the host's table over
@@ -94,11 +107,24 @@ interface NotificationMessages extends NotificationWireMessages {
94
107
  * the same screen.
95
108
  *
96
109
  * Kept as a function because all three mounts read it off a config object, and
97
- * because a later rule (a blank-string refusal, say) belongs in one place.
110
+ * because a later rule (a blank-string refusal, say) belongs in one place —
111
+ * which is exactly what made it the right place to put the RESOLUTION when the
112
+ * field learned to take a resolver.
113
+ *
114
+ * **Call it where the sentence is used.** `createApiNotifications` runs once
115
+ * per process, and at least one host memoises its call behind an `if
116
+ * (assembled) return assembled;`, so a value read there answers every later
117
+ * request in the language the process started with — and a single-locale host
118
+ * cannot tell the difference. The route handlers call it per request; the
119
+ * parsers below them keep taking a plain pack, so one request resolves exactly
120
+ * once and no helper can disagree with another about the language.
121
+ *
122
+ * The generic survives the widening: a host whose pack carries extra keys of
123
+ * its own still gets them back, resolver or not.
98
124
  */
99
125
  declare function messagesOf<T extends NotificationWireMessages>(config: {
100
- messages: T;
101
- }): T;
126
+ messages: NotificationsCopySource<T>;
127
+ }, locale?: string): T;
102
128
 
103
129
  /**
104
130
  * The preference POLICY, with no storage in it (12-15): which channels a
@@ -194,4 +220,4 @@ interface NotificationRow {
194
220
  /** Row → wire. Dates become ISO strings; a null `data` becomes `{}`. */
195
221
  declare function inboxWire(row: NotificationRow): InboxNotification;
196
222
 
197
- export { type ChannelMatrix as C, DEFAULT_CHANNEL_ROW as D, type InboxNotification as I, type ListNotificationsResult as L, type NotificationMessages as N, type ChannelRow as a, type NotificationRow as b, type NotificationWireMessages as c, defaultChannelMatrix as d, enabledChannelsOf as e, mergeStoredRow as f, messagesOf as g, inboxWire as i, mergeChoices as m };
223
+ export { type ChannelMatrix as C, DEFAULT_CHANNEL_ROW as D, type InboxNotification as I, type ListNotificationsResult as L, type NotificationMessages as N, type ChannelRow as a, type NotificationRow as b, type NotificationWireMessages as c, defaultChannelMatrix as d, enabledChannelsOf as e, mergeStoredRow as f, messagesOf as g, type NotificationsCopySource as h, inboxWire as i, mergeChoices as m };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/notifications",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
@@ -51,7 +51,7 @@
51
51
  "prisma:sync:check": "node scripts/sync-notifications-schema.mjs --check"
52
52
  },
53
53
  "dependencies": {
54
- "@12-apps/ui": "^6.10.0"
54
+ "@12-apps/ui": "^6.11.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@12-apps/wiring": ">=1.3.0",
package/src/messages.ts CHANGED
@@ -85,7 +85,21 @@ export interface NotificationMessages extends NotificationWireMessages {
85
85
  }
86
86
 
87
87
  /**
88
- * The messages in force.
88
+ * What a copy field takes once its words can follow a reader.
89
+ *
90
+ * Declared here rather than imported from `@12-apps/i18n`: this package must
91
+ * stay liftable into a repo that has never heard of it, so the two agree
92
+ * STRUCTURALLY and nothing forces the dependency. The context is deliberately
93
+ * loose — a raw tag off the wire, unnarrowed — because matching it is the host
94
+ * resolver's job, not this package's.
95
+ */
96
+ export type NotificationsCopyResolver<T> = (context: {
97
+ readonly locale?: string | null;
98
+ }) => T;
99
+ export type NotificationsCopySource<T> = T | NotificationsCopyResolver<T>;
100
+
101
+ /**
102
+ * The messages in force, for ONE reader.
89
103
  *
90
104
  * A pass-through rather than a merge: there is nothing left to merge WITH, and
91
105
  * that is the point of the change. The old version spread the host's table over
@@ -96,8 +110,27 @@ export interface NotificationMessages extends NotificationWireMessages {
96
110
  * the same screen.
97
111
  *
98
112
  * Kept as a function because all three mounts read it off a config object, and
99
- * because a later rule (a blank-string refusal, say) belongs in one place.
113
+ * because a later rule (a blank-string refusal, say) belongs in one place —
114
+ * which is exactly what made it the right place to put the RESOLUTION when the
115
+ * field learned to take a resolver.
116
+ *
117
+ * **Call it where the sentence is used.** `createApiNotifications` runs once
118
+ * per process, and at least one host memoises its call behind an `if
119
+ * (assembled) return assembled;`, so a value read there answers every later
120
+ * request in the language the process started with — and a single-locale host
121
+ * cannot tell the difference. The route handlers call it per request; the
122
+ * parsers below them keep taking a plain pack, so one request resolves exactly
123
+ * once and no helper can disagree with another about the language.
124
+ *
125
+ * The generic survives the widening: a host whose pack carries extra keys of
126
+ * its own still gets them back, resolver or not.
100
127
  */
101
- export function messagesOf<T extends NotificationWireMessages>(config: { messages: T }): T {
102
- return config.messages;
128
+ export function messagesOf<T extends NotificationWireMessages>(
129
+ config: { messages: NotificationsCopySource<T> },
130
+ locale?: string,
131
+ ): T {
132
+ const source = config.messages;
133
+ return typeof source === 'function'
134
+ ? (source as NotificationsCopyResolver<T>)({ locale })
135
+ : source;
103
136
  }
@@ -32,6 +32,16 @@ export interface NotificationsRequest {
32
32
  body?: unknown;
33
33
  /** Headers the surface reads (`user-agent`, for the device hint). */
34
34
  headers?: Record<string, string | undefined>;
35
+ /**
36
+ * The language to answer this caller in, as a language tag (`pt-BR`,
37
+ * `en-US`) — the same field `@12-apps/wiring`'s `WireRequest` carries.
38
+ *
39
+ * Populated by the host's adapter, which is the only layer that can
40
+ * negotiate one. Absent is meaningful and not an error: a host with one
41
+ * audience never sets it, and this surface must then answer with the words
42
+ * it was configured with rather than invent a language.
43
+ */
44
+ locale?: string;
35
45
  }
36
46
 
37
47
  /** What a handler answers with; the adapter maps it onto its response type. */
@@ -1,5 +1,8 @@
1
1
  import { createGeneratorRegistry, type NotificationGeneratorRegistry } from '../generators';
2
- import { messagesOf, type NotificationWireMessages } from '../messages';
2
+ import type {
3
+ NotificationsCopySource,
4
+ NotificationWireMessages,
5
+ } from '../messages';
3
6
  import type { ChannelRow } from '../preferences-core';
4
7
  import {
5
8
  taxonomyOf,
@@ -104,8 +107,15 @@ export interface NotificationsServerConfig {
104
107
  onInboxChanged?: (userId: string) => void;
105
108
  /** The host's authorization engine, for `notifyByPermission`. */
106
109
  audience?: NotificationAudienceDirectory;
107
- /** User-facing copy overrides (pt-BR product copy by default). */
108
- messages: NotificationWireMessages;
110
+ /**
111
+ * Every user-facing sentence this surface can produce — REQUIRED host config.
112
+ *
113
+ * A pack, or a RESOLVER for a host whose callers do not share a language.
114
+ * Passed to the routes UNRESOLVED: this factory runs once per process (and
115
+ * at least one host memoises its call), so resolving here would answer every
116
+ * later request in the language the process started with.
117
+ */
118
+ messages: NotificationsCopySource<NotificationWireMessages>;
109
119
  /** The host's logger. Defaults to the console. */
110
120
  logger?: NotificationLogger;
111
121
  }
@@ -132,8 +142,14 @@ export interface ApiNotifications {
132
142
  registerGenerator: NotificationGeneratorRegistry['register'];
133
143
  /** The declared transports, for diagnostics and availability probes. */
134
144
  transports: TransportRegistry;
135
- /** The copy in force, so a host's own screens can reuse a sentence. */
136
- messages: NotificationWireMessages;
145
+ /**
146
+ * The copy source in force, so a host's own screens can reuse a sentence.
147
+ *
148
+ * The SOURCE rather than a resolved pack, for the same reason the routes get
149
+ * one: a host screen serving two readers must be able to ask per reader.
150
+ * Read it with `messagesOf({ messages }, locale)`.
151
+ */
152
+ messages: NotificationsCopySource<NotificationWireMessages>;
137
153
  }
138
154
 
139
155
  /** Drop the keys the host left unset, so an absent seam stays absent. */
@@ -150,7 +166,15 @@ const consoleLogger: NotificationLogger = {
150
166
  };
151
167
 
152
168
  export function createApiNotifications(config: NotificationsServerConfig): ApiNotifications {
153
- const messages = messagesOf(config);
169
+ /**
170
+ * The SOURCE travels; nothing is resolved here.
171
+ *
172
+ * This factory runs once per process — and at least one host memoises its
173
+ * call behind an `if (assembled) return assembled;` — so a `messagesOf(config)`
174
+ * on this line would word every later request in the language the process
175
+ * started with. The handlers resolve per request instead.
176
+ */
177
+ const messages = config.messages;
154
178
  const taxonomy = taxonomyOf(config);
155
179
  const logger = config.logger ?? consoleLogger;
156
180
 
@@ -1,4 +1,8 @@
1
- import type { NotificationWireMessages } from '../messages';
1
+ import {
2
+ messagesOf,
3
+ type NotificationsCopySource,
4
+ type NotificationWireMessages,
5
+ } from '../messages';
2
6
  import { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';
3
7
 
4
8
  import {
@@ -40,7 +44,11 @@ interface NotificationRoutesDeps {
40
44
  transports: TransportRegistry;
41
45
  contacts: NotificationContactDirectory;
42
46
  categories: readonly string[];
43
- messages: NotificationWireMessages;
47
+ /**
48
+ * The SOURCE, not a resolved pack — the route table is built once per
49
+ * process and every handler below runs per request.
50
+ */
51
+ messages: NotificationsCopySource<NotificationWireMessages>;
44
52
  /** Told when a write actually changed something (for a realtime hint). */
45
53
  onInboxChanged?: (userId: string) => void;
46
54
  }
@@ -98,8 +106,13 @@ function inboxRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
98
106
  {
99
107
  method: 'GET',
100
108
  path: '/notifications',
101
- handle: guarded(async ({ actor, query }) =>
102
- ok(await deps.inbox.list(actor.userId, parseListQuery(query, deps.messages))),
109
+ handle: guarded(async ({ actor, query, locale }) =>
110
+ ok(
111
+ await deps.inbox.list(
112
+ actor.userId,
113
+ parseListQuery(query, messagesOf(deps, locale)),
114
+ ),
115
+ ),
103
116
  ),
104
117
  },
105
118
  {
@@ -113,8 +126,8 @@ function inboxRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
113
126
  {
114
127
  method: 'POST',
115
128
  path: '/notifications/mark-read',
116
- handle: guarded(async ({ actor, body }) => {
117
- const target = parseMarkReadBody(body, deps.messages);
129
+ handle: guarded(async ({ actor, body, locale }) => {
130
+ const target = parseMarkReadBody(body, messagesOf(deps, locale));
118
131
  const updated =
119
132
  'all' in target
120
133
  ? await deps.inbox.markAllRead(actor.userId)
@@ -131,10 +144,10 @@ function inboxRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
131
144
  method: 'POST',
132
145
  // POST, not DELETE, because the ids travel in a JSON body.
133
146
  path: '/notifications/delete',
134
- handle: guarded(async ({ actor, body }) => {
147
+ handle: guarded(async ({ actor, body, locale }) => {
135
148
  const deleted = await deps.inbox.softDelete(
136
149
  actor.userId,
137
- parseDeleteBody(body, deps.messages),
150
+ parseDeleteBody(body, messagesOf(deps, locale)),
138
151
  );
139
152
  // Same rule as mark-read. A delete can move the badge too — an UNREAD
140
153
  // row that is removed takes its place in the count with it.
@@ -155,10 +168,13 @@ function preferenceRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
155
168
  {
156
169
  method: 'PUT',
157
170
  path: '/notification-preferences',
158
- handle: guarded(async ({ actor, body }) => {
171
+ handle: guarded(async ({ actor, body, locale }) => {
159
172
  // The dispatch pipeline reads these on every emit, so a save takes
160
173
  // effect immediately — no cache to invalidate.
161
- await deps.preferences.save(actor.userId, parsePreferencesBody(body, deps.messages));
174
+ await deps.preferences.save(
175
+ actor.userId,
176
+ parsePreferencesBody(body, messagesOf(deps, locale)),
177
+ );
162
178
  return ok(await preferencesPayload(deps, actor.userId));
163
179
  }),
164
180
  },
@@ -170,8 +186,8 @@ function pushRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
170
186
  {
171
187
  method: 'GET',
172
188
  path: '/push-subscriptions',
173
- handle: guarded(async ({ actor, query }) => {
174
- const endpoint = parsePushEndpointQuery(query, deps.messages);
189
+ handle: guarded(async ({ actor, query, locale }) => {
190
+ const endpoint = parsePushEndpointQuery(query, messagesOf(deps, locale));
175
191
  return ok({
176
192
  // null = web push is not configured on this deployment.
177
193
  vapidPublicKey: deps.transports.webPushPublicKey(),
@@ -188,8 +204,8 @@ function pushRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
188
204
  {
189
205
  method: 'POST',
190
206
  path: '/push-subscriptions',
191
- handle: guarded(async ({ actor, body, headers }) => {
192
- const input = parsePushSubscriptionBody(body, deps.messages);
207
+ handle: guarded(async ({ actor, body, headers, locale }) => {
208
+ const input = parsePushSubscriptionBody(body, messagesOf(deps, locale));
193
209
  const userAgent = headers?.['user-agent'];
194
210
  await deps.pushSubscriptions.save(actor.userId, {
195
211
  ...input,
@@ -202,10 +218,10 @@ function pushRoutes(deps: NotificationRoutesDeps): NotificationsRoute[] {
202
218
  method: 'DELETE',
203
219
  // The endpoint is a long opaque URL, unusable as a path param.
204
220
  path: '/push-subscriptions',
205
- handle: guarded(async ({ actor, body }) => {
221
+ handle: guarded(async ({ actor, body, locale }) => {
206
222
  await deps.pushSubscriptions.remove(
207
223
  actor.userId,
208
- parsePushEndpointBody(body, deps.messages),
224
+ parsePushEndpointBody(body, messagesOf(deps, locale)),
209
225
  );
210
226
  return ok({ count: await deps.pushSubscriptions.count(actor.userId) });
211
227
  }),