@12-apps/notifications 4.3.1 → 4.5.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 (42) hide show
  1. package/dist/{chunk-4PSUZ7X5.js → chunk-ORXJH3VM.js} +2 -2
  2. package/dist/chunk-ORXJH3VM.js.map +1 -0
  3. package/dist/chunk-WU6QJLSZ.js +94 -0
  4. package/dist/chunk-WU6QJLSZ.js.map +1 -0
  5. package/dist/chunk-YE24MDS6.js +1022 -0
  6. package/dist/chunk-YE24MDS6.js.map +1 -0
  7. package/dist/create-api-notifications-CgBdjfyF.d.ts +909 -0
  8. package/dist/create-web-notifications-Du3hTs7P.d.ts +354 -0
  9. package/dist/{generators-FATT537X.d.ts → generators-B9xt3sRh.d.ts} +1 -1
  10. package/dist/hono/index.d.ts +5 -5
  11. package/dist/hono/index.js +1 -1
  12. package/dist/index.d.ts +3 -3
  13. package/dist/jobs-DhDjrAX5.d.ts +74 -0
  14. package/dist/manifest/index.d.ts +20 -6
  15. package/dist/manifest/index.js +2 -1
  16. package/dist/manifest/index.js.map +1 -1
  17. package/dist/manifest/server.d.ts +22 -30
  18. package/dist/manifest/server.js +12 -3
  19. package/dist/manifest/server.js.map +1 -1
  20. package/dist/manifest/web.d.ts +51 -0
  21. package/dist/manifest/web.js +15 -0
  22. package/dist/manifest/web.js.map +1 -0
  23. package/dist/react/index.d.ts +6 -353
  24. package/dist/react/index.js +20 -1008
  25. package/dist/react/index.js.map +1 -1
  26. package/dist/server/index.d.ts +79 -897
  27. package/dist/server/index.js +14 -3
  28. package/dist/{types-yq_o4N01.d.ts → types-CXLAG3UU.d.ts} +1 -1
  29. package/dist/web-push/index.d.ts +2 -2
  30. package/dist/{web-push-KLY6UMRT.d.ts → web-push-Cs14Wp9u.d.ts} +2 -2
  31. package/dist/{wire-SDUtscGu.d.ts → wire-5IRin4zH.d.ts} +1 -1
  32. package/package.json +10 -3
  33. package/src/manifest/index.ts +20 -6
  34. package/src/manifest/server.ts +8 -0
  35. package/src/manifest/web.ts +46 -0
  36. package/src/server/index.ts +20 -0
  37. package/src/server/jobs.ts +118 -0
  38. package/src/server/transports/email.ts +8 -3
  39. package/src/server/wire-notify-port.ts +125 -0
  40. package/dist/chunk-4PSUZ7X5.js.map +0 -1
  41. package/dist/chunk-F5ANWJCY.js +0 -1
  42. package/dist/chunk-F5ANWJCY.js.map +0 -1
