@12-apps/notifications 2.1.0 → 4.0.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/ADOPTING.md CHANGED
@@ -20,7 +20,7 @@ sweep — endpoints and screens included.
20
20
  | **Core** | `@12-apps/notifications` | Nothing to wire — the framework-free, storage-free vocabulary both halves share: types, the generator registry, the preference policy, the phone rules, the copy table, the inbox wire shape. Safe in a browser. |
21
21
  | **Server** | `@12-apps/notifications/server` | Call `createApiNotifications({ db, contacts, transports })` and mount the `routes` it returns — the nine inbox / preferences / push-subscription endpoints, with parsing, statuses and the `{ data }` envelope inside. It also returns `notify` (the emit front door), `notifyByPermission`, `dispatchDeliveries`, `drainPending` and the three stores. |
22
22
  | **Hono** | `@12-apps/notifications/hono` | `const notifications = notificationsRouter({ ...serverConfig, resolveActor }); app.route('/api/account', notifications.router)`. A one-call mount; `hono` is an OPTIONAL peer, so importing the root, `/server` or `/react` never resolves it. |
23
- | **React** | `@12-apps/notifications/react` | Call `createWebNotifications({ apiBase })`. `BellWithPanel` is the whole feature as one element; `BellButton` + `Panel` are the pair for a host with its own chrome; `page` is the preferences screen you route to. pt-BR product copy and the future-pay test ids ship inside. |
23
+ | **React** | `@12-apps/notifications/react` | Call `createWebNotifications({ apiBase })`. `BellWithPanel` is the whole feature as one element; `BellButton` + `Panel` are the pair for a host with its own chrome; `page` is the preferences screen you route to. pt-BR product copy and the origin host's test ids ship inside. |
24
24
  | **Web Push** | `@12-apps/notifications/web-push` | `sender: vapidPushSender({ subject, publicKey, privateKey })` on the `WEB_PUSH` declaration. Its own subpath because it is the only piece that needs `web-push` — an OPTIONAL peer a host that never enables the channel never installs. |
25
25
  | **Prisma** | `prisma/notifications.prisma` + `prisma/migrations/*` | Run `pnpm --filter @12-apps/notifications prisma:sync -- <host schema dir>`: the partial is **COPIED** into the host's multi-file schema folder — never symlinked (a symlinked migration is silently skipped by Prisma; a symlinked partial dangles under `turbo prune`). Migrations are discovered structurally from the installed package's `prisma/migrations` by the host's plugin-migration sync. |
26
26
 
@@ -35,7 +35,7 @@ sweep — endpoints and screens included.
35
35
  someone else's inbox would be a different surface with a different actor.
36
36
 
37
37
  2. **`contacts` is not optional, and it is not the `users` table.** A transport
38
- needs an address: `getContact(userId) → { email, phone } | null`. future-pay's
38
+ needs an address: `getContact(userId) → { email, phone } | null`. The origin host's
39
39
  router read `users.email` / `users.phone` directly, which is the one thing in
40
40
  the pipeline that was always the host's — a package cannot know the shape of
41
41
  an identity table, and a host with phone VERIFICATION wants to answer the
@@ -105,8 +105,8 @@ sweep — endpoints and screens included.
105
105
  it with hand-written SQL. The argument shapes are CLOSED (documented in
106
106
  `src/server/db.ts`), so a non-Prisma host has a finite surface to fill.
107
107
 
108
- 5. **`categories` is product vocabulary.** The default is future-pay's four
109
- (`orders` / `payments` / `stock` / `system`) and the preferences screen
108
+ 5. **`categories` is product vocabulary, and REQUIRED.** There is no default:
109
+ the host declares its own taxonomy, and the preferences screen
110
110
  renders whatever the api half was given — the taxonomy travels on the
111
111
  `GET /notification-preferences` payload, so the two halves cannot disagree
112
112
  about it. The packaged migration deliberately puts **no CHECK** on
@@ -115,7 +115,7 @@ sweep — endpoints and screens included.
115
115
  6. **`generators` are registered from the OUTSIDE, and stay the host's.** A
