@cocreate/notification 1.0.0 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [1.1.0](https://github.com/CoCreate-app/CoCreate-notification/compare/v1.0.1...v1.1.0) (2023-10-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * generates Vapid keys per client and returns publicKey to client, the public key is used to create a subscription object and return to server ([0267c19](https://github.com/CoCreate-app/CoCreate-notification/commit/0267c1904a6e6d03c73e4c767cb39f26287776d9))
7
+
8
+
9
+ ### Features
10
+
11
+ * Use subscription.endpoint to get the apikey ([5fdb66d](https://github.com/CoCreate-app/CoCreate-notification/commit/5fdb66d52ca996ca096f3c935a75bd98c812a9cc))
12
+
13
+ ## [1.0.1](https://github.com/CoCreate-app/CoCreate-notification/compare/v1.0.0...v1.0.1) (2023-09-24)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * add web-push in order to generate Vapid keys ([cffd320](https://github.com/CoCreate-app/CoCreate-notification/commit/cffd3202b28722cb930b2be18370b1c835c6e2b2))
19
+
1
20
  # 1.0.0 (2023-09-24)
2
21
 
3
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/notification",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Simple HTML5 & JavaScript component add, update & remove values in element's notification from input, select or js api. Easily configured using HTML5 notification and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "cocreate",
@@ -58,6 +58,7 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@cocreate/actions": "^1.11.2",
61
- "@cocreate/socket-client": "^1.28.0"
61
+ "@cocreate/socket-client": "^1.28.0",
62
+ "web-push": "^3.6.6"
62
63
  }
63
64
  }
package/src/client.js CHANGED
@@ -1,14 +1,6 @@
1
1
  import socket from '@cocreate/socket-client'
2
2
  // import actions from '@cocreate/actions'
3
3
 
4
- // Request push notification permission
5
- const permission = await Notification.requestPermission();
6
- if (permission === 'granted') {
7
- // User granted push notification permission, handle it here
8
- subscribeToPushNotifications();
9
- }
10
-
11
-
12
4
  async function handlePushPermission() {
13
5
  // Request push notification permission
14
6
  const permission = await Notification.requestPermission();
@@ -18,14 +10,9 @@ async function handlePushPermission() {
18
10
  const registration = await navigator.serviceWorker.ready;
19
11
  try {
20
12
  // Create a new subscription or retrieve the existing one
21
- const existingSubscription = await registration.pushManager.getSubscription();
13
+ let subscription = await registration.pushManager.getSubscription();
22
14
 
23
- if (existingSubscription) {
24
- // Update the existing subscription if needed
25
- // This might involve generating a new public key or other changes
26
- // For this example, we'll assume no updates are needed
27
- console.log('Subscription already exists:', existingSubscription);
28
- } else {
15
+ if (!subscription) {
29
16
  //TODO: getPublicKey
30
17
  let response = await socket.send({
31
18
  method: 'notification.publicKey',
@@ -35,31 +22,31 @@ async function handlePushPermission() {
35
22
  return
36
23
 
37
24
  // Create a new subscription
38
- const newSubscription = await registration.pushManager.subscribe({
25
+ subscription = await registration.pushManager.subscribe({
39
26
  userVisibleOnly: true,
40
27
  applicationServerKey: response.publicKey,
41
28
  });
42
29
 
43
- await socket.send({
44
- method: 'notification.subscription',
45
- subscription: newSubscription
46
- })
30
+ }
47
31
 
48
- console.log('New subscription created:', newSubscription);
32
+ await socket.send({
33
+ method: 'notification.subscription',
34
+ subscription: subscription
35
+ })
49
36
 
50
- navigator.serviceWorker.addEventListener('pushsubscriptionchange', (event) => {
51
- // Handle the subscription change here
52
- const newSubscription = event.newSubscription;
53
- const oldSubscription = event.oldSubscription;
37
+ console.log('New subscription created:', subscription);
54
38
 
55
- socket.send({
56
- method: 'notification.subscription',
57
- subscription: newSubscription
58
- })
39
+ navigator.serviceWorker.addEventListener('pushsubscriptionchange', (event) => {
40
+ // Handle the subscription change here
41
+ const newSubscription = event.newSubscription;
42
+ const oldSubscription = event.oldSubscription;
59
43
 
60
- });
44
+ socket.send({
45
+ method: 'notification.subscription',
46
+ subscription: subscription
47
+ })
61
48
 
62
- }
49
+ });
63
50
  } catch (error) {
64
51
  console.error('Error handling push subscription:', error);
65
52
  }
@@ -77,3 +64,4 @@ document.querySelector('[actions*="notification.subscribe"]').addEventListener('
77
64
  // endEvent: "notification",
78
65
  // callback: (action) => handlePushPermission
79
66
  // });
67
+ export default {}
package/src/server.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const crypto = require('crypto');
2
2
  const https = require('https');
3
+ const webpush = require('web-push');
3
4
 
4
5
  class CoCreateNotification {
5
6
  constructor(crud) {
6
- this.wsManager = crud.wsManager
7
+ this.socket = crud.wsManager
7
8
  this.crud = crud
8
9
  this.newKeyMap = new Map()
9
10
  this.subscriptions = new Map()
@@ -11,10 +12,10 @@ class CoCreateNotification {
11
12
  }
12
13
 
13
14
  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));
15
+ if (this.socket) {
16
+ this.socket.on('notification.publicKey', (data) => this.publicKey(data));
17
+ this.socket.on('notification.subscription', (data) => this.subscription(data));
18
+ this.socket.on('notification.send', (data) => this.send(data));
18
19
  }
19
20
  }
20
21
 
@@ -22,52 +23,100 @@ class CoCreateNotification {
22
23
  publicKey(data) {
23
24
  let subscription = this.subscriptions.has(data.clientId)
24
25
  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
26
+ // let newKeys = this.generateVapidKeys()
27
+ subscription = webpush.generateVAPIDKeys()
28
+ this.newKeyMap.set(data.clientId, subscription)
30
29
  }
31
30
 
32
- this.socket.send(data)
31
+ console.log('subscription: ', subscription)
33
32
 
33
+ data.publicKey = subscription.publicKey
34
+ if (data.socket)
35
+ this.socket.send(data)
34
36
  }
35
37
 
36
38
  async subscription(data) {
37
39
  let newKeys = this.newKeyMap.get(data.clientId)
38
40
  if (newKeys) {
39
41
  this.newKeyMap.delete(data.clientId)
42
+
40
43
  this.subscriptions.set(data.clientId, { ...newKeys, ...data.subscription })
41
44
  } else {
42
45
  newKeys = this.subscriptions.get(data.clientId)
43
46
  if (newKeys) {
44
47
  newKeys = { ...newKeys, ...data.subscription }
45
48
  } else {
46
- newKeys = {}
49
+ newKeys = await this.crud.send({
50
+ method: 'read.object',
51
+ array: 'clients',
52
+ object: {
53
+ _id: data.clientId
54
+ }
55
+ })
56
+ if (newKeys && newKeys.object && newKeys.object[0]) {
57
+ newKeys = newKeys.object[0]
58
+ newKeys = { ...newKeys, ...data.subscription }
59
+ this.subscriptions.set(data.clientId, { ...newKeys, ...data.subscription })
60
+ } else {
61
+ this.publicKey(data)
62
+ this.subscription(data)
63
+ }
47
64
  }
48
- }
49
65
 
50
- crud.send({
51
- method: 'update.object',
52
- array: 'keys',
53
- object: {
54
- ...data.subscription,
55
- ...newKeys
56
- }
57
- });
66
+ }
58
67
 
68
+ if (newKeys) {
69
+ let tokenOptions = {
70
+ vapidDetails: {
71
+ subject: 'mailto:contact@cocreate.app',
72
+ publicKey: newKeys.publicKey,
73
+ privateKey: newKeys.privateKey
74
+ },
75
+ // Other optional options can be included here
76
+ };
77
+
78
+ const jwt = webpush.generateRequestDetails(data.subscription, null, tokenOptions);
79
+
80
+ this.crud.send({
81
+ method: 'update.object',
82
+ array: 'clients',
83
+ object: {
84
+ _id: data.clientId,
85
+ ...data.subscription,
86
+ ...newKeys,
87
+ jwt
88
+ },
89
+ upsert: true,
90
+ organization_id: data.organization_id
91
+ });
92
+ }
59
93
  };
60
94
 
61
95
  // 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]
96
+ async send(data) {
97
+ let subscription = this.subscriptions.get(data.clientId)
98
+ if (!subscription) {
99
+ try {
100
+ subscription = await this.crud.send({
101
+ method: 'read.object',
102
+ array: 'clients',
103
+ object: {
104
+ _id: data.clientId
105
+ },
106
+ organization_id: data.organization_id
107
+
108
+ })
109
+ } catch (error) {
110
+ console.log(error)
111
+ }
112
+ if (subscription && subscription.object && subscription.object[0])
113
+ subscription = subscription.object[0]
114
+ }
66
115
  if (!subscription || !subscription.privateKey)
67
116
  return
68
117
 
69
118
  const cutoffDate = new Date();
70
- cutoffDate.setDate(cutoffDate.getDate() - subscription.rotateKeysIn || 90);
119
+ cutoffDate.setDate(cutoffDate.getDate() - 90);
71
120
 
72
121
  // Compare the modification time with the cutoff date
73
122
  if (!this.newKeyMap.has(data.clientId) && subscription.privateKeyCreatedOn < cutoffDate) {
@@ -78,15 +127,27 @@ class CoCreateNotification {
78
127
  if (data.payload && !data.payload.timestamp)
79
128
  data.payload.timestamp = Date.now()
80
129
 
81
- let payload = {...subscription, ...data.payload}
82
- delete payload.privateKey
83
- delete payload.publicKey
84
- payload = JSON.stringify(payload);
130
+ // let payload = data.payload
131
+ // delete payload.privateKey
132
+ // delete payload.publicKey
133
+
134
+ let tokenOptions = {
135
+ vapidDetails: {
136
+ subject: 'mailto:contact@cocreate.app',
137
+ publicKey: subscription.publicKey,
138
+ privateKey: subscription.privateKey
139
+ },
140
+ // Other optional options can be included here
141
+ };
142
+
143
+ const jwt = webpush.generateRequestDetails(subscription.endpoint, payload, tokenOptions);
144
+ const apiKey = getAPIKeyForEndpoint(subscrption.endpoint);
145
+
85
146
 
86
147
  const options = {
87
148
  method: 'POST',
88
149
  headers: {
89
- 'Authorization': `Bearer ${this.getVapidJWT(subscription.privateKey)}`,
150
+ 'Authorization': `Bearer ${apiKey || jwt.headers.Authorization}`,
90
151
  'Content-Type': 'application/json',
91
152
  },
92
153
  };
@@ -103,6 +164,26 @@ class CoCreateNotification {
103
164
  pushRequest.end();
104
165
  }
105
166
 
167
+ getAPIKeyForEndpoint(endpoint) {
168
+ // Define the mapping of browser endpoints to API keys
169
+ const browserEndpointsAndAPIKeys = {
170
+ 'https://fcm.googleapis.com/': 'yourFCMApiKey',
171
+ 'https://updates.push.services.mozilla.com/wpush/v2/': 'yourMozillaPushServiceApiKey',
172
+ 'https://api.push.apple.com/': 'yourAPNsApiKey', // For Safari on Mac
173
+ 'https://api.sandbox.push.apple.com/': 'yourAPNsDevApiKey', // For Safari on iOS (Development)
174
+ 'https://api.push.apple.com/': 'yourAPNsProdApiKey', // For Safari on iOS (Production)
175
+ // Add more browser endpoints and their API keys as needed
176
+ };
177
+
178
+ // Iterate through the keys in the mapping
179
+ for (const browserEndpoint of Object.keys(browserEndpointsAndAPIKeys)) {
180
+ if (endpoint.startsWith(browserEndpoint)) {
181
+ // Return the associated API key if the endpoint starts with a known browser endpoint
182
+ return browserEndpointsAndAPIKeys[browserEndpoint];
183
+ }
184
+ }
185
+ }
186
+
106
187
  generateVapidKeys() {
107
188
  const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
108
189
  modulusLength: 4096, // Choose an appropriate key length
@@ -138,10 +219,11 @@ class CoCreateNotification {
138
219
 
139
220
  // Function to base64 URL encode
140
221
  base64URLEncode(value) {
141
- return value.toString('base64')
222
+ const base64 = Buffer.from(value).toString('base64');
223
+ return base64
142
224
  .replace(/\+/g, '-')
143
225
  .replace(/\//g, '_')
144
- .replace(/=/g, '');
226
+ .replace(/=+$/, ''); // Remove trailing equal signs
145
227
  }
146
228
 
147
229
 
@@ -157,7 +239,7 @@ const payloadExample = {
157
239
  { action: 'action-2', title: 'Action 2' },
158
240
  ],
159
241
  tag: 'notification-tag', // A unique identifier for the notification
160
- data,
242
+ data: {},
161
243
  vibrate: [100, 50, 100], // Vibration pattern (milliseconds)
162
244
  image: '/path/to/notification-image.jpg', // An image to display within the notification
163
245
  requireInteraction: true, // Requires user interaction to close the notification