@@ -0,0 +1,354 @@
1
+ import { JSX, ComponentType } from 'react';
2
+ import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-5IRin4zH.js';
3
+ import { b as NotificationChannel } from './types-CXLAG3UU.js';
4
+
5
+ /**
6
+ * How the notification screens reach their data (12-15) — the report-builder
7
+ * transport doctrine: this is the ONLY way the surface performs I/O, so a
8
+ * caller supplying one has substituted the entire backend without stubbing a
9
+ * global. The default is same-origin `fetch` riding the browser's cookies.
10
+ */
11
+ /** A write outcome the screens branch on — never a thrown mutation. */
12
+ type NotificationsResult<T> = {
13
+ ok: true;
14
+ data: T;
15
+ } | {
16
+ ok: false;
17
+ error: string;
18
+ };
19
+ /** A failed read, carrying the status the screens branch on (401 = signed out). */
20
+ declare class NotificationsHttpError extends Error {
21
+ readonly status: number;
22
+ constructor(status: number, message: string);
23
+ }
24
+ interface NotificationsTransport {
25
+ /** A read. Returns the payload INSIDE the `{ data }` envelope. */
26
+ get<T>(path: string): Promise<T>;
27
+ /** A write. Returns a {@link NotificationsResult} rather than rejecting. */
28
+ send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>>;
29
+ }
30
+ /**
31
+ * @param fallbackError What a failed write says when the server sent no
32
+ * sentence of its own — REQUIRED, the host's words. `createWebNotifications`
33
+ * already passes its (equally required) `messages.operationFailed`; only a
34
+ * host constructing the transport directly writes it here. The old default
35
+ * was one application's Portuguese, and the only string in this package the
36
+ * required-messages port did not cover.
37
+ */
38
+ declare function httpNotificationsTransport(fallbackError: string): NotificationsTransport;
39
+
40
+ /**
41
+ * The wire client, bound to one mount (12-15).
42
+ *
43
+ * Every path this package's screens can call, in one place — which is what
44
+ * makes the api half's route table and the web half's URLs one contract instead
45
+ * of two lists that drift.
46
+ */
47
+ /** `GET <mount>/notification-preferences` and the PUT's answer. */
48
+ interface PreferencesPayload {
49
+ preferences: Record<string, ChannelRow>;
50
+ availability: Record<NotificationChannel, boolean>;
51
+ /** The host's taxonomy, so the screen renders it without being told twice. */
52
+ categories: string[];
53
+ }
54
+ /** `GET <mount>/push-subscriptions`. */
55
+ interface PushRegistrationPayload {
56
+ /** null = web push is not configured on this deployment. */
57
+ vapidPublicKey: string | null;
58
+ count: number;
59
+ /**
60
+ * Whether the endpoint asked about is still registered to the caller. Present
61
+ * only when one was passed — see {@link NotificationsApiClient.getPushRegistration}.
62
+ */
63
+ registered?: boolean;
64
+ }
65
+ interface NotificationsApiClient {
66
+ listNotifications(input: {
67
+ cursor?: string | null;
68
+ limit?: number;
69
+ filter?: 'all' | 'unread';
70
+ }): Promise<ListNotificationsResult>;
71
+ unreadCount(): Promise<number>;
72
+ markRead(ids: readonly string[]): Promise<NotificationsResult<{
73
+ updated: number;
74
+ }>>;
75
+ markAllRead(): Promise<NotificationsResult<{
76
+ updated: number;
77
+ }>>;
78
+ remove(ids: readonly string[]): Promise<NotificationsResult<{
79
+ deleted: number;
80
+ }>>;
81
+ getPreferences(): Promise<PreferencesPayload>;
82
+ savePreference(category: string, channel: NotificationChannel, enabled: boolean): Promise<NotificationsResult<PreferencesPayload>>;
83
+ /**
84
+ * The deployment's VAPID key and the caller's device count — and, when an
85
+ * `endpoint` is passed, whether the SERVER still has that exact subscription
86
+ * under the caller's id. The browser holding a subscription object is not
87
+ * evidence of that: a re-own or a 404/410 prune drops the row and leaves the
88
+ * browser's object in place.
89
+ */
90
+ getPushRegistration(input?: {
91
+ endpoint?: string;
92
+ }): Promise<PushRegistrationPayload>;
93
+ savePushSubscription(input: {
94
+ endpoint: string;
95
+ keys: {
96
+ p256dh: string;
97
+ auth: string;
98
+ };
99
+ }): Promise<NotificationsResult<{
100
+ count: number;
101
+ }>>;
102
+ removePushSubscription(endpoint: string): Promise<NotificationsResult<{
103
+ count: number;
104
+ }>>;
105
+ }
106
+ declare function createNotificationsApiClient(apiBase: string, transport: NotificationsTransport): NotificationsApiClient;
107
+
108
+ /**
109
+ * The inbox's client state, as ONE store shared by the bell and the panel.
110
+ *
111
+ * They have to share it: marking a row read in the panel must move the badge in
112
+ * the same tick, and an arrival must add a row to the list AND to the count.
113
+ * the origin got that for free from a react-query cache the host had already
114
+ * mounted; a published package cannot assume one — a query client is a host
115
+ * decision, and requiring a particular one (or a particular version of one) is
116
+ * the kind of dependency that keeps a package out of a host that made the other
117
+ * choice. So the sharing is explicit and dependency-free: one subscribable
118
+ * store, read through `useSyncExternalStore`.
119
+ *
120
+ * Optimistic on every write, with invalidate-on-error: the badge and the list
121
+ * update instantly, and a failed write refetches the server truth rather than
122
+ * leaving the screen asserting something the database does not say.
123
+ */
124
+ declare const PAGE_SIZE = 20;
125
+ /** The badge's poll while nothing is pushing to us. */
126
+ declare const BADGE_POLL_MS = 60000;
127
+ /**
128
+ * The badge's interval while a realtime connection is live.
129
+ *
130
+ * Five minutes, not "never": this is the reconcile that catches an event the bus
131
+ * dropped, and it costs one COUNT per open tab per five minutes. Deliberately
132
+ * far slower than an operational screen's — a bell badge is ambient, and the
133
+ * arrival that matters is pushed within milliseconds anyway. The poll does NOT
134
+ * stop, which is the standing contract: a dropped event must cost latency and
135
+ * never correctness.
136
+ */
137
+ declare const BADGE_RECONCILE_MS = 300000;
138
+ type InboxListStatus = 'idle' | 'pending' | 'ready' | 'error';
139
+ interface InboxState {
140
+ unread: number;
141
+ items: InboxNotification[];
142
+ status: InboxListStatus;
143
+ /** A cursor means there is another page. */
144
+ nextCursor: string | null;
145
+ loadingMore: boolean;
146
+ }
147
+ interface InboxStore {
148
+ getState(): InboxState;
149
+ subscribe(listener: () => void): () => void;
150
+ /** Load the first page (idempotent while one is in flight). */
151
+ open(): void;
152
+ /** Refetch the badge count. */
153
+ refreshBadge(): void;
154
+ /** Refetch both — what a realtime hint or a failed write triggers. */
155
+ invalidate(): void;
156
+ loadMore(): void;
157
+ markRead(ids: readonly string[]): void;
158
+ markAllRead(): void;
159
+ remove(id: string): void;
160
+ }
161
+ declare function createInboxStore(api: NotificationsApiClient): InboxStore;
162
+
163
+ /**
164
+ * The two hooks the bell and the panel use, and the realtime seam between them.
165
+ *
166
+ * A host that has a message bus passes `subscribe`; one that has not passes
167
+ * nothing and keeps the 60 s poll. The bell ships in this package and mounts in
168
+ * whatever embeds it, so it must not require the host to have adopted anything.
169
+ */
170
+ /**
171
+ * How the surface learns an inbox changed without asking.
172
+ *
173
+ * Called once per mounted bell with a callback that means only "ask again" — no
174
+ * payload, so the number on screen is always one the server just gave us.
175
+ * Returns its own teardown. A host wires this to whatever it already has.
176
+ */
177
+ type NotificationsSubscribe = (onHint: () => void) => () => void;
178
+ /**
179
+ * The same wiring, as a HOOK — for a host whose realtime connection lives in
180
+ * React context rather than in a module.
181
+ *
182
+ * `subscribe` above is supplied at FACTORY time, which is module scope, and a
183
+ * context-bound connection cannot be reached from there: the provider holding
184
+ * it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a
185
+ * `useUserTopics` hook, which is the common one) had no way to pass anything at
186
+ * all, and the badge simply never heard an event.
187
+ *
188
+ * So this is the second door, and it is the one `@12-apps/app-shell` already
189
+ * uses for the same problem — its consent dialog takes a `useSignal` hook for
190
+ * exactly this reason. Two packages solving one problem two ways is how an
191
+ * adopter ends up believing the feature is unavailable to it.
192
+ *
193
+ * Called during render, so it may use context and hooks freely. Pass one or
194
+ * the other; passing both runs both, which is a host's business.
195
+ */
196
+ type NotificationsSignalHook = (onHint: () => void) => void;
197
+ declare function useInboxState(store: InboxStore): InboxState;
198
+ /**
199
+ * The bell badge number: pushed while a subscription is live, polled otherwise.
200
+ *
201
+ * `enabled` gates the poll AND the subscription. A signed-out header still
202
+ * mounts the bell, and there is nothing for it to hear.
203
+ */
204
+ declare function useUnreadCount(store: InboxStore, options?: {
205
+ enabled?: boolean;
206
+ subscribe?: NotificationsSubscribe;
207
+ useSignal?: NotificationsSignalHook;
208
+ }): number;
209
+ /** The panel's list — only fetches while the panel is open. */
210
+ declare function useInboxList(store: InboxStore, open: boolean): InboxState;
211
+
212
+ /**
213
+ * Bare bell trigger with the live unread badge — for hosts that do not already
214
+ * have a styled icon-button slot. A host with its own trigger chrome uses
215
+ * `useUnreadCount` + `Panel` directly.
216
+ */
217
+
218
+ interface BellButtonProps {
219
+ onClick: () => void;
220
+ /** Signed-out hosts still mount the bell; `false` silences it. */
221
+ enabled?: boolean;
222
+ }
223
+
224
+ /**
225
+ * The notification-centre slide-over: newest-first list with unread styling,
226
+ * per-item open (marks read + deep-links), soft delete, mark-all, empty /
227
+ * loading / error states and a "load more" cursor pager.
228
+ *
229
+ * Rendering is app-agnostic — the host passes `onNavigate` (its router's
230
+ * navigate) for deep links. Without one a link is simply not followed, which is
231
+ * what lets the panel mount in a host that has no router at all.
232
+ */
233
+
234
+ interface NotificationsPanelProps {
235
+ open: boolean;
236
+ onClose: () => void;
237
+ /** Navigate to a notification's in-app link (the host's router). */
238
+ onNavigate?: (link: string) => void;
239
+ }
240
+
241
+ /**
242
+ * The per-BROWSER Web Push enable step, which sits above the preference matrix
243
+ * because a preference alone cannot reach a device that never subscribed.
244
+ */
245
+
246
+ /** The panel's copy for a host whose platform blocks browser-level push. */
247
+ interface WebPushPlatformHint {
248
+ title: string;
249
+ body: string;
250
+ }
251
+ interface WebPushSetupConfig {
252
+ /**
253
+ * The host's service-worker path. Path-routed SPAs each control their own
254
+ * scope, and the file itself is the host's.
255
+ */
256
+ swPath?: string;
257
+ /**
258
+ * Whether THIS platform must be installed to the home screen before a
259
+ * subscription can exist at all.
260
+ *
261
+ * iOS is the case: Safari has no browser-level Web Push, so "Ativar" there
262
+ * asks no permission, creates no subscription, and fails with nothing a user
263
+ * could act on. The check is a config seam rather than a dependency because
264
+ * "is this an installable iOS browser" is a question a host's PWA layer
265
+ * already answers (the origin passes
266
+ * `() => isIosInstallable() && !isStandalone()` from `@12-apps/pwa`).
267
+ */
268
+ needsInstallFirst?: () => boolean;
269
+ /** What to say instead of the button when the check above is true. */
270
+ installHint?: WebPushPlatformHint;
271
+ }
272
+
273
+ /**
274
+ * The notification-preferences screen: the category × channel matrix over
275
+ * `GET/PUT <mount>/notification-preferences`.
276
+ *
277
+ * Toggles auto-save (optimistic, per change); channels that cannot reach the
278
+ * user right now (no phone on file / channel not declared) render disabled with
279
+ * a hint. Web Push additionally carries the per-BROWSER enable step, since a
280
+ * preference alone cannot reach a device that never subscribed.
281
+ */
282
+
283
+ interface PreferencesScreenProps {
284
+ /** Rendered under the lead paragraph — a "back to account" link, typically. */
285
+ footer?: JSX.Element;
286
+ }
287
+
288
+ /**
289
+ * The one thing this package exposes to a FRONTEND host (12-15).
290
+ *
291
+ * Everything the notification centre IS — the bell with its live badge, the
292
+ * slide-over inbox with its optimistic mark-read / delete / mark-all and its
293
+ * cursor pager, the preferences matrix with its availability hints and the
294
+ * per-browser push enable step, and every wire call between them — lives inside
295
+ * this package. The host names where the API is mounted, and that is the whole
296
+ * wiring.
297
+ *
298
+ * `page` is the standalone surface (the preferences screen), which is the one
299
+ * thing a host routes to. The bell and the panel are a PAIR a host drops into
300
+ * its own chrome, and they share one store, so a read in the panel moves the
301
+ * badge in the same tick.
302
+ */
303
+ interface NotificationsWebConfig {
304
+ /** The account mount the routes live under, e.g. `/api/account`. */
305
+ apiBase: string;
306
+ /** How the surface reaches its data. Default: same-origin fetch. */
307
+ transport?: NotificationsTransport;
308
+ /** User-facing copy overrides (pt-BR product copy by default). */
309
+ messages: NotificationMessages;
310
+ /**
311
+ * How the surface learns an inbox changed without asking — the host's message
312
+ * bus. Without it the badge keeps its 60 s poll, which is the standing
313
+ * contract rather than a fallback: a dropped event must cost latency, never
314
+ * correctness.
315
+ */
316
+ subscribe?: NotificationsSubscribe;
317
+ /**
318
+ * The same wiring as a HOOK, for a host whose realtime connection lives in
319
+ * React context — see `NotificationsSignalHook`. `subscribe` is read at
320
+ * factory time, which such a host cannot reach.
321
+ */
322
+ useSignal?: NotificationsSignalHook;
323
+ /** The browser push enable step's host seams (SW path, platform hint). */
324
+ webPush?: WebPushSetupConfig;
325
+ }
326
+ interface WebNotifications {
327
+ /** The routed surface: the preferences screen. */
328
+ page: ComponentType<PreferencesScreenProps>;
329
+ /** The bell, already bound to the shared store. */
330
+ BellButton: ComponentType<BellButtonProps>;
331
+ /** The inbox slide-over, sharing that store. */
332
+ Panel: ComponentType<NotificationsPanelProps>;
333
+ /**
334
+ * Bell + panel as ONE element, for a host that just wants the feature in its
335
+ * header and does not want to own the open/closed state.
336
+ */
337
+ BellWithPanel: ComponentType<{
338
+ enabled?: boolean;
339
+ onNavigate?: (link: string) => void;
340
+ }>;
341
+ /** The badge number, for a host with its own trigger chrome. */
342
+ useUnreadCount: (options?: {
343
+ enabled?: boolean;
344
+ }) => number;
345
+ /** The shared client state, for host glue. */
346
+ store: InboxStore;
347
+ /** The bound wire client. */
348
+ api: NotificationsApiClient;
349
+ /** The copy in force, so a host's own chrome can reuse a sentence. */
350
+ messages: NotificationMessages;
351
+ }
352
+ declare function createWebNotifications(config: NotificationsWebConfig): WebNotifications;
353
+
354
+ export { BADGE_POLL_MS as B, type InboxListStatus as I, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BellButtonProps as b, createWebNotifications as c, type InboxState as d, type InboxStore as e, NotificationsHttpError as f, type NotificationsPanelProps as g, type NotificationsResult as h, type NotificationsSignalHook as i, type NotificationsSubscribe as j, type NotificationsTransport as k, type NotificationsWebConfig as l, type PreferencesPayload as m, type PreferencesScreenProps as n, type PushRegistrationPayload as o, type WebPushPlatformHint as p, type WebPushSetupConfig as q, createInboxStore as r, createNotificationsApiClient as s, httpNotificationsTransport as t, useInboxList as u, useInboxState as v, useUnreadCount as w };
@@ -1,4 +1,4 @@
1
- import { N as NotificationGenerator } from './types-yq_o4N01.js';
1
+ import { e as NotificationGenerator } from './types-CXLAG3UU.js';
2
2
 
