@nixxie-cms/notifications 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nixxie International DMCC
4
+ Portions Copyright (c) 2023 Thinkmill Labs Pty Ltd and contributors
5
+ (this software is derived from the KeystoneJS project, https://keystonejs.com)
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @nixxie-cms/notifications
2
+
3
+ Multi-channel notifications for Nixxie CMS: Slack, Discord, SMS (Twilio), generic webhooks and your
4
+ own custom channels. No SDK dependencies — everything goes over `fetch`.
5
+
6
+ ```ts
7
+ import { config } from '@nixxie-cms/core'
8
+ import { createNotifications } from '@nixxie-cms/notifications'
9
+
10
+ export default config({
11
+ notifications: createNotifications({
12
+ channels: {
13
+ slack: { type: 'slack', webhookUrl: process.env.SLACK_WEBHOOK! },
14
+ ops: { type: 'sms', accountSid: '...', authToken: '...', from: '+1555...' },
15
+ },
16
+ default: 'slack',
17
+ }),
18
+ })
19
+ ```
20
+
21
+ Available everywhere as `context.services.notifications`:
22
+
23
+ ```ts
24
+ await context.services.notifications.send({
25
+ channel: 'slack',
26
+ title: 'New signup',
27
+ body: `${user.email} just registered`,
28
+ })
29
+
30
+ // SMS needs a `to`:
31
+ await context.services.notifications.send({ channel: 'ops', to: '+1555...', body: 'Deploy done' })
32
+ ```
33
+
34
+ Add a `{ type: 'custom', send }` channel to deliver anywhere else (push, email queue, etc.).
@@ -0,0 +1,7 @@
1
+ import type { NixxieNotification } from '@nixxie-cms/core';
2
+ import type { ChannelConfig } from "./types.js";
3
+ /** Deliver a notification on a single configured channel. Returns the provider message id. */
4
+ export declare function deliver(channel: ChannelConfig, notification: NixxieNotification): Promise<{
5
+ id?: string;
6
+ }>;
7
+ //# sourceMappingURL=channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.d.ts","sourceRoot":"../../../src","sources":["channels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAe;AAE5C,8FAA8F;AAC9F,wBAAsB,OAAO,CAC3B,OAAO,EAAE,aAAa,EACtB,YAAY,EAAE,kBAAkB,GAC/B,OAAO,CAAC;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiB1B"}
@@ -0,0 +1,14 @@
1
+ import type { NixxieNotification, NixxieNotificationResult, NixxieNotificationsService } from '@nixxie-cms/core';
2
+ import type { NotificationsConfig } from "./types.js";
3
+ export declare class NotificationsService implements NixxieNotificationsService {
4
+ private config;
5
+ constructor(config: NotificationsConfig);
6
+ channels(): string[];
7
+ private targetChannels;
8
+ send(notification: NixxieNotification): Promise<NixxieNotificationResult[]>;
9
+ }
10
+ export declare function createNotifications(config: NotificationsConfig): NotificationsService;
11
+ export { deliver } from "./channels.js";
12
+ export type { NotificationsConfig, ChannelConfig, SlackChannelConfig, DiscordChannelConfig, WebhookChannelConfig, SmsChannelConfig, CustomChannelConfig, } from "./types.js";
13
+ export type { NixxieNotification, NixxieNotificationResult, NixxieNotificationsService, } from '@nixxie-cms/core';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"../../../src","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAe;AAElD,qBAAa,oBAAqB,YAAW,0BAA0B;IACrE,OAAO,CAAC,MAAM,CAAqB;gBAEvB,MAAM,EAAE,mBAAmB;IAIvC,QAAQ,IAAI,MAAM,EAAE;IAIpB,OAAO,CAAC,cAAc;IAKhB,IAAI,CAAC,YAAY,EAAE,kBAAkB,GAAG,OAAO,CAAC,wBAAwB,EAAE,CAAC;CAelF;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,oBAAoB,CAErF;AAED,OAAO,EAAE,OAAO,EAAE,sBAAkB;AACpC,YAAY,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,mBAAmB,GACpB,mBAAe;AAChB,YAAY,EACV,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,43 @@
1
+ import type { NixxieNotification, NixxieNotificationResult, NixxieNotificationsService } from '@nixxie-cms/core';
2
+ export type { NixxieNotification, NixxieNotificationResult, NixxieNotificationsService };
3
+ export type SlackChannelConfig = {
4
+ type: 'slack';
5
+ /** Slack Incoming Webhook URL. */
6
+ webhookUrl: string;
7
+ };
8
+ export type DiscordChannelConfig = {
9
+ type: 'discord';
10
+ /** Discord webhook URL. */
11
+ webhookUrl: string;
12
+ };
13
+ export type WebhookChannelConfig = {
14
+ type: 'webhook';
15
+ /** Endpoint to POST the notification JSON to. */
16
+ url: string;
17
+ /** Extra headers (e.g. an auth token). */
18
+ headers?: Record<string, string>;
19
+ };
20
+ export type SmsChannelConfig = {
21
+ type: 'sms';
22
+ /** Twilio Account SID. */
23
+ accountSid: string;
24
+ /** Twilio Auth Token. */
25
+ authToken: string;
26
+ /** Sending phone number in E.164 format. */
27
+ from: string;
28
+ };
29
+ export type CustomChannelConfig = {
30
+ type: 'custom';
31
+ /** Implement delivery yourself. Return the provider message id on success. */
32
+ send: (notification: NixxieNotification) => Promise<{
33
+ id?: string;
34
+ } | void>;
35
+ };
36
+ export type ChannelConfig = SlackChannelConfig | DiscordChannelConfig | WebhookChannelConfig | SmsChannelConfig | CustomChannelConfig;
37
+ export type NotificationsConfig = {
38
+ /** Named channels. Reference them by key via `notification.channel`. */
39
+ channels: Record<string, ChannelConfig>;
40
+ /** Channel(s) used when a notification doesn't specify one. Default: every configured channel. */
41
+ default?: string | string[];
42
+ };
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"../../../src","sources":["types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,CAAA;AAExF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,CAAA;IACb,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,SAAS,CAAA;IACf,2BAA2B;IAC3B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,SAAS,CAAA;IACf,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,KAAK,CAAA;IACX,0BAA0B;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,yBAAyB;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,QAAQ,CAAA;IACd,8EAA8E;IAC9E,IAAI,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,aAAa,GACrB,kBAAkB,GAClB,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,mBAAmB,CAAA;AAEvB,MAAM,MAAM,mBAAmB,GAAG;IAChC,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACvC,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAC5B,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from "./declarations/src/index.js";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibml4eGllLWNtcy1ub3RpZmljYXRpb25zLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi9kZWNsYXJhdGlvbnMvc3JjL2luZGV4LmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ==
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /** Deliver a notification on a single configured channel. Returns the provider message id. */
6
+ async function deliver(channel, notification) {
7
+ var _await$channel$send;
8
+ switch (channel.type) {
9
+ case 'slack':
10
+ return deliverSlack(channel.webhookUrl, notification);
11
+ case 'discord':
12
+ return deliverDiscord(channel.webhookUrl, notification);
13
+ case 'webhook':
14
+ return deliverWebhook(channel.url, channel.headers, notification);
15
+ case 'sms':
16
+ return deliverSms(channel, notification);
17
+ case 'custom':
18
+ return (_await$channel$send = await channel.send(notification)) !== null && _await$channel$send !== void 0 ? _await$channel$send : {};
19
+ default:
20
+ {
21
+ const exhaustive = channel;
22
+ throw new Error(`Unknown channel type: ${exhaustive.type}`);
23
+ }
24
+ }
25
+ }
26
+ function text(n) {
27
+ return n.title ? `*${n.title}*\n${n.body}` : n.body;
28
+ }
29
+ async function deliverSlack(url, n) {
30
+ var _n$data;
31
+ const res = await fetch(url, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json'
35
+ },
36
+ // Spread custom data first so it can add Slack fields (blocks, attachments, …) without
37
+ // clobbering the composed message text.
38
+ body: JSON.stringify({
39
+ ...((_n$data = n.data) !== null && _n$data !== void 0 ? _n$data : {}),
40
+ text: text(n)
41
+ })
42
+ });
43
+ if (!res.ok) throw new Error(`Slack delivery failed (${res.status}): ${await res.text()}`);
44
+ return {};
45
+ }
46
+ async function deliverDiscord(url, n) {
47
+ const res = await fetch(url, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json'
51
+ },
52
+ body: JSON.stringify({
53
+ content: text(n)
54
+ })
55
+ });
56
+ if (!res.ok) throw new Error(`Discord delivery failed (${res.status}): ${await res.text()}`);
57
+ return {};
58
+ }
59
+ async function deliverWebhook(url, headers, n) {
60
+ const res = await fetch(url, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ ...headers
65
+ },
66
+ body: JSON.stringify({
67
+ title: n.title,
68
+ body: n.body,
69
+ to: n.to,
70
+ data: n.data
71
+ })
72
+ });
73
+ if (!res.ok) throw new Error(`Webhook delivery failed (${res.status}): ${await res.text()}`);
74
+ return {};
75
+ }
76
+ async function deliverSms(channel, n) {
77
+ if (!n.to) throw new Error('SMS notifications require `to` (a destination phone number)');
78
+ const auth = Buffer.from(`${channel.accountSid}:${channel.authToken}`).toString('base64');
79
+ const body = new URLSearchParams({
80
+ To: n.to,
81
+ From: channel.from,
82
+ Body: n.title ? `${n.title}: ${n.body}` : n.body
83
+ });
84
+ const res = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${channel.accountSid}/Messages.json`, {
85
+ method: 'POST',
86
+ headers: {
87
+ Authorization: `Basic ${auth}`,
88
+ 'Content-Type': 'application/x-www-form-urlencoded'
89
+ },
90
+ body
91
+ });
92
+ if (!res.ok) throw new Error(`SMS delivery failed (${res.status}): ${await res.text()}`);
93
+ const data = await res.json();
94
+ return {
95
+ id: data.sid
96
+ };
97
+ }
98
+
99
+ class NotificationsService {
100
+ constructor(config) {
101
+ this.config = config;
102
+ }
103
+ channels() {
104
+ return Object.keys(this.config.channels);
105
+ }
106
+ targetChannels(notification) {
107
+ var _ref, _notification$channel;
108
+ const requested = (_ref = (_notification$channel = notification.channel) !== null && _notification$channel !== void 0 ? _notification$channel : this.config.default) !== null && _ref !== void 0 ? _ref : this.channels();
109
+ return Array.isArray(requested) ? requested : [requested];
110
+ }
111
+ async send(notification) {
112
+ const targets = this.targetChannels(notification);
113
+ return Promise.all(targets.map(async name => {
114
+ const channel = this.config.channels[name];
115
+ if (!channel) return {
116
+ channel: name,
117
+ ok: false,
118
+ error: `Unknown channel: ${name}`
119
+ };
120
+ try {
121
+ const {
122
+ id
123
+ } = await deliver(channel, notification);
124
+ return {
125
+ channel: name,
126
+ ok: true,
127
+ id
128
+ };
129
+ } catch (err) {
130
+ var _err$message;
131
+ return {
132
+ channel: name,
133
+ ok: false,
134
+ error: (_err$message = err === null || err === void 0 ? void 0 : err.message) !== null && _err$message !== void 0 ? _err$message : String(err)
135
+ };
136
+ }
137
+ }));
138
+ }
139
+ }
140
+ function createNotifications(config) {
141
+ return new NotificationsService(config);
142
+ }
143
+
144
+ exports.NotificationsService = NotificationsService;
145
+ exports.createNotifications = createNotifications;
146
+ exports.deliver = deliver;
@@ -0,0 +1,140 @@
1
+ /** Deliver a notification on a single configured channel. Returns the provider message id. */
2
+ async function deliver(channel, notification) {
3
+ var _await$channel$send;
4
+ switch (channel.type) {
5
+ case 'slack':
6
+ return deliverSlack(channel.webhookUrl, notification);
7
+ case 'discord':
8
+ return deliverDiscord(channel.webhookUrl, notification);
9
+ case 'webhook':
10
+ return deliverWebhook(channel.url, channel.headers, notification);
11
+ case 'sms':
12
+ return deliverSms(channel, notification);
13
+ case 'custom':
14
+ return (_await$channel$send = await channel.send(notification)) !== null && _await$channel$send !== void 0 ? _await$channel$send : {};
15
+ default:
16
+ {
17
+ const exhaustive = channel;
18
+ throw new Error(`Unknown channel type: ${exhaustive.type}`);
19
+ }
20
+ }
21
+ }
22
+ function text(n) {
23
+ return n.title ? `*${n.title}*\n${n.body}` : n.body;
24
+ }
25
+ async function deliverSlack(url, n) {
26
+ var _n$data;
27
+ const res = await fetch(url, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json'
31
+ },
32
+ // Spread custom data first so it can add Slack fields (blocks, attachments, …) without
33
+ // clobbering the composed message text.
34
+ body: JSON.stringify({
35
+ ...((_n$data = n.data) !== null && _n$data !== void 0 ? _n$data : {}),
36
+ text: text(n)
37
+ })
38
+ });
39
+ if (!res.ok) throw new Error(`Slack delivery failed (${res.status}): ${await res.text()}`);
40
+ return {};
41
+ }
42
+ async function deliverDiscord(url, n) {
43
+ const res = await fetch(url, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json'
47
+ },
48
+ body: JSON.stringify({
49
+ content: text(n)
50
+ })
51
+ });
52
+ if (!res.ok) throw new Error(`Discord delivery failed (${res.status}): ${await res.text()}`);
53
+ return {};
54
+ }
55
+ async function deliverWebhook(url, headers, n) {
56
+ const res = await fetch(url, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ ...headers
61
+ },
62
+ body: JSON.stringify({
63
+ title: n.title,
64
+ body: n.body,
65
+ to: n.to,
66
+ data: n.data
67
+ })
68
+ });
69
+ if (!res.ok) throw new Error(`Webhook delivery failed (${res.status}): ${await res.text()}`);
70
+ return {};
71
+ }
72
+ async function deliverSms(channel, n) {
73
+ if (!n.to) throw new Error('SMS notifications require `to` (a destination phone number)');
74
+ const auth = Buffer.from(`${channel.accountSid}:${channel.authToken}`).toString('base64');
75
+ const body = new URLSearchParams({
76
+ To: n.to,
77
+ From: channel.from,
78
+ Body: n.title ? `${n.title}: ${n.body}` : n.body
79
+ });
80
+ const res = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${channel.accountSid}/Messages.json`, {
81
+ method: 'POST',
82
+ headers: {
83
+ Authorization: `Basic ${auth}`,
84
+ 'Content-Type': 'application/x-www-form-urlencoded'
85
+ },
86
+ body
87
+ });
88
+ if (!res.ok) throw new Error(`SMS delivery failed (${res.status}): ${await res.text()}`);
89
+ const data = await res.json();
90
+ return {
91
+ id: data.sid
92
+ };
93
+ }
94
+
95
+ class NotificationsService {
96
+ constructor(config) {
97
+ this.config = config;
98
+ }
99
+ channels() {
100
+ return Object.keys(this.config.channels);
101
+ }
102
+ targetChannels(notification) {
103
+ var _ref, _notification$channel;
104
+ const requested = (_ref = (_notification$channel = notification.channel) !== null && _notification$channel !== void 0 ? _notification$channel : this.config.default) !== null && _ref !== void 0 ? _ref : this.channels();
105
+ return Array.isArray(requested) ? requested : [requested];
106
+ }
107
+ async send(notification) {
108
+ const targets = this.targetChannels(notification);
109
+ return Promise.all(targets.map(async name => {
110
+ const channel = this.config.channels[name];
111
+ if (!channel) return {
112
+ channel: name,
113
+ ok: false,
114
+ error: `Unknown channel: ${name}`
115
+ };
116
+ try {
117
+ const {
118
+ id
119
+ } = await deliver(channel, notification);
120
+ return {
121
+ channel: name,
122
+ ok: true,
123
+ id
124
+ };
125
+ } catch (err) {
126
+ var _err$message;
127
+ return {
128
+ channel: name,
129
+ ok: false,
130
+ error: (_err$message = err === null || err === void 0 ? void 0 : err.message) !== null && _err$message !== void 0 ? _err$message : String(err)
131
+ };
132
+ }
133
+ }));
134
+ }
135
+ }
136
+ function createNotifications(config) {
137
+ return new NotificationsService(config);
138
+ }
139
+
140
+ export { NotificationsService, createNotifications, deliver };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@nixxie-cms/notifications",
3
+ "version": "1.0.1",
4
+ "license": "MIT",
5
+ "main": "dist/nixxie-cms-notifications.cjs.js",
6
+ "module": "dist/nixxie-cms-notifications.esm.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/nixxie-cms-notifications.cjs.js",
10
+ "module": "./dist/nixxie-cms-notifications.esm.js",
11
+ "default": "./dist/nixxie-cms-notifications.cjs.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "dependencies": {
16
+ "@babel/runtime": "^7.24.7"
17
+ },
18
+ "devDependencies": {
19
+ "@nixxie-cms/core": "^1.0.1"
20
+ },
21
+ "peerDependencies": {
22
+ "@nixxie-cms/core": "^1.0.1"
23
+ },
24
+ "preconstruct": {
25
+ "entrypoints": [
26
+ "index.ts"
27
+ ]
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/nixxiecms/nixxie/tree/main/packages/notifications"
32
+ }
33
+ }
@@ -0,0 +1,92 @@
1
+ import type { NixxieNotification } from '@nixxie-cms/core'
2
+ import type { ChannelConfig } from './types'
3
+
4
+ /** Deliver a notification on a single configured channel. Returns the provider message id. */
5
+ export async function deliver(
6
+ channel: ChannelConfig,
7
+ notification: NixxieNotification
8
+ ): Promise<{ id?: string }> {
9
+ switch (channel.type) {
10
+ case 'slack':
11
+ return deliverSlack(channel.webhookUrl, notification)
12
+ case 'discord':
13
+ return deliverDiscord(channel.webhookUrl, notification)
14
+ case 'webhook':
15
+ return deliverWebhook(channel.url, channel.headers, notification)
16
+ case 'sms':
17
+ return deliverSms(channel, notification)
18
+ case 'custom':
19
+ return (await channel.send(notification)) ?? {}
20
+ default: {
21
+ const exhaustive: never = channel
22
+ throw new Error(`Unknown channel type: ${(exhaustive as any).type}`)
23
+ }
24
+ }
25
+ }
26
+
27
+ function text(n: NixxieNotification): string {
28
+ return n.title ? `*${n.title}*\n${n.body}` : n.body
29
+ }
30
+
31
+ async function deliverSlack(url: string, n: NixxieNotification): Promise<{ id?: string }> {
32
+ const res = await fetch(url, {
33
+ method: 'POST',
34
+ headers: { 'Content-Type': 'application/json' },
35
+ // Spread custom data first so it can add Slack fields (blocks, attachments, …) without
36
+ // clobbering the composed message text.
37
+ body: JSON.stringify({ ...(n.data ?? {}), text: text(n) }),
38
+ })
39
+ if (!res.ok) throw new Error(`Slack delivery failed (${res.status}): ${await res.text()}`)
40
+ return {}
41
+ }
42
+
43
+ async function deliverDiscord(url: string, n: NixxieNotification): Promise<{ id?: string }> {
44
+ const res = await fetch(url, {
45
+ method: 'POST',
46
+ headers: { 'Content-Type': 'application/json' },
47
+ body: JSON.stringify({ content: text(n) }),
48
+ })
49
+ if (!res.ok) throw new Error(`Discord delivery failed (${res.status}): ${await res.text()}`)
50
+ return {}
51
+ }
52
+
53
+ async function deliverWebhook(
54
+ url: string,
55
+ headers: Record<string, string> | undefined,
56
+ n: NixxieNotification
57
+ ): Promise<{ id?: string }> {
58
+ const res = await fetch(url, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json', ...headers },
61
+ body: JSON.stringify({ title: n.title, body: n.body, to: n.to, data: n.data }),
62
+ })
63
+ if (!res.ok) throw new Error(`Webhook delivery failed (${res.status}): ${await res.text()}`)
64
+ return {}
65
+ }
66
+
67
+ async function deliverSms(
68
+ channel: { accountSid: string; authToken: string; from: string },
69
+ n: NixxieNotification
70
+ ): Promise<{ id?: string }> {
71
+ if (!n.to) throw new Error('SMS notifications require `to` (a destination phone number)')
72
+ const auth = Buffer.from(`${channel.accountSid}:${channel.authToken}`).toString('base64')
73
+ const body = new URLSearchParams({
74
+ To: n.to,
75
+ From: channel.from,
76
+ Body: n.title ? `${n.title}: ${n.body}` : n.body,
77
+ })
78
+ const res = await fetch(
79
+ `https://api.twilio.com/2010-04-01/Accounts/${channel.accountSid}/Messages.json`,
80
+ {
81
+ method: 'POST',
82
+ headers: {
83
+ Authorization: `Basic ${auth}`,
84
+ 'Content-Type': 'application/x-www-form-urlencoded',
85
+ },
86
+ body,
87
+ }
88
+ )
89
+ if (!res.ok) throw new Error(`SMS delivery failed (${res.status}): ${await res.text()}`)
90
+ const data: any = await res.json()
91
+ return { id: data.sid }
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type {
2
+ NixxieNotification,
3
+ NixxieNotificationResult,
4
+ NixxieNotificationsService,
5
+ } from '@nixxie-cms/core'
6
+ import { deliver } from './channels'
7
+ import type { NotificationsConfig } from './types'
8
+
9
+ export class NotificationsService implements NixxieNotificationsService {
10
+ private config: NotificationsConfig
11
+
12
+ constructor(config: NotificationsConfig) {
13
+ this.config = config
14
+ }
15
+
16
+ channels(): string[] {
17
+ return Object.keys(this.config.channels)
18
+ }
19
+
20
+ private targetChannels(notification: NixxieNotification): string[] {
21
+ const requested = notification.channel ?? this.config.default ?? this.channels()
22
+ return Array.isArray(requested) ? requested : [requested]
23
+ }
24
+
25
+ async send(notification: NixxieNotification): Promise<NixxieNotificationResult[]> {
26
+ const targets = this.targetChannels(notification)
27
+ return Promise.all(
28
+ targets.map(async (name): Promise<NixxieNotificationResult> => {
29
+ const channel = this.config.channels[name]
30
+ if (!channel) return { channel: name, ok: false, error: `Unknown channel: ${name}` }
31
+ try {
32
+ const { id } = await deliver(channel, notification)
33
+ return { channel: name, ok: true, id }
34
+ } catch (err: any) {
35
+ return { channel: name, ok: false, error: err?.message ?? String(err) }
36
+ }
37
+ })
38
+ )
39
+ }
40
+ }
41
+
42
+ export function createNotifications(config: NotificationsConfig): NotificationsService {
43
+ return new NotificationsService(config)
44
+ }
45
+
46
+ export { deliver } from './channels'
47
+ export type {
48
+ NotificationsConfig,
49
+ ChannelConfig,
50
+ SlackChannelConfig,
51
+ DiscordChannelConfig,
52
+ WebhookChannelConfig,
53
+ SmsChannelConfig,
54
+ CustomChannelConfig,
55
+ } from './types'
56
+ export type {
57
+ NixxieNotification,
58
+ NixxieNotificationResult,
59
+ NixxieNotificationsService,
60
+ } from '@nixxie-cms/core'
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type {
2
+ NixxieNotification,
3
+ NixxieNotificationResult,
4
+ NixxieNotificationsService,
5
+ } from '@nixxie-cms/core'
6
+
7
+ export type { NixxieNotification, NixxieNotificationResult, NixxieNotificationsService }
8
+
9
+ export type SlackChannelConfig = {
10
+ type: 'slack'
11
+ /** Slack Incoming Webhook URL. */
12
+ webhookUrl: string
13
+ }
14
+
15
+ export type DiscordChannelConfig = {
16
+ type: 'discord'
17
+ /** Discord webhook URL. */
18
+ webhookUrl: string
19
+ }
20
+
21
+ export type WebhookChannelConfig = {
22
+ type: 'webhook'
23
+ /** Endpoint to POST the notification JSON to. */
24
+ url: string
25
+ /** Extra headers (e.g. an auth token). */
26
+ headers?: Record<string, string>
27
+ }
28
+
29
+ export type SmsChannelConfig = {
30
+ type: 'sms'
31
+ /** Twilio Account SID. */
32
+ accountSid: string
33
+ /** Twilio Auth Token. */
34
+ authToken: string
35
+ /** Sending phone number in E.164 format. */
36
+ from: string
37
+ }
38
+
39
+ export type CustomChannelConfig = {
40
+ type: 'custom'
41
+ /** Implement delivery yourself. Return the provider message id on success. */
42
+ send: (notification: NixxieNotification) => Promise<{ id?: string } | void>
43
+ }
44
+
45
+ export type ChannelConfig =
46
+ | SlackChannelConfig
47
+ | DiscordChannelConfig
48
+ | WebhookChannelConfig
49
+ | SmsChannelConfig
50
+ | CustomChannelConfig
51
+
52
+ export type NotificationsConfig = {
53
+ /** Named channels. Reference them by key via `notification.channel`. */
54
+ channels: Record<string, ChannelConfig>
55
+ /** Channel(s) used when a notification doesn't specify one. Default: every configured channel. */
56
+ default?: string | string[]
57
+ }