@substrat-run/engine-notifications 0.0.0-stage → 0.3.1

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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * engine-notifications' event contract — what a VERTICAL imports so that reading this
3
+ * engine's outcomes by event is checked rather than guessed.
4
+ *
5
+ * Types plus one stamp, the same shape as every other engine's `events.ts`: the runtime
6
+ * contract is the fat payload and the consumer's own Zod parse; `emitNotificationsEvent`
7
+ * forwards to `ctx.emit`, adding only the `schemaVersion` from
8
+ * `notificationsEventVersions` (#1597).
9
+ *
10
+ * **No address appears.** Each payload but `topic-recorded`'s names a principal (pseudonymous, and the event's
11
+ * `subjectId`) and never where their mail goes: the address is erasable and lives only on
12
+ * the engine's recipient row and on the platform's send intent, both of which an erasure
13
+ * reaches. A field added here must keep that true.
14
+ *
15
+ * Additive, like every other engine surface: a new event type may appear, an existing
16
+ * payload field does not change shape without a `schemaVersion` bump.
17
+ */
18
+ import type { DomainEventInput, EntityRef } from '@substrat-run/contracts';
19
+ import type { OperationContext } from '@substrat-run/kernel';
20
+ /**
21
+ * What `notify` did for one (occasion, topic, person, channel).
22
+ *
23
+ * `requested`: a send intent was written, and `request` is its id. `skipped`: nothing was
24
+ * sent on its own, by preference (`reason: 'muted'`), for want of an address (`'no_address'`),
25
+ * or because the person takes this topic in a digest (`'digest'`): the occasion is held for
26
+ * their next one. What becomes of a held occasion is told later, naming this `deliveryId`: the
27
+ * `notifications.digest-sent` that carried it, the `notifications.preference-set` whose
28
+ * switch-off withdrew it (its delivery then reads `skipped`/`muted`), or the
29
+ * `notifications.digest-quarantined` that set it aside unreadable (`skipped`/`quarantined`, until
30
+ * a `notifications.digest-released` holds it again), or the `notifications.digest-withdrawn` that
31
+ * withdrew it because the topic was switched off since (`skipped`/`muted`). An erasure of the person
32
+ * deletes it with no event, like a held send it cancels, and its delivery stays as it was.
33
+ * `dedupeKey` is the occasion as the caller named it — from a consumer, the triggering event's id.
34
+ *
35
+ * A digest is a delivery as well, under the reserved topic `@digest` (no vertical topic can be
36
+ * spelled that way), with `about: null`.
37
+ */
38
+ export interface NotificationsDeliveryRecordedPayload {
39
+ deliveryId: string;
40
+ topic: string;
41
+ principal: string;
42
+ channel: string;
43
+ status: 'requested' | 'skipped';
44
+ reason: string | null;
45
+ request: string | null;
46
+ dedupeKey: string;
47
+ about: EntityRef | null;
48
+ }
49
+ /**
50
+ * What the platform reported back for a `requested` delivery: the provider accepted it
51
+ * (`sent`), refused it for good (`refused`), or every attempt failed (`dead_lettered`).
52
+ * `code` is the platform's taxonomy code for a refusal or the last failure; null on `sent`.
53
+ */
54
+ export interface NotificationsDeliverySettledPayload {
55
+ deliveryId: string;
56
+ topic: string;
57
+ principal: string;
58
+ channel: string;
59
+ status: 'sent' | 'refused' | 'dead_lettered';
60
+ code: string | null;
61
+ request: string;
62
+ }
63
+ /**
64
+ * A person switched one topic on or off for one channel, and perhaps chose when it arrives.
65
+ * A switch-off also withdraws that topic's occasions still held for their digest.
66
+ */
67
+ export interface NotificationsPreferenceSetPayload {
68
+ principal: string;
69
+ topic: string;
70
+ channel: string;
71
+ enabled: boolean;
72
+ /**
73
+ * When the topic reaches them after the call — always `immediate` for a required topic.
74
+ * Absent on events written before digests existed.
75
+ */
76
+ cadence?: 'immediate' | 'daily-digest';
77
+ /**
78
+ * The deliveries of the occasions this switch-off withdrew from the person's digest, each now
79
+ * `skipped`/`muted`. Absent when it withdrew none.
80
+ */
81
+ withdrawn?: string[];
82
+ }
83
+ /** A person set or cleared where their notifications go — whether, never what. */
84
+ export interface NotificationsRecipientUpdatedPayload {
85
+ principal: string;
86
+ hasAddress: boolean;
87
+ }
88
+ /** A topic was first used, or its description or `required` changed. Names nobody. */
89
+ export interface NotificationsTopicRecordedPayload {
90
+ topic: string;
91
+ description: string;
92
+ required: boolean;
93
+ }
94
+ /** One occasion a digest carried, named by its own (`skipped`, `digest`) delivery. */
95
+ export interface NotificationsDigestItem {
96
+ deliveryId: string;
97
+ topic: string;
98
+ dedupeKey: string;
99
+ about: EntityRef | null;
100
+ queuedAt: string;
101
+ }
102
+ /**
103
+ * One person's digest was composed from everything held for them: the occasions it carried,
104
+ * oldest first, and the digest's own delivery (topic `@digest`). `status` is that delivery's:
105
+ * `requested` with the send intent's `request`, or `skipped` with `reason: 'no_address'` when
106
+ * the person had cleared their address since. Either way the items are no longer held, and the
107
+ * digest's delivery settles like any other.
108
+ */
109
+ export interface NotificationsDigestSentPayload {
110
+ deliveryId: string;
111
+ principal: string;
112
+ channel: string;
113
+ cadence: 'daily-digest';
114
+ status: 'requested' | 'skipped';
115
+ reason: string | null;
116
+ request: string | null;
117
+ items: NotificationsDigestItem[];
118
+ }
119
+ /**
120
+ * A person's digest was due and could not be composed or written — a send or a write that
121
+ * failed. (A held message that no longer reads is set aside instead; see
122
+ * `notifications.digest-quarantined`.) Nothing was sent for them and nothing of theirs was let
123
+ * go: their `items` stay held, and the next attempt is at `retryAt`, a window later. Everyone
124
+ * else's digest in the same pass went out regardless. `code` is the platform's error code; the
125
+ * cause itself is on the error line the pass writes, not here.
126
+ */
127
+ export interface NotificationsDigestFailedPayload {
128
+ principal: string;
129
+ channel: string;
130
+ cadence: 'daily-digest';
131
+ items: number;
132
+ code: string;
133
+ retryAt: string;
134
+ }
135
+ /**
136
+ * Held occasions of one person's digest no longer read — a stored row that left the shape this
137
+ * engine publishes — and were set aside rather than mailed or allowed to block the rest. Each is
138
+ * marked, its delivery reads `skipped` with reason `quarantined`, and the person's readable
139
+ * messages went out as usual. A set-aside row is read again once a window and released when it
140
+ * reads (`notifications.digest-released`); this event is written when a row is first set aside,
141
+ * never again while it stays unreadable. `occasions` names the deliveries whose id still read;
142
+ * `unidentified` counts the rows whose id did not. Never what a row held.
143
+ */
144
+ export interface NotificationsDigestQuarantinedPayload {
145
+ principal: string;
146
+ channel: string;
147
+ occasions: string[];
148
+ unidentified: number;
149
+ }
150
+ /**
151
+ * Set-aside occasions of one person read again — whatever left them unreadable, a version that
152
+ * stored a shape this one rejects, say, is gone — and are held for the digest once more: each
153
+ * delivery reads `skipped`/`digest` again, unsettled, and goes out in the person's next digest.
154
+ * One whose topic the person has switched off since is not released but withdrawn
155
+ * (`notifications.digest-withdrawn`).
156
+ */
157
+ export interface NotificationsDigestReleasedPayload {
158
+ principal: string;
159
+ channel: string;
160
+ occasions: string[];
161
+ }
162
+ /**
163
+ * Held occasions of one person were withdrawn, unsent, because the person has switched their
164
+ * topic off since they were held, found when a set-aside row read again or when the digest was
165
+ * composed. Each delivery reads `skipped`/`muted`. (A switch-off's own withdrawal is named on
166
+ * `notifications.preference-set`.) A mute always wins over a digest.
167
+ */
168
+ export interface NotificationsDigestWithdrawnPayload {
169
+ principal: string;
170
+ channel: string;
171
+ occasions: string[];
172
+ }
173
+ export type NotificationsEvents = {
174
+ events: {
175
+ 'notifications.delivery-recorded': NotificationsDeliveryRecordedPayload;
176
+ 'notifications.delivery-settled': NotificationsDeliverySettledPayload;
177
+ 'notifications.preference-set': NotificationsPreferenceSetPayload;
178
+ 'notifications.recipient-updated': NotificationsRecipientUpdatedPayload;
179
+ 'notifications.topic-recorded': NotificationsTopicRecordedPayload;
180
+ 'notifications.digest-sent': NotificationsDigestSentPayload;
181
+ 'notifications.digest-failed': NotificationsDigestFailedPayload;
182
+ 'notifications.digest-quarantined': NotificationsDigestQuarantinedPayload;
183
+ 'notifications.digest-released': NotificationsDigestReleasedPayload;
184
+ 'notifications.digest-withdrawn': NotificationsDigestWithdrawnPayload;
185
+ };
186
+ };
187
+ /** Every event type this engine emits. */
188
+ export type NotificationsEventType = keyof NotificationsEvents['events'];
189
+ /** The one home for each event's `schemaVersion` (#1597); `emits` is read off it. */
190
+ export declare const notificationsEventVersions: {
191
+ readonly 'notifications.delivery-recorded': 1;
192
+ readonly 'notifications.delivery-settled': 1;
193
+ readonly 'notifications.preference-set': 1;
194
+ readonly 'notifications.recipient-updated': 1;
195
+ readonly 'notifications.topic-recorded': 1;
196
+ readonly 'notifications.digest-sent': 1;
197
+ readonly 'notifications.digest-failed': 1;
198
+ readonly 'notifications.digest-quarantined': 1;
199
+ readonly 'notifications.digest-released': 1;
200
+ readonly 'notifications.digest-withdrawn': 1;
201
+ };
202
+ /** The manifest's `events.emits`, read off {@link notificationsEventVersions}. */
203
+ export declare const notificationsEmitDeclarations: {
204
+ type: string;
205
+ schemaVersion: number;
206
+ }[];
207
+ /** `ctx.emit` with the event type and its payload welded together, and the version stamped. */
208
+ export declare function emitNotificationsEvent<K extends NotificationsEventType>(ctx: OperationContext, event: Omit<DomainEventInput, 'type' | 'payload' | 'schemaVersion'> & {
209
+ type: K;
210
+ payload: NotificationsEvents['events'][K];
211
+ }): void;
212
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
package/dist/events.js ADDED
@@ -0,0 +1,20 @@
1
+ /** The one home for each event's `schemaVersion` (#1597); `emits` is read off it. */
2
+ export const notificationsEventVersions = {
3
+ 'notifications.delivery-recorded': 1,
4
+ 'notifications.delivery-settled': 1,
5
+ 'notifications.preference-set': 1,
6
+ 'notifications.recipient-updated': 1,
7
+ 'notifications.topic-recorded': 1,
8
+ 'notifications.digest-sent': 1,
9
+ 'notifications.digest-failed': 1,
10
+ 'notifications.digest-quarantined': 1,
11
+ 'notifications.digest-released': 1,
12
+ 'notifications.digest-withdrawn': 1,
13
+ };
14
+ /** The manifest's `events.emits`, read off {@link notificationsEventVersions}. */
15
+ export const notificationsEmitDeclarations = Object.entries(notificationsEventVersions).map(([type, schemaVersion]) => ({ type, schemaVersion }));
16
+ /** `ctx.emit` with the event type and its payload welded together, and the version stamped. */
17
+ export function emitNotificationsEvent(ctx, event) {
18
+ ctx.emit({ ...event, schemaVersion: notificationsEventVersions[event.type] });
19
+ }
20
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AA0MA,qFAAqF;AACrF,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,iCAAiC,EAAE,CAAC;IACpC,gCAAgC,EAAE,CAAC;IACnC,8BAA8B,EAAE,CAAC;IACjC,iCAAiC,EAAE,CAAC;IACpC,8BAA8B,EAAE,CAAC;IACjC,2BAA2B,EAAE,CAAC;IAC9B,6BAA6B,EAAE,CAAC;IAChC,kCAAkC,EAAE,CAAC;IACrC,+BAA+B,EAAE,CAAC;IAClC,gCAAgC,EAAE,CAAC;CACsB,CAAC;AAE5D,kFAAkF;AAClF,MAAM,CAAC,MAAM,6BAA6B,GAA8C,MAAM,CAAC,OAAO,CACpG,0BAA0B,CAC3B,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;AAE5D,+FAA+F;AAC/F,MAAM,UAAU,sBAAsB,CACpC,GAAqB,EACrB,KAGC;IAED,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,0BAA0B,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAChF,CAAC"}
@@ -0,0 +1,411 @@
1
+ /**
2
+ * engine-notifications — composed **by call**, not by event.
3
+ *
4
+ * The in-scope functions are the surface: `notify`, which turns one occasion into mail to the
5
+ * people it concerns, and the preference functions behind the operations below. A vertical
6
+ * decides WHEN to notify — usually in its own consumer of a domain event, e.g. Handlebar's
7
+ * consumer of `workorder.assigned` — and calls `notify(ctx, …)` there, inside its own
8
+ * transaction, with the template it owns. The engine owns everything after that call: the
9
+ * mute and required check, the address, the escaped render, the send intent, and a delivery
10
+ * row that follows the send to its outcome.
11
+ *
12
+ * Placement (K-47): notifications are domain logic over the kernel's primitives, so they are
13
+ * an engine. The kernel's whole part is the transactional send intent (`requestEmail`, #2102)
14
+ * and the outcome events its settle writes back; the transport behind that intent is
15
+ * `adapter-email`. Nothing here reaches a provider.
16
+ *
17
+ * The registration is one frozen constant like every other engine's. Its only consumers are the
18
+ * three email outcome events, which settle the deliveries it wrote; what triggers a notification
19
+ * is the vertical's code, so nothing about that appears here.
20
+ *
21
+ * Digests are the person's choice, per topic: a topic they take as `daily-digest` is held
22
+ * rather than mailed. The engine's own schedule (`notifications/send-digests`, run by its system
23
+ * principal every `DIGEST_TICK_MINUTES`) sends each person whose oldest held message is a day old
24
+ * one mail of up to `DIGEST_MAX_ITEMS` of them, oldest first, through the same send intent and
25
+ * delivery row as any notification, one person at a time and never more at once than the
26
+ * scope's send backlog has room for. A mute still wins, and a required topic always goes out at
27
+ * once.
28
+ *
29
+ * Deferred, and stated so an absence reads as intent: channels other than email (SMS, push,
30
+ * in-app — each a transport and a `notificationChannel` value), tenant-edited or per-locale
31
+ * templates (a table keyed by topic overriding the template the caller passes, which is also
32
+ * where a digest's own wording would come from), quiet hours and a digest's time of day, other
33
+ * digest cadences (today any cadence row holds a topic for the one daily schedule: a second
34
+ * cadence needs a `digestCadence` value, the cadence recorded on each held item, and a schedule
35
+ * that sends only its own).
36
+ */
37
+ import { z } from 'zod';
38
+ import { type Page } from '@substrat-run/contracts';
39
+ import { type ModuleRegistration, type OperationContext, type PageParams } from '@substrat-run/kernel';
40
+ /**
41
+ * The conflict reasons this engine raises, narrowing the platform's `conflict` code (#113).
42
+ * Additive only: new reasons may appear, existing ones do not change spelling.
43
+ */
44
+ export declare const NOTIFICATIONS_CONFLICT_REASONS: readonly ['topic_required'];
45
+ export type NotificationsConflictReason = (typeof NOTIFICATIONS_CONFLICT_REASONS)[number];
46
+ export { notificationsEntities, deliveryRow, myDeliveryRow } from './entities.js';
47
+ import { deliveryRow, myDeliveryRow } from './entities.js';
48
+ export { notificationsOperations, NOTIFICATIONS_PERMISSIONS, notificationCadence, notificationChannel, myPreferences, topicPreference, type NotificationCadence, type NotificationChannel, } from './operations.js';
49
+ import { myPreferences, type NotificationCadence, type NotificationChannel } from './operations.js';
50
+ export { type NotificationsEvents, type NotificationsEventType, type NotificationsDeliveryRecordedPayload, type NotificationsDeliverySettledPayload, type NotificationsDigestFailedPayload, type NotificationsDigestItem, type NotificationsDigestQuarantinedPayload, type NotificationsDigestReleasedPayload, type NotificationsDigestWithdrawnPayload, type NotificationsDigestSentPayload, type NotificationsPreferenceSetPayload, type NotificationsRecipientUpdatedPayload, type NotificationsTopicRecordedPayload, } from './events.js';
51
+ export { type NotificationTemplate } from './render.js';
52
+ export declare const NOTIFICATIONS_PERM: {
53
+ self: string & z.$brand<"PermissionKey">;
54
+ read: string & z.$brand<"PermissionKey">;
55
+ sendDigests: string & z.$brand<"PermissionKey">;
56
+ };
57
+ /**
58
+ * The topic of a digest's own delivery. A vertical's topic is a lowercase slug, so none can be
59
+ * spelled this way: filtering the delivery list on it lists digests and nothing else.
60
+ */
61
+ export declare const DIGEST_TOPIC = "@digest";
62
+ /**
63
+ * How often the digest schedule looks for people whose digest is due. Each pass is one indexed
64
+ * query when nobody is; a backlog bigger than one pass's room drains over the next passes.
65
+ */
66
+ export declare const DIGEST_TICK_MINUTES = 15;
67
+ /**
68
+ * The pending-send slots a digest pass leaves free, of the scope's
69
+ * `MAX_PENDING_PLATFORM_REQUESTS`: digests can wait for the drain, and mail that must not —
70
+ * a required topic above all — is never refused because digests filled the backlog.
71
+ */
72
+ export declare const DIGEST_RESERVE = 8;
73
+ /**
74
+ * The most held occasions one digest carries, oldest first. The rest stay held and are due by
75
+ * the same rule as any — once the oldest of them is a window old — so a person with more held
76
+ * than this gets several digests rather than one unbounded mail, and a pass reads a bounded
77
+ * number of rows for each person.
78
+ */
79
+ export declare const DIGEST_MAX_ITEMS = 50;
80
+ export declare const notificationsManifest: {
81
+ id: string & z.$brand<"ModuleId">;
82
+ version: string;
83
+ kernelContract: string;
84
+ permissions: {
85
+ key: string & z.$brand<"PermissionKey">;
86
+ description: string;
87
+ holder?: "system" | undefined;
88
+ }[];
89
+ events: {
90
+ emits: {
91
+ type: string;
92
+ schemaVersion: number;
93
+ }[];
94
+ consumes: {
95
+ type: string;
96
+ schemaVersion: number;
97
+ from?: string | undefined;
98
+ }[];
99
+ exports?: {
100
+ type: string;
101
+ schemaVersion: number;
102
+ readPermission: string & z.$brand<"PermissionKey">;
103
+ }[] | undefined;
104
+ };
105
+ migrations: {
106
+ journalDir: string;
107
+ compatibleFrom: string;
108
+ };
109
+ attachmentTargets: {
110
+ entityType: string;
111
+ readPermission: string & z.$brand<"PermissionKey">;
112
+ writePermission?: (string & z.$brand<"PermissionKey">) | undefined;
113
+ uploadLinks?: {
114
+ maxBytes: number;
115
+ contentTypes?: string[] | undefined;
116
+ } | undefined;
117
+ }[];
118
+ liveTargets?: {
119
+ entityType: string;
120
+ readPermission: string & z.$brand<"PermissionKey">;
121
+ }[] | undefined;
122
+ entityRelations?: {
123
+ entityType: string;
124
+ parentType: string;
125
+ }[] | undefined;
126
+ guards?: {
127
+ before: string;
128
+ predicate: string;
129
+ config: Record<string, unknown>;
130
+ }[] | undefined;
131
+ schedules?: {
132
+ operation: string;
133
+ cadence: {
134
+ everyMinutes: number;
135
+ };
136
+ input?: Record<string, unknown> | undefined;
137
+ permissions: (string & z.$brand<"PermissionKey">)[];
138
+ purge?: {
139
+ entityType: string;
140
+ } | undefined;
141
+ }[] | undefined;
142
+ peers?: {
143
+ vertical: string;
144
+ operations: string[];
145
+ permissions: (string & z.$brand<"PermissionKey">)[];
146
+ }[] | undefined;
147
+ connections?: {
148
+ provider: string;
149
+ operation: string;
150
+ permission: string & z.$brand<"PermissionKey">;
151
+ options?: Record<string, unknown> | undefined;
152
+ }[] | undefined;
153
+ freshness?: {
154
+ eventType: string;
155
+ within: {
156
+ hours: number;
157
+ };
158
+ }[] | undefined;
159
+ withdraws?: string[] | undefined;
160
+ entitlementKey: string;
161
+ envSpec?: {
162
+ key: string;
163
+ label?: string | undefined;
164
+ description: string;
165
+ placeholder?: string | undefined;
166
+ required: boolean;
167
+ secret: boolean;
168
+ default?: string | undefined;
169
+ group?: string | undefined;
170
+ }[] | undefined;
171
+ ownerGrants?: (string & z.$brand<"PermissionKey">)[] | undefined;
172
+ entitlements?: string[] | undefined;
173
+ provides?: string[] | undefined;
174
+ requires?: string[] | undefined;
175
+ api?: string | undefined;
176
+ searchables?: {
177
+ entityType: string;
178
+ fields: string[];
179
+ table?: string | undefined;
180
+ idColumn?: string | undefined;
181
+ tokenizer?: "prefix" | "substring" | undefined;
182
+ }[] | undefined;
183
+ lists?: {
184
+ entityType: string;
185
+ sortable: string[];
186
+ filterable?: string[] | undefined;
187
+ table?: string | undefined;
188
+ idColumn?: string | undefined;
189
+ }[] | undefined;
190
+ entityStates?: {
191
+ archivePermission?: (string & z.$brand<"PermissionKey">) | undefined;
192
+ trashPermission?: (string & z.$brand<"PermissionKey">) | undefined;
193
+ purgeAfterDays?: number | undefined;
194
+ purgeEarly?: boolean | undefined;
195
+ table?: string | undefined;
196
+ idColumn?: string | undefined;
197
+ entityType: string;
198
+ }[] | undefined;
199
+ erasure?: {
200
+ tables: string[];
201
+ entities: {
202
+ entityType: string;
203
+ table: string;
204
+ mode: "blank" | "custom" | "delete" | "unreached";
205
+ subjects?: string[] | undefined;
206
+ fields: {
207
+ name: string;
208
+ blank: "" | null;
209
+ }[];
210
+ }[];
211
+ } | undefined;
212
+ updatedStamps?: {
213
+ table: string;
214
+ idColumn: string;
215
+ at?: string | undefined;
216
+ by?: string | undefined;
217
+ entityType: string;
218
+ }[] | undefined;
219
+ ui?: {
220
+ routes?: {
221
+ path: string;
222
+ screen: string;
223
+ permission: string & z.$brand<"PermissionKey">;
224
+ }[] | undefined;
225
+ nav?: {
226
+ label: string;
227
+ icon?: string | undefined;
228
+ to: string;
229
+ permission: string & z.$brand<"PermissionKey">;
230
+ }[] | undefined;
231
+ entityViews?: {
232
+ entityType: string;
233
+ view: string;
234
+ }[] | undefined;
235
+ widgets?: {
236
+ slot: string;
237
+ component: string;
238
+ permission: string & z.$brand<"PermissionKey">;
239
+ }[] | undefined;
240
+ settingsPanels?: {
241
+ label: string;
242
+ component: string;
243
+ permission: string & z.$brand<"PermissionKey">;
244
+ }[] | undefined;
245
+ } | undefined;
246
+ };
247
+ export declare const notificationsMigrations: {
248
+ version: string;
249
+ sql: string;
250
+ }[];
251
+ export declare const notifyInput: z.ZodObject<{
252
+ topic: z.ZodString;
253
+ description: z.ZodString;
254
+ dedupeKey: z.ZodString;
255
+ recipients: z.ZodArray<z.core.$ZodBranded<z.ZodString, "DataSubjectId", "out">>;
256
+ vars: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
257
+ about: z.ZodDefault<z.ZodNullable<z.ZodObject<{
258
+ entityType: z.ZodString;
259
+ entityId: z.ZodString;
260
+ }, z.core.$strip>>>;
261
+ template: z.ZodObject<{
262
+ subject: z.ZodString;
263
+ text: z.ZodString;
264
+ html: z.ZodOptional<z.ZodString>;
265
+ }, z.core.$strip>;
266
+ required: z.ZodDefault<z.ZodBoolean>;
267
+ }, z.core.$strip>;
268
+ export type NotifyInput = z.input<typeof notifyInput>;
269
+ /** What `notify` did for one recipient. */
270
+ export interface NotifyOutcome {
271
+ principal: string;
272
+ status: 'requested' | 'skipped';
273
+ /**
274
+ * On a skip: `muted`, `no_address`, `duplicate` (this key already reached them), or `digest`
275
+ * (held for their next digest rather than mailed on its own). Null when requested.
276
+ */
277
+ reason: string | null;
278
+ /** The send intent's id, when one was written. */
279
+ request: string | null;
280
+ /** The delivery row, or null on a `duplicate`, which writes none. */
281
+ deliveryId: string | null;
282
+ }
283
+ /**
284
+ * Notify `recipients` about one occasion, inside the caller's transaction.
285
+ *
286
+ * For each person: a delivery this `dedupeKey` already made for them is left alone; a muted
287
+ * topic (unless `required`) or a missing address records a `skipped` delivery; a topic they
288
+ * take as a digest (unless `required`) records a `skipped` delivery with reason `digest` and
289
+ * holds the rendered message for `sendDigests`; otherwise a send intent is written with
290
+ * `requestEmail`, naming the person as its `subjectId`, and a `requested` delivery records it.
291
+ * The platform's outcome event settles that delivery later (the engine's own consumers).
292
+ *
293
+ * **It throws `validation_failed`, writing nothing, when the template cannot be filled** — a
294
+ * placeholder with no variable, or a message every word of which was a blank variable — or a
295
+ * recipient is not a principal id. That is a defect in the caller, and the throw rolls back the
296
+ * caller's whole transaction: from a consumer, the event dead-letters in the kernel's delivery
297
+ * journal with the topic and the reason in its error; from an operation, the operation fails.
298
+ * A blank mail is never the fallback.
299
+ *
300
+ * No permission check: like every by-call engine function, the caller checks first. A consumer
301
+ * calling it runs under the kernel's consumer authority.
302
+ */
303
+ export declare function notify(ctx: OperationContext, rawInput: NotifyInput): NotifyOutcome[];
304
+ /** The caller's address and every known topic's switch and cadence. */
305
+ export declare function myPreferencesOf(ctx: OperationContext): z.infer<typeof myPreferences>;
306
+ /** Set or clear where the caller's notifications go. */
307
+ export declare function setMyAddress(ctx: OperationContext, input: {
308
+ email: string | null;
309
+ }): z.infer<typeof myPreferences>;
310
+ /**
311
+ * Switch one topic on or off for the caller, and with `cadence`, choose whether it arrives at
312
+ * once or in their daily digest (absent leaves that as it is). The topic must be one this scope
313
+ * has used (`not_found` otherwise). A required topic refuses to be switched off, and refuses a
314
+ * digest: mail a person must not miss does not wait a day either.
315
+ *
316
+ * Switching a topic off withdraws its items still held for the caller's digest — a mute wins
317
+ * over the queue. Each withdrawn occasion's delivery is settled `skipped`/`muted`, as if the mute
318
+ * had come first, and the event names them. Switching a digest back to `immediate` leaves what is
319
+ * already held to go out with the next digest.
320
+ */
321
+ export declare function setMyPreference(ctx: OperationContext, input: {
322
+ topic: string;
323
+ channel: NotificationChannel;
324
+ enabled: boolean;
325
+ cadence?: NotificationCadence | undefined;
326
+ }): z.infer<typeof myPreferences>;
327
+ /** What became of each notification, newest first. */
328
+ export declare function listDeliveries(ctx: OperationContext, page: PageParams): Page<z.infer<typeof deliveryRow>>;
329
+ /**
330
+ * What became of each notification sent to the caller, newest first: the same page as
331
+ * `listDeliveries`, with the `principal` filter pinned to `ctx.principal`, last, so nothing in
332
+ * `page` can name anyone else.
333
+ *
334
+ * Every delivery of theirs is listed, including the ones that were not mailed on their own — a
335
+ * topic they muted (`skipped`/`muted`), a missing address (`skipped`/`no_address`), an occasion
336
+ * held for their digest (`skipped`/`digest`, settled once the digest that carried it is
337
+ * written) — and the digest itself, which is a delivery of its own under `DIGEST_TOPIC`. A held
338
+ * message set aside as unreadable reads `skipped`/`quarantined` until it is released.
339
+ *
340
+ * The row is `myDeliveryRow`, not the administrator's: it names neither the caller's idempotency
341
+ * key nor the platform's send intent, and a `refused` or `dead_lettered` row carries no `reason`,
342
+ * since that is the platform's outcome code (a missing sender grant, a provider refusal) and
343
+ * belongs to whoever reads the whole log. The `reason` of a `skipped` row is the engine's own
344
+ * word for it.
345
+ *
346
+ * No permission check: like every by-call engine function, the caller checks first.
347
+ */
348
+ export declare function listMyDeliveries(ctx: OperationContext, page: PageParams): Page<z.infer<typeof myDeliveryRow>>;
349
+ /** What one pass of `sendDigests` did. */
350
+ export interface DigestPass {
351
+ /** People whose digest went out (or was recorded skipped for want of an address). */
352
+ digests: number;
353
+ /** The occasions those digests carried. */
354
+ items: number;
355
+ /** People whose digest could not be composed: theirs stay held, retried a window later. */
356
+ failed: number;
357
+ /** People who were due but left for a later pass, because the scope had no room for more sends. */
358
+ deferred: number;
359
+ /** Held occasions found unreadable and set aside this pass, unsent. */
360
+ quarantined: number;
361
+ /** Set-aside occasions that read again this pass and are held for the digest once more. */
362
+ released: number;
363
+ /** Held occasions withdrawn this pass, unsent, because their person has switched the topic off since. */
364
+ withdrawn: number;
365
+ }
366
+ /**
367
+ * Send each person whose digest is due one mail of what is held for them.
368
+ *
369
+ * **Who is due.** A person is due once their oldest held message is a whole window old (a day,
370
+ * for `daily-digest`). So nothing waits much longer than a day, and while no more than
371
+ * `DIGEST_MAX_ITEMS` are held, a person gets one digest a day: everything held after it is
372
+ * younger. Oldest first, so a backlog drains in the order it built up. A person whose last
373
+ * attempt failed waits a window before the next.
374
+ *
375
+ * **How many people.** Each mail is a platform send intent, and a scope may hold only
376
+ * `MAX_PENDING_PLATFORM_REQUESTS` of those waiting for the drain — this transaction's own
377
+ * included. A pass therefore sends to at most the room that is left, less `DIGEST_RESERVE`
378
+ * slots kept for mail that must not wait, and leaves everyone else held for the next pass. It
379
+ * never asks for more room than there is, so it never meets the cap's refusal.
380
+ *
381
+ * **How much each.** One digest carries at most `DIGEST_MAX_ITEMS` occasions, oldest first. The
382
+ * rest stay held, and are due by the same rule: as soon as the oldest of them is a window old —
383
+ * on the very next pass if it already is. That is the one case where a person gets more than one
384
+ * digest a day.
385
+ *
386
+ * **What does not read.** A held row that no longer matches the published shape is set aside
387
+ * (`quarantine`) rather than mailed or allowed to block the rest: the person's readable messages
388
+ * go out, and the set-aside row counts toward nothing — not who is due, not a switch-off's
389
+ * withdrawal, not the stale-hold warning. Its cause may pass (a version that stored a shape this
390
+ * one rejects is rolled forward again), so each set-aside row is read again once a window
391
+ * (`releaseReadable`, at the start of a pass) and held once more when it reads — unless its topic
392
+ * was switched off meanwhile, when it is withdrawn instead.
393
+ *
394
+ * **A mute wins, last of all.** The read that composes a digest carries each row's mute
395
+ * (`MUTED`), and a held message whose topic is switched off is withdrawn (`withdrawMuted`), never
396
+ * mailed, whatever path brought it there.
397
+ *
398
+ * **One person at a time.** Each person's digest is its own `ctx.atomic`: one mail written with
399
+ * `requestEmail`, one delivery under `DIGEST_TOPIC`, the carried occasions settled and no longer
400
+ * held. If any of that throws, none of it happened for that person — their messages stay held,
401
+ * `notifications.digest-failed` and an error line say so — and everyone else's digest still goes
402
+ * out. A person who cleared their address since gets a `skipped` (`no_address`) digest, as an
403
+ * immediate notification would have.
404
+ *
405
+ * The engine's schedule runs it through `notifications/send-digests` every
406
+ * `DIGEST_TICK_MINUTES`; a vertical may also call it, after its own permission check, like every
407
+ * by-call function here. Either way it is one pass, and it sends only the people who are due.
408
+ */
409
+ export declare function sendDigests(ctx: OperationContext): Promise<DigestPass>;
410
+ export declare const notificationsModule: ModuleRegistration;
411
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}