3
3
  /**
4
4
  * Generator registry: one {@link NotificationGenerator} per event `type`.
@@ -1,9 +1,9 @@
1
1
  import { Hono, Context } from 'hono';
2
- import { ApiNotifications, NotificationsServerConfig, NotificationsActor } from '../server/index.js';
3
- import '../generators-FATT537X.js';
4
- import '../types-yq_o4N01.js';
5
- import '../wire-SDUtscGu.js';
6
- import '../web-push-KLY6UMRT.js';
2
+ import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-CgBdjfyF.js';
3
+ import '../generators-B9xt3sRh.js';
4
+ import '../types-CXLAG3UU.js';
5
+ import '../wire-5IRin4zH.js';
6
+ import '../web-push-Cs14Wp9u.js';
7
7
 
8
8
  /**
9
9
  * `@12-apps/notifications/hono` — the account notification endpoints as a
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiNotifications
3
- } from "../chunk-4PSUZ7X5.js";
3
+ } from "../chunk-ORXJH3VM.js";
4
4
  import "../chunk-AHNRSA6U.js";
5
5
  import {
6
6
  messagesOf
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, N as NotificationGenerator, f as NotificationLogger, g as NotificationRecipient, h as NotificationTaxonomy, i as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-yq_o4N01.js';
2
- export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-FATT537X.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-SDUtscGu.js';
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
+ 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';
4
4
 
5
5
  /** Thrown by `notify` when no generator is registered for the event type. */
