@cocreate/notification 1.0.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/src/index.js ADDED
@@ -0,0 +1,38 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ /**
19
+ * Commercial Licensing Information:
20
+ * For commercial use of this software without the copyleft provisions of the AGPLv3,
21
+ * you must obtain a commercial license from CoCreate LLC.
22
+ * For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
+ */
24
+
25
+ (function (root, factory) {
26
+ if (typeof define === 'function' && define.amd) {
27
+ define(["./client"], function (CoCreateNotification) {
28
+ return factory(CoCreateNotification)
29
+ });
30
+ } else if (typeof module === 'object' && module.exports) {
31
+ const CoCreateNotification = require("./server.js")
32
+ module.exports = factory(CoCreateNotification);
33
+ } else {
34
+ root.returnExports = factory(root["./client.js"]);
35
+ }
36
+ }(typeof self !== 'undefined' ? self : this, function (CoCreateNotification) {
37
+ return CoCreateNotification;
38
+ }));
package/src/server.js ADDED
@@ -0,0 +1,173 @@
1
+ const crypto = require('crypto');
2
+ const https = require('https');
3
+
4
+ class CoCreateNotification {
5
+ constructor(crud) {
6
+ this.wsManager = crud.wsManager
7
+ this.crud = crud
8
+ this.newKeyMap = new Map()
9
+ this.subscriptions = new Map()
10
+ this.init();
11
+ }
12
+
13
+ init() {
14
+ if (this.wsManager) {
15
+ this.wsManager.on('notification.publicKey', (data) => this.publicKey(data));
16
+ this.wsManager.on('notification.subscription', (data) => this.subscription(data));
17
+ this.wsManager.on('notification.send', (data) => this.send(data));
18
+ }
19
+ }
20
+
21
+ // Function to generate VAPID keys (public and private)
22
+ publicKey(data) {
23
+ let subscription = this.subscriptions.has(data.clientId)
24
+ if (!subscription) {
25
+ let newKeys = this.generateVapidKeys()
26
+ this.newKeyMap.set(data.clientId, newKeys)
27
+ data.publicKey = newKeyMap.publicKey
28
+ } else {
29
+ data.publicKey = subscription.publicKey
30
+ }
31
+
32
+ this.socket.send(data)
33
+
34
+ }
35
+
36
+ async subscription(data) {
37
+ let newKeys = this.newKeyMap.get(data.clientId)
38
+ if (newKeys) {
39
+ this.newKeyMap.delete(data.clientId)
40
+ this.subscriptions.set(data.clientId, { ...newKeys, ...data.subscription })
41
+ } else {
42
+ newKeys = this.subscriptions.get(data.clientId)
43
+ if (newKeys) {
44
+ newKeys = { ...newKeys, ...data.subscription }
45
+ } else {
46
+ newKeys = {}
47
+ }
48
+ }
49
+
50
+ crud.send({
51
+ method: 'update.object',
52
+ array: 'keys',
53
+ object: {
54
+ ...data.subscription,
55
+ ...newKeys
56
+ }
57
+ });
58
+
59
+ };
60
+
61
+ // Load the client's subscription object from storage
62
+ send(data) {
63
+ const key = crud.send({ array: 'keys', filter: { query: [{}] } });
64
+
65
+ const subscription = key.object[0]
66
+ if (!subscription || !subscription.privateKey)
67
+ return
68
+
69
+ const cutoffDate = new Date();
70
+ cutoffDate.setDate(cutoffDate.getDate() - subscription.rotateKeysIn || 90);
71
+
72
+ // Compare the modification time with the cutoff date
73
+ if (!this.newKeyMap.has(data.clientId) && subscription.privateKeyCreatedOn < cutoffDate) {
74
+ let newKeys = this.generateVapidKeys()
75
+ this.newKeyMap.set(data.clientId, newKeys)
76
+ }
77
+
78
+ if (data.payload && !data.payload.timestamp)
79
+ data.payload.timestamp = Date.now()
80
+
81
+ let payload = {...subscription, ...data.payload}
82
+ delete payload.privateKey
83
+ delete payload.publicKey
84
+ payload = JSON.stringify(payload);
85
+
86
+ const options = {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Authorization': `Bearer ${this.getVapidJWT(subscription.privateKey)}`,
90
+ 'Content-Type': 'application/json',
91
+ },
92
+ };
93
+
94
+ const pushRequest = https.request(subscription.endpoint, options, (response) => {
95
+ console.log(`Push notification status: ${response.statusCode}`);
96
+ });
97
+
98
+ pushRequest.on('error', (error) => {
99
+ console.error('Error sending push notification:', error);
100
+ });
101
+
102
+ pushRequest.write(payload);
103
+ pushRequest.end();
104
+ }
105
+
106
+ generateVapidKeys() {
107
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
108
+ modulusLength: 4096, // Choose an appropriate key length
109
+ privateKeyEncoding: {
110
+ type: 'pkcs8',
111
+ format: 'pem',
112
+ },
113
+ publicKeyEncoding: {
114
+ type: 'spki',
115
+ format: 'pem',
116
+ },
117
+ });
118
+ return { privateKey, publicKey, privateKeyCreatedOn: new Date().toISOString() };
119
+ }
120
+
121
+ // Function to generate a VAPID JWT
122
+ getVapidJWT(privateKey, subscription) {
123
+ const header = { 'alg': 'RS256', 'typ': 'JWT' };
124
+ const currentTimestamp = Math.floor(Date.now() / 1000);
125
+ const expirationTimeInSeconds = 90 * 24 * 60 * 60; // 90 days in seconds
126
+ const expirationTime = currentTimestamp + expirationTimeInSeconds;
127
+
128
+ const payload = {
129
+ 'aud': subscription.endpoint,
130
+ 'exp': expirationTime,
131
+ 'sub': 'mailto:contact@cocreate.app'
132
+ };
133
+
134
+ const jwt = this.base64URLEncode(JSON.stringify(header)) + '.' + this.base64URLEncode(JSON.stringify(payload));
135
+ const signature = crypto.createSign('sha256').update(jwt).sign(privateKey, 'base64');
136
+ return jwt + '.' + this.base64URLEncode(signature);
137
+ }
138
+
139
+ // Function to base64 URL encode
140
+ base64URLEncode(value) {
141
+ return value.toString('base64')
142
+ .replace(/\+/g, '-')
143
+ .replace(/\//g, '_')
144
+ .replace(/=/g, '');
145
+ }
146
+
147
+
148
+ }
149
+ module.exports = CoCreateNotification;
150
+
151
+ const payloadExample = {
152
+ body: 'This is the notification body text',
153
+ icon: '/path/to/notification-icon.png',
154
+ badge: '/path/to/notification-badge.png',
155
+ actions: [
156
+ { action: 'action-1', title: 'Action 1' },
157
+ { action: 'action-2', title: 'Action 2' },
158
+ ],
159
+ tag: 'notification-tag', // A unique identifier for the notification
160
+ data,
161
+ vibrate: [100, 50, 100], // Vibration pattern (milliseconds)
162
+ image: '/path/to/notification-image.jpg', // An image to display within the notification
163
+ requireInteraction: true, // Requires user interaction to close the notification
164
+ silent: true, // Delivers a silent notification (no sound or vibration)
165
+ renotify: true, // Allows a notification with the same tag to renotify the user
166
+ actions: [
167
+ { action: 'action-1', title: 'Action 1' },
168
+ { action: 'action-2', title: 'Action 2' },
169
+ ],
170
+ timestamp: Date.now(), // A timestamp for the notification
171
+ }
172
+
173
+
@@ -0,0 +1,90 @@
1
+ const path = require("path")
2
+ const TerserPlugin = require("terser-webpack-plugin")
3
+ const MiniCssExtractPlugin = require("mini-css-extract-plugin")
4
+ const { CleanWebpackPlugin } = require("clean-webpack-plugin")
5
+
6
+ module.exports = (env, argv) => {
7
+ let isProduction = false
8
+ if (argv.mode === 'production')
9
+ isProduction = true
10
+
11
+ const config = {
12
+ entry: {
13
+ "CoCreate-notification": "./src/index.js",
14
+ },
15
+ output: {
16
+ path: path.resolve(__dirname, "dist"),
17
+ filename: isProduction ? "[name].min.js" : "[name].js",
18
+ libraryTarget: "umd",
19
+ libraryExport: "default",
20
+ library: ["CoCreate", "notification"],
21
+ globalObject: "this",
22
+ },
23
+
24
+ plugins: [
25
+ new CleanWebpackPlugin(),
26
+ new MiniCssExtractPlugin({
27
+ filename: "[name].css",
28
+ }),
29
+ ],
30
+ // Default mode for Webpack is production.
31
+ // Depending on mode Webpack will apply different things
32
+ // on final bundle. For now we don't need production's JavaScript
33
+ // minifying and other thing so let's set mode to development
34
+ mode: isProduction ? "production" : "development",
35
+ module: {
36
+ rules: [
37
+ {
38
+ test: /.js$/,
39
+ exclude: /(node_modules)/,
40
+ use: {
41
+ loader: "babel-loader",
42
+ options: {
43
+ plugins: ["@babel/plugin-transform-modules-commonjs"],
44
+ },
45
+ },
46
+ },
47
+ {
48
+ test: /.css$/i,
49
+ use: [
50
+ { loader: "style-loader", options: { injectType: "linkTag" } },
51
+ "file-loader",
52
+ ],
53
+ },
54
+ ],
55
+ },
56
+
57
+ // add source map
58
+ ...(isProduction ? {} : { devtool: "eval-source-map" }),
59
+
60
+ optimization: {
61
+ minimize: true,
62
+ minimizer: [
63
+ new TerserPlugin({
64
+ extractComments: true,
65
+ // cache: true,
66
+ parallel: true,
67
+ // sourceMap: true, // Must be set to true if using source-maps in production
68
+ terserOptions: {
69
+ // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
70
+ // extractComments: 'all',
71
+ compress: {
72
+ drop_console: true,
73
+ },
74
+ },
75
+ }),
76
+ ],
77
+ splitChunks: {
78
+ chunks: "all",
79
+ minSize: 200,
80
+ // maxSize: 99999,
81
+ //minChunks: 1,
82
+
83
+ cacheGroups: {
84
+ defaultVendors: false,
85
+ },
86
+ },
87
+ },
88
+ }
89
+ return config
90
+ }