@forinda/kickjs-notifications 1.1.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/LICENSE +21 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Felix Orinda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { AppAdapter, Container } from '@forinda/kickjs-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abstract notification channel. Implement this to send notifications
|
|
5
|
+
* via any medium: email, Slack, Discord, webhook, SMS, push, etc.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* class SlackChannel implements NotificationChannel {
|
|
10
|
+
* name = 'slack'
|
|
11
|
+
* async send(notification) {
|
|
12
|
+
* await fetch(this.webhookUrl, {
|
|
13
|
+
* method: 'POST',
|
|
14
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
* body: JSON.stringify({ text: notification.message }),
|
|
16
|
+
* })
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
interface NotificationChannel {
|
|
22
|
+
/** Channel name for routing */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Send a notification through this channel */
|
|
25
|
+
send(notification: Notification): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
interface Notification {
|
|
28
|
+
/** Recipient identifier (email, user ID, channel name, etc.) */
|
|
29
|
+
to: string | string[];
|
|
30
|
+
/** Notification subject/title */
|
|
31
|
+
subject: string;
|
|
32
|
+
/** Plain text message */
|
|
33
|
+
message: string;
|
|
34
|
+
/** HTML message (for email channels) */
|
|
35
|
+
html?: string;
|
|
36
|
+
/** Which channels to send through (defaults to all) */
|
|
37
|
+
channels?: string[];
|
|
38
|
+
/** Additional data for channel-specific formatting */
|
|
39
|
+
data?: Record<string, any>;
|
|
40
|
+
/** Priority level */
|
|
41
|
+
priority?: 'low' | 'normal' | 'high' | 'urgent';
|
|
42
|
+
}
|
|
43
|
+
interface NotificationResult {
|
|
44
|
+
channel: string;
|
|
45
|
+
success: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
interface NotificationServiceOptions {
|
|
49
|
+
/** Notification channels to register */
|
|
50
|
+
channels: NotificationChannel[];
|
|
51
|
+
/** Default channels to use when notification.channels is not specified */
|
|
52
|
+
defaultChannels?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** DI token for resolving NotificationService */
|
|
56
|
+
declare const NOTIFICATIONS: unique symbol;
|
|
57
|
+
/**
|
|
58
|
+
* Multi-channel notification service.
|
|
59
|
+
* Routes notifications to one or more channels (email, Slack, webhook, etc.).
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* @Service()
|
|
64
|
+
* class AlertService {
|
|
65
|
+
* constructor(@Inject(NOTIFICATIONS) private notify: NotificationService) {}
|
|
66
|
+
*
|
|
67
|
+
* async alertOps(message: string) {
|
|
68
|
+
* await this.notify.send({
|
|
69
|
+
* to: '#ops-alerts',
|
|
70
|
+
* subject: 'System Alert',
|
|
71
|
+
* message,
|
|
72
|
+
* channels: ['slack', 'email'],
|
|
73
|
+
* priority: 'high',
|
|
74
|
+
* })
|
|
75
|
+
* }
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare class NotificationService {
|
|
80
|
+
private channels;
|
|
81
|
+
private defaultChannels;
|
|
82
|
+
constructor(options: NotificationServiceOptions);
|
|
83
|
+
/** Send a notification through specified or default channels */
|
|
84
|
+
send(notification: Notification): Promise<NotificationResult[]>;
|
|
85
|
+
/** Send to a specific channel only */
|
|
86
|
+
sendTo(channelName: string, notification: Notification): Promise<NotificationResult>;
|
|
87
|
+
/** Get all registered channel names */
|
|
88
|
+
getChannelNames(): string[];
|
|
89
|
+
/** Add a channel at runtime */
|
|
90
|
+
addChannel(channel: NotificationChannel): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Notification adapter — registers NotificationService in DI.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { NotificationAdapter, SlackChannel, EmailChannel } from '@forinda/kickjs-notifications'
|
|
99
|
+
*
|
|
100
|
+
* bootstrap({
|
|
101
|
+
* adapters: [
|
|
102
|
+
* new NotificationAdapter({
|
|
103
|
+
* channels: [
|
|
104
|
+
* new SlackChannel({ url: process.env.SLACK_WEBHOOK! }),
|
|
105
|
+
* new EmailChannel({ mailer }),
|
|
106
|
+
* ],
|
|
107
|
+
* defaultChannels: ['slack'],
|
|
108
|
+
* }),
|
|
109
|
+
* ],
|
|
110
|
+
* })
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
declare class NotificationAdapter implements AppAdapter {
|
|
114
|
+
name: string;
|
|
115
|
+
private service;
|
|
116
|
+
constructor(options: NotificationServiceOptions);
|
|
117
|
+
afterStart(_server: any, container: Container): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface WebhookChannelOptions {
|
|
121
|
+
/** Webhook URL to POST to */
|
|
122
|
+
url: string;
|
|
123
|
+
/** Custom headers (e.g. auth tokens) */
|
|
124
|
+
headers?: Record<string, string>;
|
|
125
|
+
/**
|
|
126
|
+
* Transform notification to the request body.
|
|
127
|
+
* Default: sends `{ subject, message, to, priority, data }`
|
|
128
|
+
*/
|
|
129
|
+
transform?: (notification: Notification) => any;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Webhook notification channel — POST notifications to any URL.
|
|
133
|
+
* Works with Slack incoming webhooks, Discord webhooks, custom APIs, etc.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* // Generic webhook
|
|
138
|
+
* new WebhookChannel({ url: 'https://hooks.example.com/notify' })
|
|
139
|
+
*
|
|
140
|
+
* // Slack incoming webhook
|
|
141
|
+
* new WebhookChannel({
|
|
142
|
+
* url: process.env.SLACK_WEBHOOK_URL!,
|
|
143
|
+
* transform: (n) => ({
|
|
144
|
+
* text: `*${n.subject}*\n${n.message}`,
|
|
145
|
+
* }),
|
|
146
|
+
* })
|
|
147
|
+
*
|
|
148
|
+
* // Discord webhook
|
|
149
|
+
* new WebhookChannel({
|
|
150
|
+
* url: process.env.DISCORD_WEBHOOK_URL!,
|
|
151
|
+
* transform: (n) => ({
|
|
152
|
+
* content: `**${n.subject}**\n${n.message}`,
|
|
153
|
+
* }),
|
|
154
|
+
* })
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
declare class WebhookChannel implements NotificationChannel {
|
|
158
|
+
name: string;
|
|
159
|
+
private options;
|
|
160
|
+
constructor(options: WebhookChannelOptions);
|
|
161
|
+
send(notification: Notification): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Slack-specific webhook channel with pre-configured transform.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* new SlackChannel({ url: process.env.SLACK_WEBHOOK_URL! })
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare class SlackChannel extends WebhookChannel {
|
|
172
|
+
constructor(options: {
|
|
173
|
+
url: string;
|
|
174
|
+
channel?: string;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Discord-specific webhook channel.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* new DiscordChannel({ url: process.env.DISCORD_WEBHOOK_URL! })
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
declare class DiscordChannel extends WebhookChannel {
|
|
186
|
+
constructor(options: {
|
|
187
|
+
url: string;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Email notification channel — bridges to @forinda/kickjs-mailer.
|
|
193
|
+
* Pass your MailerService instance to route notifications as emails.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* import { EmailChannel } from '@forinda/kickjs-notifications'
|
|
198
|
+
*
|
|
199
|
+
* // mailer is your MailerService instance
|
|
200
|
+
* new EmailChannel({ mailer, defaultFrom: 'alerts@myapp.com' })
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
declare class EmailChannel implements NotificationChannel {
|
|
204
|
+
name: string;
|
|
205
|
+
private mailer;
|
|
206
|
+
private defaultFrom?;
|
|
207
|
+
constructor(options: {
|
|
208
|
+
mailer: any;
|
|
209
|
+
defaultFrom?: string;
|
|
210
|
+
});
|
|
211
|
+
send(notification: Notification): Promise<void>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Console notification channel — logs notifications.
|
|
216
|
+
* Useful for development and testing.
|
|
217
|
+
*/
|
|
218
|
+
declare class ConsoleChannel implements NotificationChannel {
|
|
219
|
+
name: string;
|
|
220
|
+
send(notification: Notification): Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export { ConsoleChannel, DiscordChannel, EmailChannel, NOTIFICATIONS, type Notification, NotificationAdapter, type NotificationChannel, type NotificationResult, NotificationService, type NotificationServiceOptions, SlackChannel, WebhookChannel, type WebhookChannelOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import "reflect-metadata";
|
|
6
|
+
|
|
7
|
+
// src/service.ts
|
|
8
|
+
import { Logger } from "@forinda/kickjs-core";
|
|
9
|
+
var log = Logger.for("Notifications");
|
|
10
|
+
var NOTIFICATIONS = /* @__PURE__ */ Symbol("NotificationService");
|
|
11
|
+
var NotificationService = class {
|
|
12
|
+
static {
|
|
13
|
+
__name(this, "NotificationService");
|
|
14
|
+
}
|
|
15
|
+
channels = /* @__PURE__ */ new Map();
|
|
16
|
+
defaultChannels;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
for (const channel of options.channels) {
|
|
19
|
+
this.channels.set(channel.name, channel);
|
|
20
|
+
}
|
|
21
|
+
this.defaultChannels = options.defaultChannels ?? options.channels.map((c) => c.name);
|
|
22
|
+
}
|
|
23
|
+
/** Send a notification through specified or default channels */
|
|
24
|
+
async send(notification) {
|
|
25
|
+
const channelNames = notification.channels ?? this.defaultChannels;
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const name of channelNames) {
|
|
28
|
+
const channel = this.channels.get(name);
|
|
29
|
+
if (!channel) {
|
|
30
|
+
log.warn(`Channel "${name}" not found \u2014 skipping`);
|
|
31
|
+
results.push({
|
|
32
|
+
channel: name,
|
|
33
|
+
success: false,
|
|
34
|
+
error: "Channel not found"
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await channel.send(notification);
|
|
40
|
+
log.info(`Sent via ${name}: ${notification.subject}`);
|
|
41
|
+
results.push({
|
|
42
|
+
channel: name,
|
|
43
|
+
success: true
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
log.error({
|
|
47
|
+
err
|
|
48
|
+
}, `Failed via ${name}: ${notification.subject}`);
|
|
49
|
+
results.push({
|
|
50
|
+
channel: name,
|
|
51
|
+
success: false,
|
|
52
|
+
error: err.message
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
/** Send to a specific channel only */
|
|
59
|
+
async sendTo(channelName, notification) {
|
|
60
|
+
return (await this.send({
|
|
61
|
+
...notification,
|
|
62
|
+
channels: [
|
|
63
|
+
channelName
|
|
64
|
+
]
|
|
65
|
+
}))[0];
|
|
66
|
+
}
|
|
67
|
+
/** Get all registered channel names */
|
|
68
|
+
getChannelNames() {
|
|
69
|
+
return Array.from(this.channels.keys());
|
|
70
|
+
}
|
|
71
|
+
/** Add a channel at runtime */
|
|
72
|
+
addChannel(channel) {
|
|
73
|
+
this.channels.set(channel.name, channel);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/adapter.ts
|
|
78
|
+
import { Logger as Logger2 } from "@forinda/kickjs-core";
|
|
79
|
+
var log2 = Logger2.for("NotificationAdapter");
|
|
80
|
+
var NotificationAdapter = class {
|
|
81
|
+
static {
|
|
82
|
+
__name(this, "NotificationAdapter");
|
|
83
|
+
}
|
|
84
|
+
name = "NotificationAdapter";
|
|
85
|
+
service;
|
|
86
|
+
constructor(options) {
|
|
87
|
+
this.service = new NotificationService(options);
|
|
88
|
+
}
|
|
89
|
+
afterStart(_server, container) {
|
|
90
|
+
container.registerInstance(NOTIFICATIONS, this.service);
|
|
91
|
+
log2.info(`Channels: ${this.service.getChannelNames().join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/channels/webhook.channel.ts
|
|
96
|
+
var WebhookChannel = class {
|
|
97
|
+
static {
|
|
98
|
+
__name(this, "WebhookChannel");
|
|
99
|
+
}
|
|
100
|
+
name = "webhook";
|
|
101
|
+
options;
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.options = options;
|
|
104
|
+
}
|
|
105
|
+
async send(notification) {
|
|
106
|
+
const body = this.options.transform ? this.options.transform(notification) : {
|
|
107
|
+
subject: notification.subject,
|
|
108
|
+
message: notification.message,
|
|
109
|
+
to: notification.to,
|
|
110
|
+
priority: notification.priority,
|
|
111
|
+
data: notification.data
|
|
112
|
+
};
|
|
113
|
+
const response = await fetch(this.options.url, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
...this.options.headers
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify(body)
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
var SlackChannel = class extends WebhookChannel {
|
|
127
|
+
static {
|
|
128
|
+
__name(this, "SlackChannel");
|
|
129
|
+
}
|
|
130
|
+
constructor(options) {
|
|
131
|
+
super({
|
|
132
|
+
url: options.url,
|
|
133
|
+
transform: /* @__PURE__ */ __name((n) => ({
|
|
134
|
+
...options.channel ? {
|
|
135
|
+
channel: options.channel
|
|
136
|
+
} : {},
|
|
137
|
+
text: `*${n.subject}*
|
|
138
|
+
${n.message}`,
|
|
139
|
+
...n.priority === "urgent" || n.priority === "high" ? {
|
|
140
|
+
attachments: [
|
|
141
|
+
{
|
|
142
|
+
color: "#ff0000",
|
|
143
|
+
text: n.message
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
} : {}
|
|
147
|
+
}), "transform")
|
|
148
|
+
});
|
|
149
|
+
this.name = "slack";
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var DiscordChannel = class extends WebhookChannel {
|
|
153
|
+
static {
|
|
154
|
+
__name(this, "DiscordChannel");
|
|
155
|
+
}
|
|
156
|
+
constructor(options) {
|
|
157
|
+
super({
|
|
158
|
+
url: options.url,
|
|
159
|
+
transform: /* @__PURE__ */ __name((n) => ({
|
|
160
|
+
content: `**${n.subject}**
|
|
161
|
+
${n.message}`
|
|
162
|
+
}), "transform")
|
|
163
|
+
});
|
|
164
|
+
this.name = "discord";
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/channels/email.channel.ts
|
|
169
|
+
var EmailChannel = class {
|
|
170
|
+
static {
|
|
171
|
+
__name(this, "EmailChannel");
|
|
172
|
+
}
|
|
173
|
+
name = "email";
|
|
174
|
+
mailer;
|
|
175
|
+
defaultFrom;
|
|
176
|
+
constructor(options) {
|
|
177
|
+
this.mailer = options.mailer;
|
|
178
|
+
this.defaultFrom = options.defaultFrom;
|
|
179
|
+
}
|
|
180
|
+
async send(notification) {
|
|
181
|
+
const recipients = Array.isArray(notification.to) ? notification.to : [
|
|
182
|
+
notification.to
|
|
183
|
+
];
|
|
184
|
+
await this.mailer.send({
|
|
185
|
+
from: this.defaultFrom,
|
|
186
|
+
to: recipients,
|
|
187
|
+
subject: notification.subject,
|
|
188
|
+
text: notification.message,
|
|
189
|
+
html: notification.html
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/channels/console.channel.ts
|
|
195
|
+
import { Logger as Logger3 } from "@forinda/kickjs-core";
|
|
196
|
+
var log3 = Logger3.for("ConsoleNotification");
|
|
197
|
+
var ConsoleChannel = class {
|
|
198
|
+
static {
|
|
199
|
+
__name(this, "ConsoleChannel");
|
|
200
|
+
}
|
|
201
|
+
name = "console";
|
|
202
|
+
async send(notification) {
|
|
203
|
+
const to = Array.isArray(notification.to) ? notification.to.join(", ") : notification.to;
|
|
204
|
+
log3.info(`[${notification.priority ?? "normal"}] \u2192 ${to} | ${notification.subject}: ${notification.message}`);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
export {
|
|
208
|
+
ConsoleChannel,
|
|
209
|
+
DiscordChannel,
|
|
210
|
+
EmailChannel,
|
|
211
|
+
NOTIFICATIONS,
|
|
212
|
+
NotificationAdapter,
|
|
213
|
+
NotificationService,
|
|
214
|
+
SlackChannel,
|
|
215
|
+
WebhookChannel
|
|
216
|
+
};
|
|
217
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/service.ts","../src/adapter.ts","../src/channels/webhook.channel.ts","../src/channels/email.channel.ts","../src/channels/console.channel.ts"],"sourcesContent":["import 'reflect-metadata'\n\n// Types\nexport {\n type NotificationChannel,\n type Notification,\n type NotificationResult,\n type NotificationServiceOptions,\n} from './types'\n\n// Service\nexport { NotificationService, NOTIFICATIONS } from './service'\n\n// Adapter\nexport { NotificationAdapter } from './adapter'\n\n// Built-in channels\nexport {\n WebhookChannel,\n SlackChannel,\n DiscordChannel,\n EmailChannel,\n ConsoleChannel,\n type WebhookChannelOptions,\n} from './channels'\n","import { Logger } from '@forinda/kickjs-core'\nimport type {\n NotificationChannel,\n Notification,\n NotificationResult,\n NotificationServiceOptions,\n} from './types'\n\nconst log = Logger.for('Notifications')\n\n/** DI token for resolving NotificationService */\nexport const NOTIFICATIONS = Symbol('NotificationService')\n\n/**\n * Multi-channel notification service.\n * Routes notifications to one or more channels (email, Slack, webhook, etc.).\n *\n * @example\n * ```ts\n * @Service()\n * class AlertService {\n * constructor(@Inject(NOTIFICATIONS) private notify: NotificationService) {}\n *\n * async alertOps(message: string) {\n * await this.notify.send({\n * to: '#ops-alerts',\n * subject: 'System Alert',\n * message,\n * channels: ['slack', 'email'],\n * priority: 'high',\n * })\n * }\n * }\n * ```\n */\nexport class NotificationService {\n private channels = new Map<string, NotificationChannel>()\n private defaultChannels: string[]\n\n constructor(options: NotificationServiceOptions) {\n for (const channel of options.channels) {\n this.channels.set(channel.name, channel)\n }\n this.defaultChannels = options.defaultChannels ?? options.channels.map((c) => c.name)\n }\n\n /** Send a notification through specified or default channels */\n async send(notification: Notification): Promise<NotificationResult[]> {\n const channelNames = notification.channels ?? this.defaultChannels\n const results: NotificationResult[] = []\n\n for (const name of channelNames) {\n const channel = this.channels.get(name)\n if (!channel) {\n log.warn(`Channel \"${name}\" not found — skipping`)\n results.push({ channel: name, success: false, error: 'Channel not found' })\n continue\n }\n\n try {\n await channel.send(notification)\n log.info(`Sent via ${name}: ${notification.subject}`)\n results.push({ channel: name, success: true })\n } catch (err: any) {\n log.error({ err }, `Failed via ${name}: ${notification.subject}`)\n results.push({ channel: name, success: false, error: err.message })\n }\n }\n\n return results\n }\n\n /** Send to a specific channel only */\n async sendTo(channelName: string, notification: Notification): Promise<NotificationResult> {\n return (await this.send({ ...notification, channels: [channelName] }))[0]\n }\n\n /** Get all registered channel names */\n getChannelNames(): string[] {\n return Array.from(this.channels.keys())\n }\n\n /** Add a channel at runtime */\n addChannel(channel: NotificationChannel): void {\n this.channels.set(channel.name, channel)\n }\n}\n","import { Logger, type AppAdapter, type Container } from '@forinda/kickjs-core'\nimport { NotificationService, NOTIFICATIONS } from './service'\nimport type { NotificationServiceOptions } from './types'\n\nconst log = Logger.for('NotificationAdapter')\n\n/**\n * Notification adapter — registers NotificationService in DI.\n *\n * @example\n * ```ts\n * import { NotificationAdapter, SlackChannel, EmailChannel } from '@forinda/kickjs-notifications'\n *\n * bootstrap({\n * adapters: [\n * new NotificationAdapter({\n * channels: [\n * new SlackChannel({ url: process.env.SLACK_WEBHOOK! }),\n * new EmailChannel({ mailer }),\n * ],\n * defaultChannels: ['slack'],\n * }),\n * ],\n * })\n * ```\n */\nexport class NotificationAdapter implements AppAdapter {\n name = 'NotificationAdapter'\n private service: NotificationService\n\n constructor(options: NotificationServiceOptions) {\n this.service = new NotificationService(options)\n }\n\n afterStart(_server: any, container: Container): void {\n container.registerInstance(NOTIFICATIONS, this.service)\n log.info(`Channels: ${this.service.getChannelNames().join(', ')}`)\n }\n}\n","import type { NotificationChannel, Notification } from '../types'\n\nexport interface WebhookChannelOptions {\n /** Webhook URL to POST to */\n url: string\n /** Custom headers (e.g. auth tokens) */\n headers?: Record<string, string>\n /**\n * Transform notification to the request body.\n * Default: sends `{ subject, message, to, priority, data }`\n */\n transform?: (notification: Notification) => any\n}\n\n/**\n * Webhook notification channel — POST notifications to any URL.\n * Works with Slack incoming webhooks, Discord webhooks, custom APIs, etc.\n *\n * @example\n * ```ts\n * // Generic webhook\n * new WebhookChannel({ url: 'https://hooks.example.com/notify' })\n *\n * // Slack incoming webhook\n * new WebhookChannel({\n * url: process.env.SLACK_WEBHOOK_URL!,\n * transform: (n) => ({\n * text: `*${n.subject}*\\n${n.message}`,\n * }),\n * })\n *\n * // Discord webhook\n * new WebhookChannel({\n * url: process.env.DISCORD_WEBHOOK_URL!,\n * transform: (n) => ({\n * content: `**${n.subject}**\\n${n.message}`,\n * }),\n * })\n * ```\n */\nexport class WebhookChannel implements NotificationChannel {\n name = 'webhook'\n private options: WebhookChannelOptions\n\n constructor(options: WebhookChannelOptions) {\n this.options = options\n }\n\n async send(notification: Notification): Promise<void> {\n const body = this.options.transform\n ? this.options.transform(notification)\n : {\n subject: notification.subject,\n message: notification.message,\n to: notification.to,\n priority: notification.priority,\n data: notification.data,\n }\n\n const response = await fetch(this.options.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...this.options.headers,\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n throw new Error(`Webhook failed: ${response.status} ${response.statusText}`)\n }\n }\n}\n\n/**\n * Slack-specific webhook channel with pre-configured transform.\n *\n * @example\n * ```ts\n * new SlackChannel({ url: process.env.SLACK_WEBHOOK_URL! })\n * ```\n */\nexport class SlackChannel extends WebhookChannel {\n constructor(options: { url: string; channel?: string }) {\n super({\n url: options.url,\n transform: (n) => ({\n ...(options.channel ? { channel: options.channel } : {}),\n text: `*${n.subject}*\\n${n.message}`,\n ...(n.priority === 'urgent' || n.priority === 'high'\n ? { attachments: [{ color: '#ff0000', text: n.message }] }\n : {}),\n }),\n })\n this.name = 'slack'\n }\n}\n\n/**\n * Discord-specific webhook channel.\n *\n * @example\n * ```ts\n * new DiscordChannel({ url: process.env.DISCORD_WEBHOOK_URL! })\n * ```\n */\nexport class DiscordChannel extends WebhookChannel {\n constructor(options: { url: string }) {\n super({\n url: options.url,\n transform: (n) => ({\n content: `**${n.subject}**\\n${n.message}`,\n }),\n })\n this.name = 'discord'\n }\n}\n","import type { NotificationChannel, Notification } from '../types'\n\n/**\n * Email notification channel — bridges to @forinda/kickjs-mailer.\n * Pass your MailerService instance to route notifications as emails.\n *\n * @example\n * ```ts\n * import { EmailChannel } from '@forinda/kickjs-notifications'\n *\n * // mailer is your MailerService instance\n * new EmailChannel({ mailer, defaultFrom: 'alerts@myapp.com' })\n * ```\n */\nexport class EmailChannel implements NotificationChannel {\n name = 'email'\n private mailer: any\n private defaultFrom?: string\n\n constructor(options: { mailer: any; defaultFrom?: string }) {\n this.mailer = options.mailer\n this.defaultFrom = options.defaultFrom\n }\n\n async send(notification: Notification): Promise<void> {\n const recipients = Array.isArray(notification.to) ? notification.to : [notification.to]\n\n await this.mailer.send({\n from: this.defaultFrom,\n to: recipients,\n subject: notification.subject,\n text: notification.message,\n html: notification.html,\n })\n }\n}\n","import { Logger } from '@forinda/kickjs-core'\nimport type { NotificationChannel, Notification } from '../types'\n\nconst log = Logger.for('ConsoleNotification')\n\n/**\n * Console notification channel — logs notifications.\n * Useful for development and testing.\n */\nexport class ConsoleChannel implements NotificationChannel {\n name = 'console'\n\n async send(notification: Notification): Promise<void> {\n const to = Array.isArray(notification.to) ? notification.to.join(', ') : notification.to\n log.info(\n `[${notification.priority ?? 'normal'}] → ${to} | ${notification.subject}: ${notification.message}`,\n )\n }\n}\n"],"mappings":"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","names":["Logger","log","Logger","for","NOTIFICATIONS","Symbol","NotificationService","channels","Map","defaultChannels","options","channel","set","name","map","c","send","notification","channelNames","results","get","warn","push","success","error","info","subject","err","message","sendTo","channelName","getChannelNames","Array","from","keys","addChannel","Logger","log","Logger","for","NotificationAdapter","name","service","options","NotificationService","afterStart","_server","container","registerInstance","NOTIFICATIONS","info","getChannelNames","join","WebhookChannel","name","options","send","notification","body","transform","subject","message","to","priority","data","response","fetch","url","method","headers","JSON","stringify","ok","Error","status","statusText","SlackChannel","n","channel","text","attachments","color","DiscordChannel","content","EmailChannel","name","mailer","defaultFrom","options","send","notification","recipients","Array","isArray","to","from","subject","text","message","html","Logger","log","Logger","for","ConsoleChannel","name","send","notification","to","Array","isArray","join","info","priority","subject","message"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forinda/kickjs-notifications",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Multi-channel notifications for KickJS — email, Slack, Discord, webhook, custom",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kickjs",
|
|
7
|
+
"notifications",
|
|
8
|
+
"slack",
|
|
9
|
+
"discord",
|
|
10
|
+
"webhook",
|
|
11
|
+
"email",
|
|
12
|
+
"multi-channel"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"reflect-metadata": "^0.2.2",
|
|
28
|
+
"@forinda/kickjs-core": "1.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.5.2",
|
|
32
|
+
"tsup": "^8.5.0",
|
|
33
|
+
"typescript": "^5.9.2",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Felix Orinda",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.0"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://forinda.github.io/kick-js/",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/forinda/kick-js.git",
|
|
48
|
+
"directory": "packages/notifications"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/forinda/kick-js/issues"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup",
|
|
55
|
+
"dev": "tsup --watch",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"clean": "rm -rf dist .turbo"
|
|
59
|
+
}
|
|
60
|
+
}
|