116
116
  generator maps a domain event to `{ title, body, link, data }`, and the
117
117
  events are exactly what does not port: `order.paid`, `stock.low` and
118
- `short-payment` are Future Pay's, not any host's. Pass them in `generators`,
118
+ `short-payment` are the origin host's, not any host's. Pass them in `generators`,
119
119
  or call `registerGenerator` for a module imported later. The generator's
120
120
  `category` is what the router gates fan-out on.
121
121
 
@@ -184,7 +184,7 @@ sweep — endpoints and screens included.
184
184
  notified — and the log line that tells "nobody holds it" apart from "every
185
185
  dispatch failed". Two rules for your implementation:
186
186
 
187
- - `listCandidates` must be **bounded to people who hold a role**. future-pay's
187
+ - `listCandidates` must be **bounded to people who hold a role**. The origin host's
188
188
  requires a role grant, which keeps a store's storefront BUYERS out of a loop
189
189
  that resolves permissions one user at a time.
190
190
  - `getPermissions` must be **scoped to the tenant**. Unioning a user's grants
@@ -222,7 +222,7 @@ sweep — endpoints and screens included.
222
222
 
223
223
  13. **These endpoints are cookie-authenticated WRITES. CSRF is yours.** Eight of
224
224
  the nine change state, and `resolveActor` typically reads a session cookie —
225
- so the host, not this package, owns the cross-site question. In future-pay it
225
+ so the host, not this package, owns the cross-site question. In the origin host it
226
226
  is fully mitigated by the Auth.js cookie's `SameSite=Lax`; a host
227
227
  authenticating with `SameSite=None`, or one whose `resolveActor` trusts a
228
228
  header a proxy sets, inherits an unguarded write surface. The one that matters
package/README.md CHANGED
@@ -79,7 +79,7 @@ which no sweep selects again.
79
79
 
80
80
  ## The endpoints
81
81
 
82
- Mounted under whatever prefix the host chooses (future-pay: `/api/account`):
82
+ Mounted under whatever prefix the host chooses (the origin host: `/api/account`):
83
83
 
84
84
  | | |
85
85
  |---|---|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/notifications",
3
- "version": "2.1.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "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.",
6
6
  "exports": {
@@ -22,7 +22,7 @@
22
22
  "prisma:sync:check": "node scripts/sync-notifications-schema.mjs --check"
23
23
  },