6
6
  declare class UnknownNotificationTypeError extends Error {
@@ -0,0 +1,74 @@
1
+ import { WireJobBlueprint } from '@12-apps/wiring';
2
+ import { A as ApiNotifications } from './create-api-notifications-CgBdjfyF.js';
3
+
4
+ /**
5
+ * The two background jobs getting a message out actually needs.
6
+ *
7
+ * ## Why they are declared here and not left to the host
8
+ *
9
+ * `notify()` commits the inbox record and one QUEUED delivery per channel in a
10
+ * transaction, and that row IS the durable record of the send. Everything after
11
+ * it is a decision about WHEN the provider call happens — and every one of
12
+ * those decisions is this package's knowledge, not a host's:
13
+ *
14
+ * - the fast path retries only INFRASTRUCTURE faults, because a provider
15
+ * rejecting one channel does not throw here (the row goes FAILED and the
16
+ * sweep owns the retry), so three spaced attempts is about surviving a
17
+ * restart rather than chasing a provider;
18
+ * - the sweep's five-minute cadence is the resolution at which "my customer
19
+ * never got the e-mail" stops being an incident;
20
+ * - the sweep runs single-flight, because overlapping passes are duplicate
21
+ * billed provider calls;
22
+ * - and the sweep is what makes the system UNSTUCK-ABLE: it re-dispatches
23
+ * FAILED rows and stale claims whose dispatching process died mid-flight.
24
+ * Before it existed a provider blip left a delivery FAILED forever.
25
+ *
26
+ * A host asked to restate all of that is a host that can get it wrong — and,
27
+ * far more likely, a host that never schedules the sweep at all and quietly
28
+ * has no retry. That is the `paymentsJobBlueprints()` incident exactly: a
29
+ * mechanism a host must remember to schedule is a mechanism most hosts do not
30
+ * have. The origin host DID write both jobs, correctly, by hand — cadence,
31
+ * lease ttl, attempts and concurrency restated in its own `lib/jobs` — which
32
+ * is the drift this declaration ends rather than a gap it fills.
33
+ *
34
+ * What stays the host's: whether to run them at all, on which queue runtime,
35
+ * and the lease implementation. A host with no worker declines the `jobs`
36
+ * capability in writing and the wiring report says so.
37
+ */
38
+
39
+ /**
40
+ * What the host closes over at bind time — the mounted api's two send paths,
41
+ * and nothing else. Deliberately a `Pick` of the real thing rather than a
42
+ * parallel interface: a reshape of either method stops compiling here.
43
+ */
44
+ type NotificationsJobDeps = Pick<ApiNotifications, 'dispatchDeliveries' | 'drainPending'>;
45
+ /**
46
+ * The single-flight queue name — the same string `@12-apps/jobs` exports as
47
+ * `SWEEP_QUEUE`, stated as a literal because this package does not depend on
48
+ * the job library (the payments-backend precedent, for the same reason).
49
+ */
50
+ declare const NOTIFICATIONS_SWEEP_QUEUE = "sweeps";
51
+ /** Five minutes — see the header on why this number is the package's. */
52
+ declare const NOTIFICATIONS_DRAIN_CRON = "*/5 * * * *";
53
+ /**
54
+ * Comfortably longer than a drain pass, and under the cadence's own patience:
55
+ * every delivery a pass re-dispatches is a provider call.
56
+ */
57
+ declare const NOTIFICATIONS_DRAIN_LEASE_MS: number;
58
+ /**
59
+ * The jobs contribution. `namespace` is prepended once at bind time, so these
60
+ * arrive at a runner as `notifications.dispatch` and `notifications.drain` —
61
+ * the wire names the origin host already uses, so adopting is a deletion
62
+ * rather than a rename.
63
+ */
64
+ declare const NOTIFICATIONS_JOBS: {
65
+ readonly namespace: "notifications";
66
+ readonly blueprints: {
67
+ readonly dispatch: WireJobBlueprint<{
68
+ notificationId: string;
69
+ }, NotificationsJobDeps>;
70
+ readonly drain: WireJobBlueprint<void, NotificationsJobDeps>;
71
+ };
72
+ };
73
+
74
+ export { type NotificationsJobDeps as N, NOTIFICATIONS_DRAIN_CRON as a, NOTIFICATIONS_DRAIN_LEASE_MS as b, NOTIFICATIONS_JOBS as c, NOTIFICATIONS_SWEEP_QUEUE as d };
@@ -14,11 +14,24 @@
14
14
  * (`config.transports`, `config.drivers`), so what would be declared here
15
15
  * is a seam the host already fills — and declaring it would oblige an
16
16
  * adopter to bind a mailer this package never owns.
17
- * - **No `web` inventory**, though `./react` ships the bell and the
18
- * preference screens. Listing it would oblige every SERVER host adopting
19
- * this manifest to answer for a React surface it never mounts —
20
- * `assemble()` refuses a declared-but-unanswered capability, so the
21
- * inventory must not overstate.
17
+ *
18
+ * ON THE `jobs` DECLARATION. The dispatch fast path and the retry sweep are
19
+ * this package's own cadence decisions — attempts, backoff, the five-minute
20
+ * tick, the single-flight lease — and they were host code in every adopting
21
+ * host, restated by hand from this package's docstrings. That is the
22
+ * `paymentsJobBlueprints()` incident's shape exactly: a mechanism a host must
23
+ * remember to schedule is a mechanism most hosts silently do not have, and the
24
+ * one host that DID write them wrote them correctly only because someone read
25
+ * the source. `./server`'s `NOTIFICATIONS_JOBS` declares both; a host with no
26
+ * worker declines the capability in writing and the report says so.
27
+ *
28
+ * ON THE `web` INVENTORY, which this manifest used to narrow away. The reason
29
+ * given — that listing it would oblige every SERVER host to answer for a React
30
+ * surface it never mounts — is not how the consumer behaves: a capability
31
+ * declared for the OTHER runtime is reported `out-of-scope`, and only an
32
+ * applicable, unanswered one is `unbound`. So the narrowing protected nothing
33
+ * and hid Bell, Panel and Preferences from every adopting host, which is why
34
+ * the origin host hand-duplicated two of the three.
22
35
  *
23
36
  * ON THE `db` DECLARATION. The origin host already composes
24
37
  * `prisma/notifications.prisma` into its schema — but by STRUCTURAL
@@ -47,7 +60,8 @@ declare const notificationsManifest: {
47
60
  readonly observability: {
48
61
  readonly namespace: "notifications";
49
62
  };
50
- readonly server: readonly ["http"];
63
+ readonly server: readonly ["http", "jobs"];
64
+ readonly web: readonly ["surface"];
51
65
  };
52
66
 
53
67
  export { notificationsManifest };
@@ -10,7 +10,8 @@ var notificationsManifest = {
10
10
  * exhausts its attempts files under `notifications`, not nowhere.
11
11
  */
12
12
  observability: { namespace: "notifications" },
13
- server: ["http"]
13
+ server: ["http", "jobs"],
14
+ web: ["surface"]
14
15
  };
15
16
  export {
16
17
  notificationsManifest
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/manifest/index.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest` — the SHARED wiring manifest.\n *\n * Identity, the Prisma contribution (the four owned models) and the runtime\n * inventory: `http` on the server. Three narrowings are deliberate:\n *\n * - **No `notifications` blueprints.** This package is the notification\n * MECHANISM — the inbox, the preference matrix, the transports, the retry\n * sweep — not an author of alerts. The blueprints belong to whichever\n * package raises the alert (`@12-apps/product-research`'s budget warning\n * ships as a factory over host copy for exactly this reason), and this one\n * takes them through the `generators` seam at mount.\n * - **No `email` capability.** The transports are host-supplied config\n * (`config.transports`, `config.drivers`), so what would be declared here\n * is a seam the host already fills — and declaring it would oblige an\n * adopter to bind a mailer this package never owns.\n * - **No `web` inventory**, though `./react` ships the bell and the\n * preference screens. Listing it would oblige every SERVER host adopting\n * this manifest to answer for a React surface it never mounts —\n * `assemble()` refuses a declared-but-unanswered capability, so the\n * inventory must not overstate.\n *\n * ON THE `db` DECLARATION. The origin host already composes\n * `prisma/notifications.prisma` into its schema — but by STRUCTURAL\n * DISCOVERY, the assembler's fallback for a package that declares nothing.\n * That fallback is why the gap was invisible: four tables reached a host's\n * database with no declaration behind them, and the contract's whole claim\n * is that a package's models arrive because it said so. Declaring changes no\n * host behaviour (the assembler reads the declaration where it used to scan)\n * and closes the one case where composition was happening by accident.\n *\n * `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):\n * the manifest is a plain `satisfies`-checked value, and the producer\n * factories' runtime assertions run in this package's own test suite.\n */\n\nimport type { PackageManifest } from '@12-apps/wiring';\n\nexport const notificationsManifest = {\n name: '@12-apps/notifications',\n contract: 1,\n db: { partial: 'prisma/notifications.prisma', migrations: 'prisma/migrations' },\n /**\n * Mandatory for runtime manifests since wiring 1.3.0: a delivery that\n * exhausts its attempts files under `notifications`, not nowhere.\n */\n observability: { namespace: 'notifications' },\n server: ['http'],\n} as const satisfies PackageManifest;\n"],"mappings":";;;AAsCO,IAAM,wBAAwB;AAAA,EACnC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,IAAI,EAAE,SAAS,+BAA+B,YAAY,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,EAK9E,eAAe,EAAE,WAAW,gBAAgB;AAAA,EAC5C,QAAQ,CAAC,MAAM;AACjB;","names":[]}
1
+ {"version":3,"sources":["../../src/manifest/index.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest` — the SHARED wiring manifest.\n *\n * Identity, the Prisma contribution (the four owned models) and the runtime\n * inventory: `http` on the server. Three narrowings are deliberate:\n *\n * - **No `notifications` blueprints.** This package is the notification\n * MECHANISM — the inbox, the preference matrix, the transports, the retry\n * sweep — not an author of alerts. The blueprints belong to whichever\n * package raises the alert (`@12-apps/product-research`'s budget warning\n * ships as a factory over host copy for exactly this reason), and this one\n * takes them through the `generators` seam at mount.\n * - **No `email` capability.** The transports are host-supplied config\n * (`config.transports`, `config.drivers`), so what would be declared here\n * is a seam the host already fills — and declaring it would oblige an\n * adopter to bind a mailer this package never owns.\n *\n * ON THE `jobs` DECLARATION. The dispatch fast path and the retry sweep are\n * this package's own cadence decisions — attempts, backoff, the five-minute\n * tick, the single-flight lease — and they were host code in every adopting\n * host, restated by hand from this package's docstrings. That is the\n * `paymentsJobBlueprints()` incident's shape exactly: a mechanism a host must\n * remember to schedule is a mechanism most hosts silently do not have, and the\n * one host that DID write them wrote them correctly only because someone read\n * the source. `./server`'s `NOTIFICATIONS_JOBS` declares both; a host with no\n * worker declines the capability in writing and the report says so.\n *\n * ON THE `web` INVENTORY, which this manifest used to narrow away. The reason\n * given — that listing it would oblige every SERVER host to answer for a React\n * surface it never mounts — is not how the consumer behaves: a capability\n * declared for the OTHER runtime is reported `out-of-scope`, and only an\n * applicable, unanswered one is `unbound`. So the narrowing protected nothing\n * and hid Bell, Panel and Preferences from every adopting host, which is why\n * the origin host hand-duplicated two of the three.\n *\n * ON THE `db` DECLARATION. The origin host already composes\n * `prisma/notifications.prisma` into its schema — but by STRUCTURAL\n * DISCOVERY, the assembler's fallback for a package that declares nothing.\n * That fallback is why the gap was invisible: four tables reached a host's\n * database with no declaration behind them, and the contract's whole claim\n * is that a package's models arrive because it said so. Declaring changes no\n * host behaviour (the assembler reads the declaration where it used to scan)\n * and closes the one case where composition was happening by accident.\n *\n * `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):\n * the manifest is a plain `satisfies`-checked value, and the producer\n * factories' runtime assertions run in this package's own test suite.\n */\n\nimport type { PackageManifest } from '@12-apps/wiring';\n\nexport const notificationsManifest = {\n name: '@12-apps/notifications',\n contract: 1,\n db: { partial: 'prisma/notifications.prisma', migrations: 'prisma/migrations' },\n /**\n * Mandatory for runtime manifests since wiring 1.3.0: a delivery that\n * exhausts its attempts files under `notifications`, not nowhere.\n */\n observability: { namespace: 'notifications' },\n server: ['http', 'jobs'],\n web: ['surface'],\n} as const satisfies PackageManifest;\n"],"mappings":";;;AAmDO,IAAM,wBAAwB;AAAA,EACnC,MAAM;AAAA,EACN,UAAU;AAAA,EACV,IAAI,EAAE,SAAS,+BAA+B,YAAY,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,EAK9E,eAAe,EAAE,WAAW,gBAAgB;AAAA,EAC5C,QAAQ,CAAC,QAAQ,MAAM;AAAA,EACvB,KAAK,CAAC,SAAS;AACjB;","names":[]}
@@ -1,34 +1,11 @@
1
+ import * as _12_apps_wiring from '@12-apps/wiring';
1
2
  import { WireRequest } from '@12-apps/wiring';
2
- import { NotificationsServerConfig, ApiNotifications, NotificationsRoute } from '../server/index.js';
3
- import '../generators-FATT537X.js';
4
- import '../types-yq_o4N01.js';
5
- import '../wire-SDUtscGu.js';
6
- import '../web-push-KLY6UMRT.js';
7
-
8
- /**
9
- * `@12-apps/notifications/manifest/server` — the server capabilities.
10
- *
11
- * `http.create` wraps `createApiNotifications` in a WIRE VIEW, and the reason
12
- * is one field. `NotificationsRequest` carries `headers` — the contract's
13
- * `WireRequest` does not, because headers are the adapter's business
14
- * everywhere else — and exactly one descriptor reads it: push-subscribe takes
15
- * `user-agent` as the DEVICE HINT it labels a subscription with. Without the
16
- * view the field would simply be absent at runtime while still type-checking,
17
- * and every saved device would come back unnamed: a silent quality loss, the
18
- * failure mode the wiring contract exists to convert into a loud one.
19
- *
20
- * So the view derives `headers` from the raw request the contract already
21
- * carries for the handlers `params`/`query`/`body` cannot serve. A host whose
22
- * adapter leaves `request` unset still gets a working surface — every route
23
- * answers, the subscription saves — with an unnamed device, which is the
24
- * honest degradation for a hint. `@12-apps/notifications/hono` populates it.
25
- *
26
- * Everything else rides beside the mapped routes on the aggregate unchanged:
27
- * `notify`, `notifyByPermission`, `dispatchDeliveries`, `drainPending`, the
28
- * three stores, `registerGenerator` and the transports registry. A host still
29
- * calls those directly — being mounted does not make the emit front door stop
30
- * being a library.
31
- */
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';
5
+ import '../generators-B9xt3sRh.js';
6
+ import '../types-CXLAG3UU.js';
7
+ import '../wire-5IRin4zH.js';
8
+ import '../web-push-Cs14Wp9u.js';
32
9
 
33
10
  /** One `NotificationsRoute` as the wiring contract reads it. */
34
11
  declare function asWireRoute(route: NotificationsRoute): {
@@ -48,6 +25,21 @@ declare const notificationsServerManifest: {
48
25
  readonly http: {
49
26
  readonly create: typeof createWireApiNotifications;
50
27
  };
28
+ /**
29
+ * The dispatch fast path and the retry sweep, with their cadence. The host
30
+ * binds `{ dispatchDeliveries, drainPending }` off its own mount — the two
31
+ * methods the aggregate already hands it — and deletes the hand-rolled
32
+ * copies. See `../server/jobs` for why the numbers are the package's.
33
+ */
34
+ readonly jobs: {
35
+ readonly namespace: "notifications";
36
+ readonly blueprints: {
37
+ readonly dispatch: _12_apps_wiring.WireJobBlueprint<{
38
+ notificationId: string;
39
+ }, NotificationsJobDeps>;
40
+ readonly drain: _12_apps_wiring.WireJobBlueprint<void, NotificationsJobDeps>;
41
+ };
42
+ };
51
43
  };
52
44
 
53
45
  export { createWireApiNotifications, notificationsServerManifest };
@@ -1,7 +1,9 @@
1
- import "../chunk-F5ANWJCY.js";
1
+ import {
2
+ NOTIFICATIONS_JOBS
3
+ } from "../chunk-WU6QJLSZ.js";
2
4
  import {
3
5
  createApiNotifications
4
- } from "../chunk-4PSUZ7X5.js";
6
+ } from "../chunk-ORXJH3VM.js";
5
7
  import "../chunk-AHNRSA6U.js";
6
8
  import "../chunk-4TTYQVPK.js";
7
9
  import {
@@ -39,7 +41,14 @@ function createWireApiNotifications(config) {
39
41
  __name(createWireApiNotifications, "createWireApiNotifications");
40
42
  var notificationsServerManifest = {
41
43
  name: "@12-apps/notifications",
42
- http: { create: createWireApiNotifications }
44
+ http: { create: createWireApiNotifications },
45
+ /**
46
+ * The dispatch fast path and the retry sweep, with their cadence. The host
47
+ * binds `{ dispatchDeliveries, drainPending }` off its own mount — the two
48
+ * methods the aggregate already hands it — and deletes the hand-rolled
49
+ * copies. See `../server/jobs` for why the numbers are the package's.
50
+ */
51
+ jobs: NOTIFICATIONS_JOBS
43
52
  };
44
53
  export {
45
54
  createWireApiNotifications,