@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/.github/FUNDING.yml +3 -0
- package/.github/workflows/automated.yml +44 -0
- package/.github/workflows/manual.yml +44 -0
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +96 -0
- package/CoCreate.config.js +24 -0
- package/LICENSE +683 -0
- package/README.md +85 -0
- package/demo/index.html +20 -0
- package/docs/index.html +378 -0
- package/package.json +63 -0
- package/release.config.js +22 -0
- package/src/client.js +79 -0
- package/src/index.js +38 -0
- package/src/server.js +173 -0
- package/webpack.config.js +90 -0
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
|
+
}
|