24
24
  "dependencies": {
25
- "@12-apps/ui": "^4.1.0"
25
+ "@12-apps/ui": "^5.0.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "hono": ">=4.0.0",
@@ -24,7 +24,7 @@
24
24
  --
25
25
  -- ============================ REPLAY SAFETY ================================
26
26
  -- Every statement is guarded, because the first adopters ALREADY HAVE these
27
- -- tables: future-pay created them by hand before the package existed, so this
27
+ -- tables: the origin host created them by hand before the package existed, so this
28
28
  -- migration must be a no-op there and correct on an empty database.
29
29
  --
30
30
  -- The guards are per COLUMN, not per table. `CREATE TABLE IF NOT EXISTS` alone
@@ -22,7 +22,7 @@
22
22
  // and for a stronger reason: the category set is HOST vocabulary (`categories`
23
23
  // on the server config), so a closed set in the schema would be wrong for every
24
24
  // adopter but the first. A host that wants its own taxonomy enforced adds the
25
- // CHECK in a migration of its own — future-pay does. Only `channel` and `status`
25
+ // CHECK in a migration of its own — the origin host does. Only `channel` and `status`
26
26
  // on the delivery row are closed here, because those two are the LIBRARY's.
27
27
  model Notification {
28
28
  id String @id @default(uuid())
@@ -20,7 +20,7 @@
20
20
  * The host package that owns the schema folder MUST also declare this package
21
21
  * as a dependency, so the source of the copy is present in every build context.
22
22
  *
23
- * Default host path follows the future-pay layout
23
+ * Default host path follows the origin host layout
24
24
  * (`packages/prisma/prisma/schema/`); another repo passes its own schema folder
25
25
  * as the positional argument, or sets NOTIFICATIONS_HOST_SCHEMA_DIR.
26
26
  */
package/src/generators.ts CHANGED
@@ -4,7 +4,7 @@ import type { NotificationGenerator } from './types';
4
4
  /**
5
5
  * Generator registry: one {@link NotificationGenerator} per event `type`.
6
6
  *
7
- * INSTANCE state, not a module-level Map. future-pay's original was
7
+ * INSTANCE state, not a module-level Map. the origin's own was
8
8
  * process-wide, which is what a package of loose functions forces; a factory
9
9
  * config does not need it, and one registry per mount is what makes a test (or
10
10
  * a second mount) able to hold its own set without clearing anyone else's.
package/src/index.ts CHANGED
@@ -59,8 +59,8 @@ export {
59
59
  export { normalizePhoneE164, type PhoneNormalizeOptions } from './phone';
60
60
 
61
61
  export {
62
- DEFAULT_NOTIFICATION_MESSAGES,
63
62
  messagesOf,
63
+ type NotificationWireMessages,
64
64
  type NotificationMessages,
65
65
  } from './messages';
66
66
 
package/src/messages.ts CHANGED
@@ -1,21 +1,47 @@
1
1
  /**
2
- * Every sentence this package can say to a USER, in one table.
2
+ * Every sentence this package can say to a USER, stated by the HOST.
3
3
  *
4
- * pt-BR by default because that is the product copy the surface shipped with
5
- * (future-pay's storefront and backoffice are Brazilian); a host in another
6
- * market passes `messages` and overrides the subset it cares about. The copy
7
- * lives HERE rather than in each screen so the api half and the react half can
8
- * never disagree about a sentence — the 401 body the wire returns and the
9
- * error the panel renders come from the same key.
4
+ * The copy lives in ONE table rather than in each screen so the api half and
5
+ * the react half can never disagree about a sentence — the 401 body the wire
6
+ * returns and the error the panel renders come from the same key.
7
+ *
8
+ * THE pt-BR TABLE THAT USED TO BE THE DEFAULT IS GONE. Its own docstring said
9
+ * what it was: "the product copy the surface shipped with", labelled in the
10
+ * source as one named application's "exact copy". A description of one adopter,
11
+ * shipped inside the package every other adopter installs, and reached by
12
+ * saying nothing.
13
+ *
14
+ * `categoryLabels` is the sharpest of the forty. The categories themselves
15
+ * became required config in the release before this one, precisely because
16
+ * WHICH categories exist is product vocabulary — and their LABELS kept
17
+ * defaulting, so a host that declared `['loans', 'fines']` got a labels map
18
+ * describing somebody else's four. Required categories with defaulted labels
19
+ * for a different host's categories is not a smaller version of the bug; it is
20
+ * the same bug with a compile-time gesture in front of it.
21
+ *
22
+ * So `messages` is REQUIRED and whole. The interface is the checklist, and the
23
+ * compiler names the sentences a host has not written yet.
24
+ */
25
+ /**
26
+ * The sentences the SERVER half renders — and the whole of what a backend mount
27
+ * has to state.
28
+ *
29
+ * Split out when `messages` became required. Requiring the full forty on a
30
+ * server config would have made a backend-only adopter write three dozen
31
+ * sentences for screens it does not serve, which is the kind of tax that gets a
32
+ * required-config migration reverted rather than adopted. These four are the
33
+ * ones the router and the route descriptors actually put on a wire.
10
34
  */
11
- export interface NotificationMessages {
12
- // --- the wire ------------------------------------------------------------
35
+ export interface NotificationWireMessages {
13
36
  unauthenticated: string;
14
37
  invalidBody: string;
15
38
  operationFailed: string;
16
39
  /** `POST /notifications/mark-read` with neither `ids` nor `all`. */
17
40
  markReadTargetRequired: string;
41
+ }
18
42
 
43
+ /** Every sentence, wire and screen — what the REACT half needs. */
44
+ export interface NotificationMessages extends NotificationWireMessages {
19
45
  // --- the inbox panel -----------------------------------------------------
20
46
  panelTitle: string;
21
47
  markAllRead: string;
@@ -58,99 +84,20 @@ export interface NotificationMessages {
58
84
  devicePushEnabling: string;
59
85
  }
60
86
 
61
- /** future-pay's exact copy — the product default. */
62
- export const DEFAULT_NOTIFICATION_MESSAGES: NotificationMessages = {
63
- unauthenticated: 'Não autenticado.',
64
- invalidBody: 'Dados inválidos.',
65
- operationFailed: 'Não foi possível concluir a operação.',
66
- markReadTargetRequired: 'Informe `ids` ou `all: true` (exatamente um).',
67
-
68
- panelTitle: 'Notificações',
69
- markAllRead: 'Marcar todas como lidas',
70
- loading: 'Carregando notificações...',
71
- loadMore: 'Carregar mais',
72
- loadingMore: 'Carregando...',
73
- loadFailedTitle: 'Não foi possível carregar',
74
- loadFailedBody: 'Tente novamente em instantes.',
75
- retry: 'Tentar novamente',
76
- emptyTitle: 'Nenhuma notificação',
77
- emptyBody: 'Você está em dia — novidades aparecem aqui.',
78
- openBell: 'Abrir notificações',
79
- openBellWithUnread: (count) => `Abrir notificações (${count} não lidas)`,
80
- unreadSuffix: 'não lida',
81
- deleteOne: (title) => `Excluir notificação: ${title}`,
82
-
83
- justNow: 'agora',
84
- minutesAgo: (minutes) => `há ${minutes} min`,
85
- hoursAgo: (hours) => `há ${hours} h`,
86
- daysAgo: (days) => (days === 1 ? 'há 1 dia' : `há ${days} dias`),
87
- dateLocale: 'pt-BR',
88
-
89
- preferencesTitle: 'Notificações',
90
- preferencesLead:
91
- 'Escolha como quer ser avisado, por tipo de assunto. O sino do app sempre recebe tudo.',
92
- channelLabels: {
93
- EMAIL: 'E-mail',
94
- SMS: 'SMS',
95
- WHATSAPP: 'WhatsApp',
96
- WEB_PUSH: 'Navegador',
97
- },
98
- channelUnavailableHints: {
99
- EMAIL: 'Envio de e-mail não está configurado neste ambiente.',
100
- SMS: 'Cadastre um telefone no seu perfil para receber SMS.',
101
- WHATSAPP: 'Cadastre um telefone no seu perfil para receber WhatsApp.',
102
- WEB_PUSH: 'Alertas do navegador não estão configurados neste ambiente.',
103
- },
104
- categoryLabels: {
105
- orders: {
106
- title: 'Pedidos',
107
- description: 'Confirmações e andamento dos seus pedidos.',
108
- },
109
- payments: {
110
- title: 'Pagamentos',
111
- description: 'Cobranças, comprovantes e falhas de pagamento.',
112
- },
113
- stock: {
114
- title: 'Estoque',
115
- description: 'Alertas de estoque das lojas que você administra.',
116
- },
117
- system: {
118
- title: 'Sistema',
119
- description: 'Avisos da sua conta e da plataforma.',
120
- },
121
- },
122
- categoryFallbackTitle: (category) => category,
123
- devicePushTitle: 'Alertas neste navegador',
124
- devicePushIdle: 'Permita notificações para receber alertas mesmo com o site fechado.',
125
- devicePushOn: 'Este navegador está recebendo alertas.',
126
- devicePushDenied:
127
- 'Permissão negada — habilite notificações nas configurações do navegador.',
128
- devicePushFailed: 'Não foi possível ativar. Tente novamente.',
129
- devicePushEnable: 'Ativar',
130
- devicePushEnabling: 'Ativando...',
131
- };
132
-
133
- /** The messages in force, defaulted to the pt-BR product copy. */
134
- export function messagesOf(config: {
135
- messages?: Partial<NotificationMessages>;
136
- }): NotificationMessages {
137
- const overrides = config.messages ?? {};
138
- return {
139
- ...DEFAULT_NOTIFICATION_MESSAGES,
140
- ...overrides,
141
- // Nested records merge per KEY. A host relabelling one channel must not
142
- // erase the labels for the other three, which a shallow spread would do.
143
- channelLabels: {
144
- ...DEFAULT_NOTIFICATION_MESSAGES.channelLabels,
145
- ...overrides.channelLabels,
146
- },
147
- channelUnavailableHints: {
148
- ...DEFAULT_NOTIFICATION_MESSAGES.channelUnavailableHints,
149
- ...overrides.channelUnavailableHints,
150
- },
151
- categoryLabels: {
152
- ...DEFAULT_NOTIFICATION_MESSAGES.categoryLabels,
153
- ...overrides.categoryLabels,
154
- },
155
- };
87
+ /**
88
+ * The messages in force.
89
+ *
90
+ * A pass-through rather than a merge: there is nothing left to merge WITH, and
91
+ * that is the point of the change. The old version spread the host's table over
92
+ * the origin's, including PER KEY inside `channelLabels`,
93
+ * `channelUnavailableHints` and `categoryLabels` — so a host that relabelled one
94
+ * channel kept the origin's wording for the other three, and a host that
95
+ * labelled its own two categories kept the origin's four sitting beside them in
96
+ * the same screen.
97
+ *
98
+ * 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.
100
+ */
101
+ export function messagesOf<T extends NotificationWireMessages>(config: { messages: T }): T {
102
+ return config.messages;
156
103
  }
package/src/phone.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * `4155552671` into `+554155552671` — a plausible Brazilian mobile — and sent a
14
14
  * stranger the customer's order details. There is no country this package could
15
15
  * assume that is not wrong for every other adopter, so it assumes none and the
16
- * omission is a compile error rather than a wrong number. future-pay passes
16
+ * omission is a compile error rather than a wrong number. the origin passes
17
17
  * `'55'` explicitly.
18
18
  *
19
19
  * NOTE: "verified phone" is approximated by "has a normalizable phone on
@@ -37,7 +37,7 @@ export interface NotificationsWebConfig {
37
37
  /** How the surface reaches its data. Default: same-origin fetch. */
38
38
  transport?: NotificationsTransport;
39
39
  /** User-facing copy overrides (pt-BR product copy by default). */
40
- messages?: Partial<NotificationMessages>;
40
+ messages: NotificationMessages;
41
41
  /**
42
42
  * How the surface learns an inbox changed without asking — the host's message
43
43
  * bus. Without it the badge keeps its 60 s poll, which is the standing
@@ -7,7 +7,7 @@ import type { NotificationsApiClient } from './api';
7
7
  *
8
8
  * They have to share it: marking a row read in the panel must move the badge in
9
9
  * the same tick, and an arrival must add a row to the list AND to the count.
10
- * future-pay got that for free from a react-query cache the host had already
10
+ * the origin got that for free from a react-query cache the host had already
11
11
  * mounted; a published package cannot assume one — a query client is a host
12
12
  * decision, and requiring a particular one (or a particular version of one) is
13
13
  * the kind of dependency that keeps a package out of a host that made the other
@@ -33,7 +33,7 @@ export interface WebPushSetupConfig {
33
33
  * asks no permission, creates no subscription, and fails with nothing a user
34
34
  * could act on. The check is a config seam rather than a dependency because
35
35
  * "is this an installable iOS browser" is a question a host's PWA layer
36
- * already answers (future-pay passes
36
+ * already answers (the origin passes
37
37
  * `() => isIosInstallable() && !isStandalone()` from `@12-apps/pwa`).
38
38
  */
39
39
  needsInstallFirst?: () => boolean;
@@ -24,7 +24,7 @@ import type { NotificationRouter } from './router';
24
24
  * wrong.
25
25
  *
26
26
  * The two QUERIES are the host's, through {@link NotificationAudienceDirectory}
27
- * — because an authorization engine is host machinery. In future-pay this
27
+ * — because an authorization engine is host machinery. In the extraction origin this
28
28
  * module could not live in a package at all: it needed `notify()` AND the RBAC
29
29
  * engine, and neither package could see the other. Inverting the dependency
30
30
  * (the host answers, the package asks) is what makes it portable.
@@ -34,7 +34,7 @@ import type { NotificationRouter } from './router';
34
34
  * The host's authorization engine, as this fan-out needs it.
35
35
  *
36
36
  * `listCandidates` must be BOUNDED to people who actually hold a role at the
37
- * tenant. future-pay's implementation requires a role grant, which is what
37
+ * tenant. that host's implementation requires a role grant, which is what
38
38
  * keeps a store's storefront BUYERS — who all carry a default membership — out
39
39
  * of a loop that resolves permissions one user at a time.
40
40
  *
@@ -1,4 +1,4 @@
1
- import type { NotificationMessages } from '../messages';
1
+ import type { NotificationWireMessages } from '../messages';
2
2
  import { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';
3
3
 
4
4
  /**
@@ -96,7 +96,7 @@ export function guarded(
96
96
  // Body / query parsing — the request contract, in the package
97
97
  // ---------------------------------------------------------------------------
98
98
 
99
- function asRecord(body: unknown, messages: NotificationMessages): Record<string, unknown> {
99
+ function asRecord(body: unknown, messages: NotificationWireMessages): Record<string, unknown> {
100
100
  if (typeof body !== 'object' || body === null || Array.isArray(body)) {
101
101
  throw new NotificationsApiError(400, messages.invalidBody);
102
102
  }
@@ -104,7 +104,7 @@ function asRecord(body: unknown, messages: NotificationMessages): Record<string,
104
104
  }
105
105
 
106
106
  /** 1..100 non-empty string ids. */
107
- function parseIds(value: unknown, messages: NotificationMessages): string[] {
107
+ function parseIds(value: unknown, messages: NotificationWireMessages): string[] {
108
108
  if (!Array.isArray(value) || value.length === 0 || value.length > 100) {
109
109
  throw new NotificationsApiError(400, messages.invalidBody);
110
110
  }
@@ -120,7 +120,7 @@ function parseIds(value: unknown, messages: NotificationMessages): string[] {
120
120
  * Query strings arrive as strings; the store clamps, but a non-number is a
121
121
  * client bug and must not silently read as "the default page".
122
122
  */
123
- function parseLimit(raw: string | undefined, messages: NotificationMessages): number | undefined {
123
+ function parseLimit(raw: string | undefined, messages: NotificationWireMessages): number | undefined {
124
124
  if (raw === undefined || raw === '') return undefined;
125
125
  const limit = Number(raw);
126
126
  if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
@@ -131,7 +131,7 @@ function parseLimit(raw: string | undefined, messages: NotificationMessages): nu
131
131
 
132
132
  function parseFilter(
133
133
  raw: string | undefined,
134
- messages: NotificationMessages,
134
+ messages: NotificationWireMessages,
135
135
  ): 'all' | 'unread' | undefined {
136
136
  if (raw === undefined) return undefined;
137
137
  if (raw !== 'all' && raw !== 'unread') {
@@ -143,7 +143,7 @@ function parseFilter(
143
143
  /** `GET <mount>/notifications` — `filter`, `cursor`, `limit`. */
144
144
  export function parseListQuery(
145
145
  query: Record<string, string | undefined>,
146
- messages: NotificationMessages,
146
+ messages: NotificationWireMessages,
147
147
  ): { filter?: 'all' | 'unread'; cursor?: string; limit?: number } {
148
148
  const filter = parseFilter(query.filter, messages);
149
149
  const limit = parseLimit(query.limit, messages);
@@ -157,7 +157,7 @@ export function parseListQuery(
157
157
  /** `POST <mount>/notifications/mark-read` — explicit ids, or `all: true`. */
158
158
  export function parseMarkReadBody(
159
159
  body: unknown,
160
- messages: NotificationMessages,
160
+ messages: NotificationWireMessages,
161
161
  ): { all: true } | { ids: string[] } {
162
162
  const record = asRecord(body, messages);
163
163
  const wantsAll = record.all === true;
@@ -171,7 +171,7 @@ export function parseMarkReadBody(
171
171
  }
172
172
 
173
173
  /** `POST <mount>/notifications/delete` — 1..100 ids. */
174
- export function parseDeleteBody(body: unknown, messages: NotificationMessages): string[] {
174
+ export function parseDeleteBody(body: unknown, messages: NotificationWireMessages): string[] {
175
175
  return parseIds(asRecord(body, messages).ids, messages);
176
176
  }
177
177
 
@@ -181,7 +181,7 @@ export function parseDeleteBody(body: unknown, messages: NotificationMessages):
181
181
  */
182
182
  export function parsePreferencesBody(
183
183
  body: unknown,
184
- messages: NotificationMessages,
184
+ messages: NotificationWireMessages,
185
185
  ): Record<string, Partial<Record<NotificationChannel, boolean>>> {
186
186
  const record = asRecord(body, messages);
187
187
  const parsed: Record<string, Partial<Record<NotificationChannel, boolean>>> = {};
@@ -194,7 +194,7 @@ export function parsePreferencesBody(
194
194
  /** One category's toggles, narrowed onto the closed channel set. */
195
195
  function parseToggles(
196
196
  value: unknown,
197
- messages: NotificationMessages,
197
+ messages: NotificationWireMessages,
198
198
  ): Partial<Record<NotificationChannel, boolean>> {
199
199
  const toggles = asRecord(value, messages);
200
200
  const row: Partial<Record<NotificationChannel, boolean>> = {};
@@ -210,7 +210,7 @@ function parseToggles(
210
210
  const MAX_ENDPOINT_CHARS = 2000;
211
211
  const MAX_KEY_CHARS = 500;
212
212
 
213
- function parseEndpoint(value: unknown, messages: NotificationMessages): string {
213
+ function parseEndpoint(value: unknown, messages: NotificationWireMessages): string {
214
214
  if (typeof value !== 'string' || value.length === 0 || value.length > MAX_ENDPOINT_CHARS) {
215
215
  throw new NotificationsApiError(400, messages.invalidBody);
216
216
  }
@@ -231,7 +231,7 @@ function parseEndpoint(value: unknown, messages: NotificationMessages): string {
231
231
  /** `POST <mount>/push-subscriptions` — `PushSubscription.toJSON()`. */
232
232
  export function parsePushSubscriptionBody(
233
233
  body: unknown,
234
- messages: NotificationMessages,
234
+ messages: NotificationWireMessages,
235
235
  ): { endpoint: string; keys: { p256dh: string; auth: string } } {
236
236
  const record = asRecord(body, messages);
237
237
  const keys = asRecord(record.keys, messages);
@@ -248,7 +248,7 @@ export function parsePushSubscriptionBody(
248
248
  }
249
249
 
250
250
  /** `DELETE <mount>/push-subscriptions` — by endpoint. */
251
- export function parsePushEndpointBody(body: unknown, messages: NotificationMessages): string {
251
+ export function parsePushEndpointBody(body: unknown, messages: NotificationWireMessages): string {
252
252
  return parseEndpoint(asRecord(body, messages).endpoint, messages);
253
253
  }
254
254
 
@@ -262,7 +262,7 @@ export function parsePushEndpointBody(body: unknown, messages: NotificationMessa
262
262
  */
263
263
  export function parsePushEndpointQuery(
264
264
  query: Record<string, string | undefined>,
265
- messages: NotificationMessages,
265
+ messages: NotificationWireMessages,
266
266
  ): string | undefined {
267
267
  if (query.endpoint === undefined || query.endpoint === '') return undefined;
268
268
  return parseEndpoint(query.endpoint, messages);
@@ -1,5 +1,5 @@
1
1
  import { createGeneratorRegistry, type NotificationGeneratorRegistry } from '../generators';
2
- import { messagesOf, type NotificationMessages } from '../messages';
2
+ import { messagesOf, type NotificationWireMessages } from '../messages';
3
3
  import type { ChannelRow } from '../preferences-core';
4
4
  import {
5
5
  taxonomyOf,
@@ -105,7 +105,7 @@ export interface NotificationsServerConfig {
105
105
  /** The host's authorization engine, for `notifyByPermission`. */
106
106
  audience?: NotificationAudienceDirectory;
107
107
  /** User-facing copy overrides (pt-BR product copy by default). */
108
- messages?: Partial<NotificationMessages>;
108
+ messages: NotificationWireMessages;
109
109
  /** The host's logger. Defaults to the console. */
110
110
  logger?: NotificationLogger;
111
111
  }
@@ -133,7 +133,7 @@ export interface ApiNotifications {
133
133
  /** The declared transports, for diagnostics and availability probes. */
134
134
  transports: TransportRegistry;
135
135
  /** The copy in force, so a host's own screens can reuse a sentence. */
136
- messages: NotificationMessages;
136
+ messages: NotificationWireMessages;
137
137
  }
138
138
 
139
139
  /** Drop the keys the host left unset, so an absent seam stays absent. */
package/src/server/db.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * own stores actually pass — so a non-Prisma implementation has a finite,
10
10
  * documented surface to satisfy instead of "all of Prisma".
11
11
  *
12
- * What is NOT here: the `users` table. future-pay's router read
12
+ * What is NOT here: the `users` table. the origin's router read
13
13
  * `users.email/phone` directly to answer "can this channel reach them", which
14
14
  * is the one thing in the pipeline that belonged to the host all along — a
15
15
  * package cannot know the shape of a host's identity table, and a host with
@@ -1,4 +1,4 @@
1
- import type { NotificationMessages } from '../messages';
1
+ import type { NotificationWireMessages } from '../messages';
2
2
  import { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';
3
3
 
4
4
  import {
@@ -25,7 +25,7 @@ import type { TransportRegistry } from './transports/registry';
25
25
  * Nine routes, and the paths are the PACKAGE's: the shipped react client
26
26
  * builds these URLs, so a host that renamed one would be a host whose own bell
27
27
  * stopped working. The host names only where the whole block is mounted
28
- * (future-pay: `/api/account`).
28
+ * (the origin mounts it at `/api/account`).
29
29
  *
30
30
  * Route ORDER is preserved by every adapter. Nothing here is shaped `/:id`, so
31
31
  * no sibling can capture a literal — but the order is still the contract,
@@ -40,7 +40,7 @@ interface NotificationRoutesDeps {
40
40
  transports: TransportRegistry;
41
41
  contacts: NotificationContactDirectory;
42
42
  categories: readonly string[];
43
- messages: NotificationMessages;
43
+ messages: NotificationWireMessages;
44
44
  /** Told when a write actually changed something (for a realtime hint). */
45
45
  onInboxChanged?: (userId: string) => void;
46
46
  }
@@ -2,7 +2,7 @@
2
2
  * The DRIVER port — the reason a host adds a vendor with a config entry and no
3
3
  * code (12-15).
4
4
  *
5
- * future-pay's transports each read `process.env` directly:
5
+ * the origin's transports each read `process.env` directly:
6
6
  * `NOTIFICATIONS_EMAIL_PROVIDER=resend` plus `RESEND_API_KEY`, and a second
7
7
  * vendor meant editing the package. That is exactly backwards for a published
8
8
  * package — it cannot know a host's variable names, and it must not be the
@@ -101,7 +101,7 @@ export function resolveDriver<TDeclaration extends DriverDeclarationBase, TDrive
101
101
  *
102
102
  * A relative path is useless in an inbox and a fabricated localhost link is
103
103
  * worse than none, so with no `appUrl` configured the link is simply dropped —
104
- * future-pay's rule, kept.
104
+ * The origin's rule, kept.
105
105
  */
106
106
  export function absoluteLink(link: string | undefined, appUrl: string | undefined): string | null {
107
107
  if (!link || !appUrl) return null;
@@ -19,7 +19,7 @@ import { whatsAppTransport } from './whatsapp';
19
19
  * transports are untouched (open/closed).
20
20
  *
21
21
  * A mount declares its channels and gets a registry; nothing is process-wide.
22
- * future-pay registered its four transports as an IMPORT SIDE EFFECT of the
22
+ * The origin registered its four transports as an IMPORT SIDE EFFECT of the
23
23
  * package's root entry, which made "which channels are on" a property of the
24
24
  * module graph rather than of any configuration — importing the inbox helpers
25
25
  * in a unit test silently armed four transports.