@mostajs/notifications 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +37 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -22
- package/dist/index.js.map +1 -1
- package/dist/multi-channel.d.ts +39 -0
- package/dist/multi-channel.d.ts.map +1 -0
- package/dist/multi-channel.js +29 -0
- package/dist/multi-channel.js.map +1 -0
- package/dist/throttle.d.ts +13 -0
- package/dist/throttle.d.ts.map +1 -0
- package/dist/throttle.js +22 -0
- package/dist/throttle.js.map +1 -0
- package/llms.txt +102 -0
- package/package.json +10 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { type ThrottleStore } from './throttle.js';
|
|
2
|
+
import type { Mail, MailAttachment } from '@mostajs/mailer';
|
|
3
|
+
export type { ThrottleStore } from './throttle.js';
|
|
4
|
+
export { createInMemoryThrottleStore } from './throttle.js';
|
|
5
|
+
export { createMultiChannelSender, smsMailAdapter } from './multi-channel.js';
|
|
6
|
+
export type { ChannelSender } from './multi-channel.js';
|
|
7
|
+
export type { Mail, MailAttachment };
|
|
1
8
|
export type Locale = string;
|
|
2
9
|
export interface NotificationContext {
|
|
3
10
|
/** Locale du destinataire — détermine quel template variant utiliser. */
|
|
@@ -5,24 +12,28 @@ export interface NotificationContext {
|
|
|
5
12
|
/** Champs libres consommés par le template (reservation, slot, session, etc.). */
|
|
6
13
|
[key: string]: unknown;
|
|
7
14
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from?: string;
|
|
13
|
-
subject: string;
|
|
14
|
-
text?: string;
|
|
15
|
-
html?: string;
|
|
16
|
-
/** Idempotence : mailer skip si déjà sent avec ce messageId. */
|
|
17
|
-
messageId?: string;
|
|
18
|
-
/** Metadata libre persistée dans MailLog. */
|
|
19
|
-
metadata?: Record<string, unknown>;
|
|
20
|
-
}
|
|
15
|
+
/** Sortie d'un template — un `Mail` au sens de `@mostajs/mailer`. La forme
|
|
16
|
+
* complète est disponible (attachments, headers, replyTo, …) ; notifications
|
|
17
|
+
* se contente d'y ajouter `kind` / `locale` dans `metadata`. */
|
|
18
|
+
export type RenderedMail = Mail;
|
|
21
19
|
/** Template = fonction qui produit un Mail à partir du contexte.
|
|
22
20
|
* Retour null → skip (template applicable mais l'app a décidé de pas envoyer). */
|
|
23
21
|
export interface NotificationTemplate {
|
|
24
22
|
render(context: NotificationContext): Promise<RenderedMail | null> | RenderedMail | null;
|
|
25
23
|
}
|
|
24
|
+
/** Options de throttle attachées à un kind. (v0.2 / N2) */
|
|
25
|
+
export interface ThrottleOptions {
|
|
26
|
+
/** Fenêtre durant laquelle un envoi suivant pour la même clé est skippé. */
|
|
27
|
+
windowMs: number;
|
|
28
|
+
/** Construit la clé secondaire (combinée avec `kind`). Défaut : clé vide. */
|
|
29
|
+
keyFrom?: (context: NotificationContext) => string;
|
|
30
|
+
}
|
|
31
|
+
/** Options de registration d'un template. (v0.2) */
|
|
32
|
+
export interface RegisterOptions {
|
|
33
|
+
throttle?: ThrottleOptions;
|
|
34
|
+
}
|
|
35
|
+
/** Cause d'un skip retourné par `notify(...) → null`. (v0.2) */
|
|
36
|
+
export type SkipReason = 'disabled' | 'unknown-kind' | 'template-null' | 'no-recipient' | 'render-error' | 'send-error' | 'throttled';
|
|
26
37
|
export interface NotificationEngineOptions {
|
|
27
38
|
/** Instance @mostajs/mailer (ou compatible : tout objet avec `.send(mail)`). */
|
|
28
39
|
mailer: {
|
|
@@ -38,16 +49,22 @@ export interface NotificationEngineOptions {
|
|
|
38
49
|
defaultFrom?: string;
|
|
39
50
|
/** Désactive certains kinds (e.g. mode dégradé). */
|
|
40
51
|
disabled?: string[];
|
|
52
|
+
/** Store de throttle injecté. Défaut : in-memory. (v0.2 / N2) */
|
|
53
|
+
throttleStore?: ThrottleStore;
|
|
54
|
+
/** Source d'horloge (testabilité). Défaut : `() => Date.now()`. */
|
|
55
|
+
now?: () => number;
|
|
41
56
|
/** Callback succès. */
|
|
42
57
|
onSent?: (kind: string, context: NotificationContext, result: {
|
|
43
58
|
messageId: string;
|
|
44
59
|
}) => void | Promise<void>;
|
|
45
60
|
/** Callback erreur. */
|
|
46
61
|
onError?: (kind: string, context: NotificationContext, error: Error) => void | Promise<void>;
|
|
62
|
+
/** Callback skip — y compris throttle, unknown-kind, etc. (v0.2) */
|
|
63
|
+
onSkipped?: (kind: string, context: NotificationContext, reason: SkipReason) => void | Promise<void>;
|
|
47
64
|
}
|
|
48
65
|
export interface NotificationEngine {
|
|
49
|
-
/** Enregistre / override un template. */
|
|
50
|
-
register(kind: string, template: NotificationTemplate): void;
|
|
66
|
+
/** Enregistre / override un template. `options.throttle` paramètre la fenêtre anti-spam. */
|
|
67
|
+
register(kind: string, template: NotificationTemplate, options?: RegisterOptions): void;
|
|
51
68
|
/** Retire un template. */
|
|
52
69
|
unregister(kind: string): void;
|
|
53
70
|
/** Liste les kinds enregistrés. */
|
|
@@ -65,6 +82,9 @@ export interface NotificationEngine {
|
|
|
65
82
|
}>): Promise<Array<{
|
|
66
83
|
messageId: string;
|
|
67
84
|
} | null>>;
|
|
85
|
+
/** Render-only : renvoie le `RenderedMail` sans dispatcher ni journaliser.
|
|
86
|
+
* Utilisable pour prévisualiser un rapport mensuel côté UI. (v0.2 / N3) */
|
|
87
|
+
preview(kind: string, context: NotificationContext): Promise<RenderedMail | null>;
|
|
68
88
|
}
|
|
69
89
|
export declare function createNotificationEngine(opts: NotificationEngineOptions): NotificationEngine;
|
|
70
90
|
/**
|
|
@@ -78,6 +98,7 @@ export declare function simpleTemplate(spec: {
|
|
|
78
98
|
text?: (ctx: NotificationContext) => string;
|
|
79
99
|
html?: (ctx: NotificationContext) => string;
|
|
80
100
|
messageId?: (ctx: NotificationContext) => string;
|
|
101
|
+
attachments?: (ctx: NotificationContext) => MailAttachment[] | undefined;
|
|
81
102
|
}): NotificationTemplate;
|
|
82
103
|
/**
|
|
83
104
|
* Template multi-locale : utilise `ctx.locale` pour choisir la variant.
|
|
@@ -89,7 +110,7 @@ export declare function localizedTemplate(variants: {
|
|
|
89
110
|
text?: string | ((ctx: NotificationContext) => string);
|
|
90
111
|
html?: string | ((ctx: NotificationContext) => string);
|
|
91
112
|
};
|
|
92
|
-
}, resolveTo: (ctx: NotificationContext) => string | null, resolveMessageId?: (ctx: NotificationContext) => string): NotificationTemplate;
|
|
113
|
+
}, resolveTo: (ctx: NotificationContext) => string | null, resolveMessageId?: (ctx: NotificationContext) => string, resolveAttachments?: (ctx: NotificationContext) => MailAttachment[] | undefined): NotificationTemplate;
|
|
93
114
|
export declare const moduleInfo: {
|
|
94
115
|
name: string;
|
|
95
116
|
version: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -6,51 +6,110 @@
|
|
|
6
6
|
// et n'importe quel `context: Record<string, unknown>`. Les adapters spécifiques
|
|
7
7
|
// (booking-notifications, media-sfu-notifications) traduisent leurs events
|
|
8
8
|
// internes en `kind + context` et appellent `engine.notify(...)`.
|
|
9
|
-
|
|
9
|
+
import { createInMemoryThrottleStore, } from './throttle.js';
|
|
10
|
+
export { createInMemoryThrottleStore } from './throttle.js';
|
|
11
|
+
export { createMultiChannelSender, smsMailAdapter } from './multi-channel.js'; // dispatch multi-canal (email + sms)
|
|
10
12
|
export function createNotificationEngine(opts) {
|
|
11
|
-
const
|
|
13
|
+
const entries = new Map(Object.entries(opts.templates ?? {}).map(([k, t]) => [k, { template: t }]));
|
|
12
14
|
const disabled = new Set(opts.disabled ?? []);
|
|
13
15
|
const defaultLocale = opts.defaultLocale ?? 'fr';
|
|
14
|
-
|
|
16
|
+
const throttleStore = opts.throttleStore ?? createInMemoryThrottleStore();
|
|
17
|
+
const now = opts.now ?? (() => Date.now());
|
|
18
|
+
function buildContext(context) {
|
|
19
|
+
return { ...context, locale: context.locale ?? defaultLocale };
|
|
20
|
+
}
|
|
21
|
+
async function emitSkipped(kind, ctx, reason) {
|
|
22
|
+
await Promise.resolve(opts.onSkipped?.(kind, ctx, reason));
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
async function renderOnly(kind, context) {
|
|
26
|
+
const ctx = buildContext(context);
|
|
15
27
|
if (disabled.has(kind))
|
|
16
|
-
return null;
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
19
|
-
return null;
|
|
20
|
-
const ctx = { ...context, locale: context.locale ?? defaultLocale };
|
|
28
|
+
return { rendered: null, skipReason: 'disabled', ctx };
|
|
29
|
+
const entry = entries.get(kind);
|
|
30
|
+
if (!entry)
|
|
31
|
+
return { rendered: null, skipReason: 'unknown-kind', ctx };
|
|
21
32
|
let rendered;
|
|
22
33
|
try {
|
|
23
|
-
rendered = await Promise.resolve(
|
|
34
|
+
rendered = await Promise.resolve(entry.template.render(ctx));
|
|
24
35
|
}
|
|
25
36
|
catch (e) {
|
|
26
37
|
await Promise.resolve(opts.onError?.(kind, ctx, e));
|
|
27
|
-
return null;
|
|
38
|
+
return { rendered: null, skipReason: 'render-error', ctx };
|
|
28
39
|
}
|
|
29
40
|
if (!rendered)
|
|
30
|
-
return null;
|
|
31
|
-
|
|
41
|
+
return { rendered: null, skipReason: 'template-null', ctx };
|
|
42
|
+
const to = rendered.to;
|
|
43
|
+
const hasTo = Array.isArray(to) ? to.length > 0 : !!to;
|
|
44
|
+
if (!hasTo) {
|
|
32
45
|
await Promise.resolve(opts.onError?.(kind, ctx, new Error('Template returned no `to` address')));
|
|
46
|
+
return { rendered: null, skipReason: 'no-recipient', ctx };
|
|
47
|
+
}
|
|
48
|
+
return { rendered, ctx };
|
|
49
|
+
}
|
|
50
|
+
function throttleKey(kind, entry, ctx) {
|
|
51
|
+
const t = entry.options?.throttle;
|
|
52
|
+
if (!t)
|
|
33
53
|
return null;
|
|
54
|
+
const secondary = t.keyFrom ? t.keyFrom(ctx) : '';
|
|
55
|
+
return `${kind}::${secondary}`;
|
|
56
|
+
}
|
|
57
|
+
async function doNotify(kind, context) {
|
|
58
|
+
const { rendered, skipReason, ctx } = await renderOnly(kind, context);
|
|
59
|
+
if (skipReason && (skipReason === 'disabled' || skipReason === 'unknown-kind' || skipReason === 'template-null')) {
|
|
60
|
+
return emitSkipped(kind, ctx, skipReason);
|
|
61
|
+
}
|
|
62
|
+
if (!rendered) {
|
|
63
|
+
// render-error / no-recipient → onError déjà appelé, on signale aussi skip
|
|
64
|
+
if (skipReason)
|
|
65
|
+
return emitSkipped(kind, ctx, skipReason);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
// Throttle (après render OK, avant dispatch — la décision tient au déclenchement
|
|
69
|
+
// applicatif, pas au coût de rendu ; cohérent avec l'idempotence par messageId).
|
|
70
|
+
const entry = entries.get(kind);
|
|
71
|
+
const tKey = throttleKey(kind, entry, ctx);
|
|
72
|
+
if (tKey && entry.options?.throttle) {
|
|
73
|
+
const last = await Promise.resolve(throttleStore.getLastSent(tKey));
|
|
74
|
+
if (last !== undefined && now() - last < entry.options.throttle.windowMs) {
|
|
75
|
+
return emitSkipped(kind, ctx, 'throttled');
|
|
76
|
+
}
|
|
34
77
|
}
|
|
35
78
|
const mail = {
|
|
36
79
|
...rendered,
|
|
37
80
|
from: rendered.from ?? opts.defaultFrom,
|
|
38
81
|
metadata: { ...(rendered.metadata ?? {}), kind, locale: ctx.locale },
|
|
39
82
|
};
|
|
83
|
+
let result;
|
|
40
84
|
try {
|
|
41
|
-
|
|
42
|
-
await Promise.resolve(opts.onSent?.(kind, ctx, result));
|
|
43
|
-
return result;
|
|
85
|
+
result = await opts.mailer.send(mail);
|
|
44
86
|
}
|
|
45
87
|
catch (e) {
|
|
46
88
|
await Promise.resolve(opts.onError?.(kind, ctx, e));
|
|
47
|
-
return
|
|
89
|
+
return emitSkipped(kind, ctx, 'send-error');
|
|
48
90
|
}
|
|
91
|
+
if (tKey) {
|
|
92
|
+
await Promise.resolve(throttleStore.recordSent(tKey, now()));
|
|
93
|
+
}
|
|
94
|
+
await Promise.resolve(opts.onSent?.(kind, ctx, result));
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
async function doPreview(kind, context) {
|
|
98
|
+
const { rendered, ctx } = await renderOnly(kind, context);
|
|
99
|
+
if (!rendered)
|
|
100
|
+
return null;
|
|
101
|
+
// Préserve la fusion `from`/`metadata` que verrait le mailer — utile pour
|
|
102
|
+
// valider visuellement la pièce jointe ou le sujet final.
|
|
103
|
+
return {
|
|
104
|
+
...rendered,
|
|
105
|
+
from: rendered.from ?? opts.defaultFrom,
|
|
106
|
+
metadata: { ...(rendered.metadata ?? {}), kind, locale: ctx.locale },
|
|
107
|
+
};
|
|
49
108
|
}
|
|
50
109
|
return {
|
|
51
|
-
register(kind, template) {
|
|
52
|
-
unregister(kind) {
|
|
53
|
-
registeredKinds() { return Array.from(
|
|
110
|
+
register(kind, template, options) { entries.set(kind, { template, options }); },
|
|
111
|
+
unregister(kind) { entries.delete(kind); },
|
|
112
|
+
registeredKinds() { return Array.from(entries.keys()); },
|
|
54
113
|
setEnabled(kind, enabled) {
|
|
55
114
|
if (enabled)
|
|
56
115
|
disabled.delete(kind);
|
|
@@ -61,6 +120,7 @@ export function createNotificationEngine(opts) {
|
|
|
61
120
|
async notifyBatch(items) {
|
|
62
121
|
return Promise.all(items.map(it => doNotify(it.kind, it.context)));
|
|
63
122
|
},
|
|
123
|
+
preview: doPreview,
|
|
64
124
|
};
|
|
65
125
|
}
|
|
66
126
|
// ─── Built-in template helpers ─────────────────────────────────────────
|
|
@@ -81,6 +141,7 @@ export function simpleTemplate(spec) {
|
|
|
81
141
|
text: spec.text?.(ctx),
|
|
82
142
|
html: spec.html?.(ctx),
|
|
83
143
|
messageId: spec.messageId?.(ctx),
|
|
144
|
+
attachments: spec.attachments?.(ctx),
|
|
84
145
|
};
|
|
85
146
|
},
|
|
86
147
|
};
|
|
@@ -89,7 +150,7 @@ export function simpleTemplate(spec) {
|
|
|
89
150
|
* Template multi-locale : utilise `ctx.locale` pour choisir la variant.
|
|
90
151
|
* Si la locale n'est pas dans les variants, fallback sur 'fr' puis 'en'.
|
|
91
152
|
*/
|
|
92
|
-
export function localizedTemplate(variants, resolveTo, resolveMessageId) {
|
|
153
|
+
export function localizedTemplate(variants, resolveTo, resolveMessageId, resolveAttachments) {
|
|
93
154
|
return {
|
|
94
155
|
render(ctx) {
|
|
95
156
|
const to = resolveTo(ctx);
|
|
@@ -106,13 +167,14 @@ export function localizedTemplate(variants, resolveTo, resolveMessageId) {
|
|
|
106
167
|
text: resolve(v.text),
|
|
107
168
|
html: resolve(v.html),
|
|
108
169
|
messageId: resolveMessageId?.(ctx),
|
|
170
|
+
attachments: resolveAttachments?.(ctx),
|
|
109
171
|
};
|
|
110
172
|
},
|
|
111
173
|
};
|
|
112
174
|
}
|
|
113
175
|
export const moduleInfo = {
|
|
114
176
|
name: '@mostajs/notifications',
|
|
115
|
-
version: '0.
|
|
116
|
-
scope: 'Generic notification engine — domain-agnostic templating + mailer dispatch',
|
|
177
|
+
version: '0.2.0',
|
|
178
|
+
scope: 'Generic notification engine — domain-agnostic templating + mailer dispatch + throttle + preview',
|
|
117
179
|
};
|
|
118
180
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,0CAA0C;AAC1C,EAAE;AACF,oFAAoF;AACpF,qFAAqF;AACrF,iFAAiF;AACjF,2EAA2E;AAC3E,kEAAkE;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/notifications — sender MULTI-CANAL (email + sms + …). Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
* AGPL-3.0-or-later.
|
|
4
|
+
*
|
|
5
|
+
* Le moteur appelle `opts.mailer.send(mail)`. En lui passant CE sender, la notification est routée selon
|
|
6
|
+
* `mail.channel` (posé par le template/contexte) vers le bon canal — email par défaut, 'sms' via @mostajs/sms-lite.
|
|
7
|
+
* notifications reste découplé : on COMPOSE des senders `{ send(mail) -> { messageId } }`, on ne réimplémente rien.
|
|
8
|
+
*
|
|
9
|
+
* Usage :
|
|
10
|
+
* const sender = createMultiChannelSender({
|
|
11
|
+
* email: mailer, // @mostajs/mailer
|
|
12
|
+
* sms: smsMailAdapter(smsTransport), // adapte @mostajs/sms-lite (cf. ci-dessous)
|
|
13
|
+
* });
|
|
14
|
+
* const engine = createNotificationEngine({ mailer: sender });
|
|
15
|
+
* // un template qui rend { to: ctx.phone, text: "...", channel: "sms" } part par SMS.
|
|
16
|
+
*/
|
|
17
|
+
export interface ChannelSender {
|
|
18
|
+
send(mail: any): Promise<{
|
|
19
|
+
messageId?: string;
|
|
20
|
+
} | {
|
|
21
|
+
id?: string;
|
|
22
|
+
} | any>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createMultiChannelSender(channels: Record<string, ChannelSender>, opts?: {
|
|
25
|
+
defaultChannel?: string;
|
|
26
|
+
channelOf?: (mail: any) => string | undefined;
|
|
27
|
+
}): ChannelSender;
|
|
28
|
+
/** Adapte un transport @mostajs/sms-lite ({ send({to,text}) }) en sender de notifications ({ send(mail) -> {messageId} }). */
|
|
29
|
+
export declare function smsMailAdapter(smsTransport: {
|
|
30
|
+
send(m: {
|
|
31
|
+
to: any;
|
|
32
|
+
text?: string;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
ok: boolean;
|
|
35
|
+
id?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}>;
|
|
38
|
+
}): ChannelSender;
|
|
39
|
+
//# sourceMappingURL=multi-channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-channel.d.ts","sourceRoot":"","sources":["../src/multi-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,aAAa;IAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,GAAG,CAAC,CAAC;CAAE;AAE5G,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EACvC,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAA;CAAO,GACpF,aAAa,CAYf;AAED,8HAA8H;AAC9H,wBAAgB,cAAc,CAAC,YAAY,EAAE;IAAE,IAAI,CAAC,CAAC,EAAE;QAAE,EAAE,EAAE,GAAG,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,GAAG,aAAa,CAS1J"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function createMultiChannelSender(channels, opts = {}) {
|
|
2
|
+
if (!channels || typeof channels !== "object")
|
|
3
|
+
throw new Error("createMultiChannelSender: channels requis");
|
|
4
|
+
const defaultChannel = opts.defaultChannel || "email";
|
|
5
|
+
return {
|
|
6
|
+
async send(mail) {
|
|
7
|
+
const key = (opts.channelOf ? opts.channelOf(mail) : mail?.channel) || defaultChannel;
|
|
8
|
+
const sender = channels[key];
|
|
9
|
+
if (!sender?.send)
|
|
10
|
+
throw new Error(`createMultiChannelSender: canal inconnu « ${key} »`);
|
|
11
|
+
const r = await sender.send(mail);
|
|
12
|
+
return { messageId: r?.messageId ?? r?.id ?? null };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Adapte un transport @mostajs/sms-lite ({ send({to,text}) }) en sender de notifications ({ send(mail) -> {messageId} }). */
|
|
17
|
+
export function smsMailAdapter(smsTransport) {
|
|
18
|
+
if (!smsTransport?.send)
|
|
19
|
+
throw new Error("smsMailAdapter: transport SMS requis");
|
|
20
|
+
return {
|
|
21
|
+
async send(mail) {
|
|
22
|
+
const r = await smsTransport.send({ to: mail?.to, text: mail?.text ?? mail?.subject ?? "" });
|
|
23
|
+
if (!r?.ok)
|
|
24
|
+
throw new Error(r?.error || "échec d'envoi SMS");
|
|
25
|
+
return { messageId: r.id ?? null };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=multi-channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-channel.js","sourceRoot":"","sources":["../src/multi-channel.ts"],"names":[],"mappings":"AAkBA,MAAM,UAAU,wBAAwB,CACtC,QAAuC,EACvC,OAAmF,EAAE;IAErF,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC5G,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,OAAO,CAAC;IACtD,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,IAAS;YAClB,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,cAAc,CAAC;YACtF,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,MAAM,EAAE,IAAI;gBAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,GAAG,IAAI,CAAC,CAAC;YACzF,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClC,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,IAAI,CAAC,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,8HAA8H;AAC9H,MAAM,UAAU,cAAc,CAAC,YAA4G;IACzI,IAAI,CAAC,YAAY,EAAE,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACjF,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,IAAS;YAClB,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;YAC7F,IAAI,CAAC,CAAC,EAAE,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,CAAC,EAAE,KAAK,IAAI,mBAAmB,CAAC,CAAC;YAC7D,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;QACrC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Magasin de timestamps pour le throttle. In-memory par défaut. */
|
|
2
|
+
export interface ThrottleStore {
|
|
3
|
+
/** Timestamp (ms epoch) du dernier envoi pour cette clé, ou undefined. */
|
|
4
|
+
getLastSent(key: string): Promise<number | undefined> | number | undefined;
|
|
5
|
+
/** Enregistre un envoi. */
|
|
6
|
+
recordSent(key: string, at: number): Promise<void> | void;
|
|
7
|
+
/** Optionnel : purge des entrées plus anciennes que `olderThan` (ms epoch). */
|
|
8
|
+
prune?(olderThan: number): Promise<void> | void;
|
|
9
|
+
}
|
|
10
|
+
/** Map<key, lastSentAt> en mémoire — par défaut. Pas de persistance, pas de
|
|
11
|
+
* partage inter-process : adapté POC et single-instance. */
|
|
12
|
+
export declare function createInMemoryThrottleStore(): ThrottleStore;
|
|
13
|
+
//# sourceMappingURL=throttle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"throttle.d.ts","sourceRoot":"","sources":["../src/throttle.ts"],"names":[],"mappings":"AAQA,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAAA;IAC1E,2BAA2B;IAC3B,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzD,+EAA+E;IAC/E,KAAK,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAChD;AAED;6DAC6D;AAC7D,wBAAgB,2BAA2B,IAAI,aAAa,CAS3D"}
|
package/dist/throttle.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @mostajs/notifications — Throttle store abstraction (N2 / N7-ready)
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
//
|
|
4
|
+
// Le throttle empêche un kind de spammer quand son trigger se répète (alerte
|
|
5
|
+
// franchissant un seuil ré-évalué chaque heure). L'interface est injectable
|
|
6
|
+
// pour rester transparente lors du passage cross-instance (Mongo/Redis) en
|
|
7
|
+
// v0.4 — voir GAP-N7 de AUDIT-SPRINT5-FRAUDE-REPORTING-22052026.md.
|
|
8
|
+
/** Map<key, lastSentAt> en mémoire — par défaut. Pas de persistance, pas de
|
|
9
|
+
* partage inter-process : adapté POC et single-instance. */
|
|
10
|
+
export function createInMemoryThrottleStore() {
|
|
11
|
+
const map = new Map();
|
|
12
|
+
return {
|
|
13
|
+
getLastSent(key) { return map.get(key); },
|
|
14
|
+
recordSent(key, at) { map.set(key, at); },
|
|
15
|
+
prune(olderThan) {
|
|
16
|
+
for (const [k, v] of map)
|
|
17
|
+
if (v < olderThan)
|
|
18
|
+
map.delete(k);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=throttle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"throttle.js","sourceRoot":"","sources":["../src/throttle.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,0CAA0C;AAC1C,EAAE;AACF,6EAA6E;AAC7E,4EAA4E;AAC5E,2EAA2E;AAC3E,oEAAoE;AAYpE;6DAC6D;AAC7D,MAAM,UAAU,2BAA2B;IACzC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAA;IACrC,OAAO;QACL,WAAW,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA,CAAC,CAAC;QACxC,UAAU,CAAC,GAAG,EAAE,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA,CAAC,CAAC;QACxC,KAAK,CAAC,SAAS;YACb,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG;gBAAE,IAAI,CAAC,GAAG,SAAS;oBAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC5D,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @mostajs/notifications — fiche LLM
|
|
2
|
+
> Moteur de notifications générique : templating + i18n + dispatch MULTI-CANAL (email + SMS + …) + idempotence + throttle + preview, agnostique du domaine.
|
|
3
|
+
|
|
4
|
+
- Version: 0.3.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Chemin: mostajs/mosta-notifications · Statut audit: complet (dist/) · Tests: 23/23 + 3 (multi-canal)
|
|
6
|
+
- MULTI-CANAL (0.3.0) : `createMultiChannelSender(channels,{defaultChannel='email',channelOf?})` route par `mail.channel` ; `smsMailAdapter(smsTransport)` adapte @mostajs/sms-lite. Passé comme `mailer` au moteur → notify email OU sms. Rétro-compat (mailer seul inchangé).
|
|
7
|
+
|
|
8
|
+
## RÔLE
|
|
9
|
+
Découple le dispatch des notifications (registry de templates, i18n, idempotence, callbacks, throttle, preview) du domaine métier (booking, media-sfu, transit-apc, apps custom). Tout domaine envoie une notification via `engine.notify(kind, context)` ; les adapters spécifiques (`@mostajs/booking-notifications`, etc.) sont de fines glues qui mappent leurs events internes en kinds génériques. S'appuie sur `@mostajs/mailer` pour l'envoi effectif.
|
|
10
|
+
|
|
11
|
+
**Frontière de responsabilités** : *quand* et *comment* déclencher (kind, throttle, locale, preview) = ce module. *Quoi* envoyer (champs du mail, pièces jointes, headers, replyTo) = `@mostajs/mailer`. `RenderedMail` est un **ré-export** du type `Mail` de mailer — pas de duplication.
|
|
12
|
+
|
|
13
|
+
## INSTALLATION
|
|
14
|
+
npm i @mostajs/notifications @mostajs/mailer
|
|
15
|
+
|
|
16
|
+
## EXPORTS
|
|
17
|
+
- Factories: `createNotificationEngine`, `simpleTemplate`, `localizedTemplate`, `createInMemoryThrottleStore`
|
|
18
|
+
- Constante: `moduleInfo` (`{ name, version, scope }`)
|
|
19
|
+
- Types: `Locale`, `NotificationContext`, `RenderedMail` (= `Mail` de `@mostajs/mailer`), `MailAttachment` (ré-export), `NotificationTemplate`, `NotificationEngineOptions`, `NotificationEngine`, `RegisterOptions`, `ThrottleOptions`, `ThrottleStore`, `SkipReason`
|
|
20
|
+
|
|
21
|
+
## API — SIGNATURES
|
|
22
|
+
- `createNotificationEngine(opts: NotificationEngineOptions): NotificationEngine`
|
|
23
|
+
- `simpleTemplate(spec: { to; subject; text?; html?; messageId?; attachments? }): NotificationTemplate`
|
|
24
|
+
- `localizedTemplate(variants, resolveTo, resolveMessageId?, resolveAttachments?): NotificationTemplate`
|
|
25
|
+
- `createInMemoryThrottleStore(): ThrottleStore`
|
|
26
|
+
|
|
27
|
+
Interface `NotificationEngine` :
|
|
28
|
+
- `register(kind, template, options?: { throttle?: ThrottleOptions }): void`
|
|
29
|
+
- `unregister(kind): void`
|
|
30
|
+
- `registeredKinds(): string[]`
|
|
31
|
+
- `setEnabled(kind, enabled): void`
|
|
32
|
+
- `notify(kind, context): Promise<{messageId}|null>` — null = skipped
|
|
33
|
+
- `notifyBatch(items): Promise<Array<{messageId}|null>>`
|
|
34
|
+
- `preview(kind, context): Promise<RenderedMail|null>` — render-only, **n'envoie pas**, **ne consomme pas la fenêtre de throttle** *(v0.2 / N3)*
|
|
35
|
+
|
|
36
|
+
## TYPES CLÉS
|
|
37
|
+
- `Locale = string`
|
|
38
|
+
- `NotificationContext { locale?: Locale; [key: string]: unknown }`
|
|
39
|
+
- `RenderedMail = Mail` (de `@mostajs/mailer`) — fournit `to/cc/bcc/from/replyTo/subject/text/html/attachments/headers/messageId/metadata`
|
|
40
|
+
- `NotificationTemplate { render(context): Promise<RenderedMail|null>|RenderedMail|null }`
|
|
41
|
+
- `ThrottleOptions { windowMs: number; keyFrom?: (ctx)=>string }` *(v0.2 / N2)*
|
|
42
|
+
- `ThrottleStore { getLastSent(key); recordSent(key, at); prune?(olderThan) }` — injectable (in-memory par défaut ; Mongo/Redis v0.4)
|
|
43
|
+
- `SkipReason = 'disabled'|'unknown-kind'|'template-null'|'no-recipient'|'render-error'|'send-error'|'throttled'`
|
|
44
|
+
- `NotificationEngineOptions { mailer: {send}; templates?; defaultLocale?; defaultFrom?; disabled?; throttleStore?; now?; onSent?; onError?; onSkipped? }`
|
|
45
|
+
|
|
46
|
+
## PATTERN
|
|
47
|
+
```ts
|
|
48
|
+
import { createNotificationEngine, simpleTemplate } from '@mostajs/notifications'
|
|
49
|
+
import { createMailer, createSmtpDriver } from '@mostajs/mailer'
|
|
50
|
+
|
|
51
|
+
const mailer = createMailer({
|
|
52
|
+
drivers: { smtp: createSmtpDriver({ host, port: 587, user, pass }) },
|
|
53
|
+
defaultDriver: 'smtp',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const engine = createNotificationEngine({
|
|
57
|
+
mailer, defaultFrom: 'noreply@example.com', defaultLocale: 'fr',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Alerte avec anti-spam (throttle 6 h par ligne)
|
|
61
|
+
engine.register('fraud.threshold.exceeded', simpleTemplate({
|
|
62
|
+
to: ctx => `ops@${ctx.accountId}.tld`,
|
|
63
|
+
subject: ctx => `[Fraude] Ligne ${ctx.lineId} : ${ctx.rate}%`,
|
|
64
|
+
html: ctx => `…`,
|
|
65
|
+
}), { throttle: { windowMs: 6 * 3600_000, keyFrom: ctx => String(ctx.lineId) } })
|
|
66
|
+
|
|
67
|
+
// Rapport mensuel avec PDF en pièce jointe (les attachments transitent telles
|
|
68
|
+
// quelles vers @mostajs/mailer — pas de duplication de type côté notifications)
|
|
69
|
+
engine.register('report.monthly', simpleTemplate({
|
|
70
|
+
to: ctx => String(ctx.opsEmail),
|
|
71
|
+
subject: ctx => `Rapport mensuel — ${ctx.month}`,
|
|
72
|
+
text: () => 'Cf. PJ.',
|
|
73
|
+
attachments: ctx => [{ filename: `rapport-${ctx.month}.pdf`, content: ctx.pdf as Buffer }],
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
// Preview avant déclenchement réel (ergo opérateur)
|
|
77
|
+
const preview = await engine.preview('report.monthly', { month: '2026-04', pdf, opsEmail })
|
|
78
|
+
// preview === RenderedMail (rendu fusionné) ; rien envoyé.
|
|
79
|
+
|
|
80
|
+
await engine.notify('report.monthly', { month: '2026-04', pdf, opsEmail })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## DÉPEND DE
|
|
84
|
+
- `@mostajs/mailer` (peer dep `^0.1.1`) — fournit les types `Mail`/`MailAttachment` ET le dispatch. `RenderedMail` est l'alias de `Mail` côté notifications.
|
|
85
|
+
|
|
86
|
+
## CHANGEMENTS v0.2.0
|
|
87
|
+
- **N1** : `RenderedMail` est désormais le `Mail` de `@mostajs/mailer` (pas de duplication). Les pièces jointes sont ainsi disponibles sans qu'aucun nouveau type ne soit défini ici.
|
|
88
|
+
- **N2** : throttle par kind via `register(kind, tpl, { throttle: { windowMs, keyFrom? } })`. Store par défaut in-memory ; `ThrottleStore` injectable pour cross-instance (Mongo/Redis en v0.4).
|
|
89
|
+
- **N3** : `engine.preview(kind, context)` — render-only, sans dispatch, sans consommer le throttle, sans `onSent`. Utilisable pour UI ops.
|
|
90
|
+
- Ajout : callback `onSkipped(kind, ctx, reason: SkipReason)`, option `now?: () => number` (testabilité).
|
|
91
|
+
- Rétro-compat : aucun appel signé v0.1.0 ne casse (les 23 tests incluent une suite `test-backward-compat`).
|
|
92
|
+
|
|
93
|
+
## PIÈGES
|
|
94
|
+
- L'option `mailer` n'exige qu'un objet `{ send }` — pas forcément `@mostajs/mailer`, n'importe quel adapter compatible.
|
|
95
|
+
- `notify` retourne `null` (sans erreur) avec `SkipReason` typée envoyée à `onSkipped`.
|
|
96
|
+
- Throttle in-memory : ne survit pas au redémarrage ni au multi-process. Pour MVP production → injecter un `ThrottleStore` persisté (gap N7, v0.4).
|
|
97
|
+
- `preview` applique `defaultLocale` et fusionne `from`/`metadata` comme `notify` — pour ne pas tromper l'utilisateur sur le rendu final.
|
|
98
|
+
- Pièces jointes = type `MailAttachment` de **mailer**, pas redéfini ici. Si l'app a besoin d'un champ absent de `Mail`, c'est mailer qu'il faut étendre.
|
|
99
|
+
- L'idempotence par `messageId` reste déléguée à `@mostajs/mailer` (MailLog).
|
|
100
|
+
|
|
101
|
+
## RÉFÉRENCES
|
|
102
|
+
README.md · docs/STATE-OF-THE-ART.md · /home/hmd/dev/MostaGare-Install/SolutionCh/Tramway/AUDIT-SPRINT5-FRAUDE-REPORTING-22052026.md §6
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/notifications",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Generic notification engine for @mostajs/* — domain-agnostic templating + i18n +
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Generic notification engine for @mostajs/* — domain-agnostic templating + i18n + MULTI-CHANNEL dispatch (email + SMS + …) + idempotence + throttling + preview. Consume any event kind from any source. Plug-and-play with @mostajs/mailer (email) and @mostajs/sms-lite (SMS).",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
7
7
|
"type": "module",
|
|
@@ -17,12 +17,9 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
19
|
"README.md",
|
|
20
|
-
"LICENSE"
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"llms.txt"
|
|
21
22
|
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "tsc",
|
|
24
|
-
"prepublishOnly": "npm run build"
|
|
25
|
-
},
|
|
26
23
|
"peerDependencies": {
|
|
27
24
|
"@mostajs/mailer": "^0.1.1"
|
|
28
25
|
},
|
|
@@ -30,6 +27,11 @@
|
|
|
30
27
|
"node": ">=18"
|
|
31
28
|
},
|
|
32
29
|
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.11.0",
|
|
31
|
+
"tsx": "^4.7.0",
|
|
33
32
|
"typescript": "^6.0.3"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc"
|
|
34
36
|
}
|
|
35
|
-
}
|
|
37
|
+
}
|