@abtnode/blocklet-services 1.8.1 → 1.8.2

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/api/index.js CHANGED
@@ -181,6 +181,7 @@ module.exports = function createServer(node, serverOptions = {}) {
181
181
 
182
182
  // API: notification
183
183
  notificationService.sendToUser.attach(server);
184
+ notificationService.sendToAppChannel.attach(server);
184
185
 
185
186
  // Middleware: auth info
186
187
  server.use(authMiddlewares.bearerToken);
package/api/libs/auth.js CHANGED
@@ -4,7 +4,7 @@ const joinUrl = require('url-join');
4
4
  const DiskStorage = require('@arcblock/did-auth-storage-nedb');
5
5
  const { WalletAuthenticator } = require('@arcblock/did-auth');
6
6
  const WalletHandlers = require('@blocklet/sdk/lib/wallet-handler');
7
- const sendNotification = require('@blocklet/sdk/lib/util/send-notification');
7
+ const { sendToUser } = require('@blocklet/sdk/lib/util/send-notification');
8
8
  const { WELLKNOWN_SERVICE_PATH_PREFIX, NODE_SERVICES_PREFIX } = require('@abtnode/constant');
9
9
 
10
10
  const { getBlockletLogo } = require('../util');
@@ -62,11 +62,10 @@ module.exports = (node, opts) => {
62
62
  sendNotificationFn: async (connectedDid, message, { req }) => {
63
63
  const { wallet } = await req.getBlockletInfo();
64
64
  const sender = {
65
- appId: wallet.address,
65
+ appDid: wallet.address,
66
66
  appSk: wallet.secretKey,
67
- did: req.getBlockletDid(),
68
67
  };
69
- return sendNotification(connectedDid, message, sender, process.env.ABT_NODE_SERVICE_PORT);
68
+ return sendToUser(connectedDid, message, sender, process.env.ABT_NODE_SERVICE_PORT);
70
69
  },
71
70
  };
72
71
 
@@ -133,6 +133,18 @@ module.exports = {
133
133
 
134
134
  await node.setBlockletInitialized({ did: blocklet.meta.did, owner: { did: userDid, pk } });
135
135
 
136
+ // 调用 store 管理公开实例
137
+ // 如果一个 blocklet 没有设置 公开实例,启动成功后不应给 store 发请求
138
+ const { publicToStore } = blocklet.settings;
139
+ if (publicToStore) {
140
+ try {
141
+ await handleInstanceInStore(blocklet, { userDid, publicToStore });
142
+ } catch (error) {
143
+ // 即使实例公开不成功,不能影响正常启动流程
144
+ logger.error('failed to send blocklet logo', { did: blocklet.meta.did, error });
145
+ }
146
+ }
147
+
136
148
  // create vc
137
149
  const vc = createPassportVC({
138
150
  issuerName: name,
@@ -174,20 +186,11 @@ module.exports = {
174
186
  node
175
187
  );
176
188
 
177
- // 调用 store 管理公开实例
178
- const { publicToStore } = blocklet.settings;
179
- try {
180
- await handleInstanceInStore(blocklet, { userDid, publicToStore });
181
- } catch (error) {
182
- // 即使实例公开不成功,不能影响正常启动流程
183
- logger.error('failed to send blocklet logo', { did: blocklet.meta.did, error });
184
- }
185
-
186
189
  // send vc to wallet
187
190
  const receiver = userDid;
188
191
  const token = JWT.sign(wallet.address, wallet.secretKey);
189
192
  const data = {
190
- sender: { did: teamDid, token, appDid: wallet.address },
193
+ sender: { token, appDid: wallet.address },
191
194
  receiver,
192
195
  notification: {
193
196
  title: {
@@ -0,0 +1,114 @@
1
+ const get = require('lodash/get');
2
+
3
+ const { CHANNEL_TYPE, parseChannel } = require('@blocklet/meta/lib/channel');
4
+ const { validateNotification } = require('@blocklet/sdk/lib/validators/notification');
5
+ const { NODE_MODES } = require('@abtnode/constant');
6
+
7
+ const { ensureSender, parseNotification, broadcast, EVENTS } = require('../util');
8
+
9
+ /**
10
+ *
11
+ * @param {ABTNode} node
12
+ * @param {{
13
+ * {{
14
+ * {String} did
15
+ * {String} token
16
+ * }} sender
17
+ * {String} channel
18
+ * {String} event
19
+ * {Array|Object} notification
20
+ * }} payload
21
+ * @param {WsServer} wsServer
22
+ * @returns
23
+ */
24
+ const sendToAppChannel = async ({
25
+ sender,
26
+ channel,
27
+ event = EVENTS.MESSAGE,
28
+ notification,
29
+ options,
30
+ node,
31
+ wsServer,
32
+ } = {}) => {
33
+ const { socketId, socketDid } = options || {};
34
+
35
+ const channelInfo = parseChannel(channel);
36
+
37
+ if (channelInfo.type !== CHANNEL_TYPE.APP) {
38
+ throw new Error('Cannot send message to non-app channel');
39
+ }
40
+
41
+ const nodeInfo = await node.getNodeInfo();
42
+
43
+ // get socket match
44
+ const socketFilters = {};
45
+ if (socketId) {
46
+ socketFilters.id = socketId;
47
+ }
48
+ if (socketDid) {
49
+ socketFilters[`channel.${channel}.authInfo.did`] = socketDid;
50
+ }
51
+
52
+ // validate
53
+
54
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
55
+ await validateNotification(notification);
56
+ }
57
+
58
+ // parse sender
59
+
60
+ const senderInfo = await ensureSender({ sender, node, nodeInfo });
61
+
62
+ if (channelInfo.appDid !== senderInfo.wallet.address) {
63
+ throw new Error('Cannot sent message to channel of other app');
64
+ }
65
+
66
+ // parse notification
67
+
68
+ const notifications = parseNotification(notification, senderInfo);
69
+
70
+ // send notification
71
+ notifications.forEach((data) => {
72
+ broadcast(wsServer, channel, event, data, { socketFilters });
73
+ });
74
+ };
75
+
76
+ const onAuthenticate = async ({ channel, node }) => {
77
+ const { appDid } = parseChannel(channel);
78
+
79
+ const exist = await node.hasBlocklet({ did: appDid });
80
+ if (!exist) {
81
+ throw new Error(`App does not exist: ${appDid}`);
82
+ }
83
+ };
84
+
85
+ const onJoin = async ({ socket, channel, wsServer }) => {
86
+ const { appDid } = parseChannel(channel);
87
+
88
+ const senderDid = get(socket, `channel.${channel}.authInfo.did`);
89
+
90
+ if (senderDid === appDid) {
91
+ return;
92
+ }
93
+
94
+ broadcast(
95
+ wsServer,
96
+ channel,
97
+ 'hi',
98
+ {
99
+ sender: {
100
+ socketId: socket.id,
101
+ did: senderDid,
102
+ },
103
+ },
104
+ {
105
+ socketFilters: {
106
+ [`channel.${channel}.authInfo.did`]: appDid,
107
+ },
108
+ }
109
+ );
110
+ };
111
+
112
+ const onMessage = () => {};
113
+
114
+ module.exports = { onAuthenticate, onJoin, onMessage, sendToAppChannel };
@@ -0,0 +1,147 @@
1
+ const {
2
+ validateReceiver,
3
+ validateNotification,
4
+ validateMessage,
5
+ } = require('@blocklet/sdk/lib/validators/notification');
6
+ const { NODE_MODES } = require('@abtnode/constant');
7
+
8
+ // eslint-disable-next-line global-require
9
+ const logger = require('@abtnode/logger')(`${require('../../../../package.json').name}:notification`);
10
+ const { ensureSender, parseNotification, broadcast, EVENTS } = require('../util');
11
+ const states = require('../../../state');
12
+
13
+ /**
14
+ *
15
+ * @param {{
16
+ * {{
17
+ * did: String
18
+ * token: String
19
+ * }} sender
20
+ * receiver: Array|String // user did
21
+ * notification: Array|Object
22
+ * options: Object
23
+ * node: ABTNode
24
+ * wsServer: WsServer
25
+ * }}
26
+ * @returns
27
+ */
28
+ const sendToDid = async ({ sender, receiver, notification, options, node, wsServer }) => {
29
+ const { keepForOfflineUser = true } = options || {};
30
+
31
+ // validate
32
+
33
+ await validateReceiver(receiver);
34
+
35
+ const nodeInfo = await node.getNodeInfo();
36
+
37
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
38
+ await validateNotification(notification);
39
+ }
40
+
41
+ // parse sender
42
+
43
+ const senderInfo = await ensureSender({ sender, node, nodeInfo });
44
+
45
+ // parse notification
46
+
47
+ const notifications = parseNotification(notification, senderInfo);
48
+
49
+ // parse receiver
50
+
51
+ const receivers = [].concat(receiver);
52
+
53
+ // send notification
54
+
55
+ receivers.forEach((did) => {
56
+ notifications.forEach((data) => {
57
+ broadcast(wsServer, did, EVENTS.MESSAGE, data, async (count) => {
58
+ if (count <= 0 && keepForOfflineUser) {
59
+ logger.info('Online client was not found', { userDid: did });
60
+ await states.message.insert({ did, event: EVENTS.MESSAGE, data });
61
+ }
62
+ });
63
+ });
64
+ });
65
+ };
66
+
67
+ const sendCachedMessages = async (wsServer, did) => {
68
+ try {
69
+ const messages = await states.message.find({ did });
70
+ if (!messages.length) {
71
+ return;
72
+ }
73
+
74
+ messages.forEach(({ did: channel, event, data }) => {
75
+ wsServer.broadcast(channel, event, data);
76
+ });
77
+
78
+ await states.message.remove({ did }, { multi: true });
79
+ } catch (error) {
80
+ logger.error('Error on sending cached messages', { error });
81
+ }
82
+ };
83
+
84
+ /**
85
+ * Receive message from channel
86
+ * @param {{
87
+ * {WsServer} wsServer
88
+ * {ABTNode} Node
89
+ * {string} topic
90
+ * {string} event
91
+ * {Object} payload
92
+ * }}
93
+ * @returns
94
+ */
95
+ const onMessage = async ({ channel: from, event = EVENTS.MESSAGE, payload: message, wsServer, node }) => {
96
+ if (event !== EVENTS.MESSAGE) {
97
+ throw new Error(`Invalid event. expect: "message". got: "${event}"`);
98
+ }
99
+
100
+ await validateMessage(message);
101
+
102
+ // validate receiver
103
+ const { receiver, ...data } = message;
104
+
105
+ const blocklet = await node.getBlocklet({ did: receiver.did, attachConfig: false });
106
+ if (!blocklet) {
107
+ const nodeInfo = await node.getNodeInfo();
108
+
109
+ // only throw error if receiver is a blocklet (not server)
110
+ if (nodeInfo.did !== receiver.did) {
111
+ throw new Error(`App is not installed in the server. receiver: ${receiver.did}`);
112
+ }
113
+ }
114
+
115
+ logger.info('send message to blocklet', { sender: from, receiver: receiver.did });
116
+ wsServer.broadcast(receiver.did, event, {
117
+ ...data,
118
+ sender: {
119
+ did: from,
120
+ },
121
+ });
122
+ };
123
+
124
+ const onJoin = async ({ channel: did, payload, wsServer, node }) => {
125
+ await sendCachedMessages(wsServer, did);
126
+
127
+ if (payload.message) {
128
+ await onMessage({
129
+ channel: did,
130
+ payload: payload.message,
131
+ wsServer,
132
+ node,
133
+ });
134
+ }
135
+ };
136
+
137
+ const onAuthenticate = async ({ channel, did, payload }) => {
138
+ if (did !== channel) {
139
+ throw new Error(`verified did and channel does not match. did: ${did}, channel: ${channel}`);
140
+ }
141
+
142
+ if (payload.message) {
143
+ await validateMessage(payload.message);
144
+ }
145
+ };
146
+
147
+ module.exports = { onAuthenticate, onJoin, onMessage, sendToDid };
@@ -0,0 +1,18 @@
1
+ const { CHANNEL_TYPE, parseChannel } = require('@blocklet/meta/lib/channel');
2
+
3
+ const AppChannel = require('./app-channel');
4
+ const DidChannel = require('./did-channel');
5
+
6
+ const getHooksByChannel = (channel) => {
7
+ const { type } = parseChannel(channel);
8
+ if (type === CHANNEL_TYPE.DID) {
9
+ return DidChannel;
10
+ }
11
+ if (type === CHANNEL_TYPE.APP) {
12
+ return AppChannel;
13
+ }
14
+
15
+ throw new Error('Unknown channel type');
16
+ };
17
+
18
+ module.exports = getHooksByChannel;
@@ -1,26 +1,19 @@
1
1
  /* eslint-disable arrow-parens */
2
2
  const JWT = require('@arcblock/jwt');
3
3
  const { WsServer } = require('@arcblock/ws');
4
- const uuid = require('uuid');
5
4
  const Cron = require('@abtnode/cron');
6
- const {
7
- validateNotification,
8
- validateReceiver,
9
- validateMessage,
10
- } = require('@blocklet/sdk/lib/validators/notification');
11
5
  // eslint-disable-next-line global-require
12
6
  const logger = require('@abtnode/logger')(`${require('../../../package.json').name}:notification`);
13
- const { getTeamInfo } = require('@abtnode/auth/lib/auth');
14
- const { NODE_MODES } = require('@abtnode/constant');
15
7
 
16
8
  const states = require('../../state');
17
9
  const { PREFIXES } = require('../../util/constants');
18
10
 
19
- const getDid = (jwt) => jwt.iss.replace(/^did:abt:/, '');
11
+ const { sendToAppChannel } = require('./channel/app-channel');
12
+ const { sendToDid } = require('./channel/did-channel');
13
+ const getHooksByChannel = require('./channel/get-hooks-by-channel');
14
+ const { getDid } = require('./util');
20
15
 
21
- // todo: move to notification dir
22
-
23
- const authenticate = (req, cb) => {
16
+ const authenticateConnect = (req, cb) => {
24
17
  const { searchParams } = new URL(req.url, `http://${req.headers.host || 'unknown'}`);
25
18
  const token = searchParams.get('token');
26
19
  const pk = searchParams.get('pk');
@@ -38,97 +31,7 @@ const authenticate = (req, cb) => {
38
31
  cb(null, { did: getDid(JWT.decode(token)) });
39
32
  };
40
33
 
41
- /**
42
- *
43
- * @param {ABTNode} node
44
- * @param {Object} payload
45
- * {Object} sender: blocklet
46
- * {String} sender.did: blocklet did
47
- * {String} sender.token: for the verification
48
- * {Array|String} receiver: user did
49
- * {Array|Object} notification
50
- * @returns
51
- */
52
- const onSendToUser = async (node, payload, wsServer) => {
53
- const { sender, receiver, notification, options } = payload;
54
- const { keepForOfflineUser = true } = options || {};
55
-
56
- await validateReceiver(receiver);
57
-
58
- const nodeInfo = await node.getNodeInfo();
59
-
60
- if (nodeInfo.mode !== NODE_MODES.DEBUG) {
61
- await validateNotification(notification);
62
- }
63
-
64
- let senderInfo;
65
- try {
66
- senderInfo = await getTeamInfo({ node, nodeInfo, teamDid: sender.did });
67
- } catch (err) {
68
- if (err.message === 'Blocklet state must be an object') {
69
- err.message = `Sender blocklet does not exist: ${sender.did}`;
70
- }
71
- throw err;
72
- }
73
-
74
- const { wallet, name } = senderInfo;
75
- if (!JWT.verify(sender.token, wallet.publicKey)) {
76
- throw new Error(`Invalid authentication token for sender blocklet: ${sender.did}`);
77
- }
78
-
79
- if (sender.appDid !== wallet.address) {
80
- throw new Error(`Invalid app did, expected: ${wallet.address}, actual: ${sender.appDid}`);
81
- }
82
-
83
- // parse notifications
84
-
85
- const notifications = [].concat(notification);
86
-
87
- notifications.forEach((x) => {
88
- x.id = uuid.v4();
89
- x.sender = {
90
- did: sender.appDid,
91
- name,
92
- };
93
- x.createdAt = new Date();
94
- });
95
-
96
- // parse receivers
97
-
98
- const receivers = [].concat(receiver);
99
-
100
- // send notification
101
-
102
- const EVENT_NAME = 'message';
103
- const createTask = async (did, data) => {
104
- try {
105
- const count = await new Promise((resolve) => {
106
- wsServer.broadcast(did, EVENT_NAME, data, ({ count: c } = {}) => {
107
- resolve(c);
108
- });
109
- });
110
-
111
- if (count <= 0 && keepForOfflineUser) {
112
- logger.info('Online client was not found', { userDid: did });
113
- await states.message.insert({ did, event: EVENT_NAME, data });
114
- }
115
- } catch (error) {
116
- logger.error('Failed on broadcast message', { error });
117
- }
118
- };
119
-
120
- const tasks = [];
121
- receivers.forEach((did) => {
122
- notifications.forEach((data) => {
123
- tasks.push(createTask(did, data));
124
- });
125
- });
126
-
127
- // FIXME: Enhance task reliability. e.g. resend after timeout or error; add message status: sent, received, staged
128
- await Promise.allSettled(tasks);
129
- };
130
-
131
- const verify = async ({ topic, payload }) => {
34
+ const authenticateJoinChannel = async ({ topic: channel, payload, node }) => {
132
35
  const did = getDid(JWT.decode(payload.token));
133
36
 
134
37
  // Support the web wallet to continue to run for one day
@@ -143,76 +46,45 @@ const verify = async ({ topic, payload }) => {
143
46
  throw new Error(`verify did failed: ${did}`);
144
47
  }
145
48
 
146
- if (did !== topic) {
147
- throw new Error(`verified did and topic does not match. did: ${did}, topic: ${topic}`);
148
- }
149
-
150
- if (payload.message) {
151
- await validateMessage(payload.message);
152
- }
153
- };
154
-
155
- const receiveMessage = async ({ topic, event, payload, wsServer, node }) => {
156
- if (event !== 'message') {
157
- throw new Error(`Invalid event. expect: "message". got: "${event}"`);
158
- }
159
-
160
- await validateMessage(payload);
161
-
162
- const { receiver, ...data } = payload;
163
-
164
- const blocklet = await node.getBlocklet({ did: receiver.did, attachConfig: false });
165
- if (!blocklet) {
166
- const nodeInfo = await node.getNodeInfo();
49
+ const hooks = getHooksByChannel(channel);
50
+ await hooks.onAuthenticate({ channel, did, payload, node });
167
51
 
168
- // only log error if receiver is a blocklet (not server)
169
- if (nodeInfo.did !== receiver.did) {
170
- throw new Error(`App is not installed in the server. receiver: ${receiver.did}`);
171
- }
172
- }
52
+ const authInfo = { did };
173
53
 
174
- logger.info('send message to blocklet', { sender: topic, receiver });
175
- wsServer.broadcast(payload.receiver.did, 'message', {
176
- ...data,
177
- sender: {
178
- did: topic,
179
- },
180
- });
54
+ return authInfo;
181
55
  };
182
56
 
183
- const sendCachedMessages = async (wsServer, payload) => {
184
- try {
185
- const did = getDid(JWT.decode(payload.token));
57
+ const postJoinChannel = async ({ socket, topic: channel, payload, wsServer, node }) => {
58
+ logger.info('Subscribe notification success', { channel });
186
59
 
187
- const messages = await states.message.find({ did });
188
- if (!messages.length) {
189
- return;
190
- }
191
-
192
- messages.forEach(({ did: d, event, data }) => {
193
- wsServer.broadcast(d, event, data);
194
- });
60
+ const hooks = getHooksByChannel(channel);
61
+ await hooks.onJoin({ socket, channel, payload, wsServer, node });
62
+ };
195
63
 
196
- await states.message.remove({ did }, { multi: true });
197
- } catch (error) {
198
- logger.error('Error on sending cached messages', { error });
199
- }
64
+ /**
65
+ * Receive message from channel
66
+ * @param {{
67
+ * wsServer: WsServer
68
+ * node: ABTNode
69
+ * topic: String
70
+ * event: String
71
+ * payload: Object
72
+ * }}
73
+ * @returns
74
+ */
75
+ const receiveMessage = async ({ socket, topic: channel, event, payload, wsServer, node }) => {
76
+ const hooks = getHooksByChannel(channel);
77
+ await hooks.onMessage({ socket, channel, event, payload, wsServer, node });
200
78
  };
201
79
 
202
80
  const init = ({ node }) => {
203
81
  // Create ws server
204
82
  const wsServer = new WsServer({
205
83
  logger,
206
- authenticate,
84
+ authenticate: authenticateConnect,
207
85
  hooks: {
208
- preJoinChannel: (param) => verify({ ...param, wsServer, node }),
209
- postJoinChannel: async ({ topic, payload }) => {
210
- logger.info('Subscribe notification success', { account: topic });
211
- await sendCachedMessages(wsServer, payload);
212
- if (payload.message) {
213
- await receiveMessage({ topic, event: 'message', payload: payload.message, wsServer, node });
214
- }
215
- },
86
+ authenticateJoinChannel: (param) => authenticateJoinChannel({ ...param, wsServer, node }),
87
+ postJoinChannel: (param) => postJoinChannel({ ...param, wsServer, node }),
216
88
  receiveMessage: (param) => receiveMessage({ ...param, wsServer, node }),
217
89
  },
218
90
  });
@@ -234,9 +106,9 @@ const init = ({ node }) => {
234
106
  });
235
107
  }
236
108
 
237
- const sendToUser = async (req, res) => {
109
+ const onSendToUser = async (req, res) => {
238
110
  try {
239
- await onSendToUser(node, req.body.data, wsServer);
111
+ await sendToDid({ ...req.body.data, node, wsServer });
240
112
  res.status(200).send('');
241
113
  } catch (error) {
242
114
  logger.error('Send message to user failed', { error });
@@ -245,16 +117,40 @@ const init = ({ node }) => {
245
117
  }
246
118
  };
247
119
 
120
+ const onSendToAppChannel = async (req, res) => {
121
+ try {
122
+ await sendToAppChannel({ ...req.body.data, node, wsServer });
123
+ res.status(200).send('');
124
+ } catch (error) {
125
+ logger.error('Send message to channel failed', { error });
126
+ res.statusMessage = error.message;
127
+ res.status(400).send(error.message);
128
+ }
129
+ };
130
+
248
131
  // mount
249
132
  return {
250
133
  sendToUser: {
251
134
  attach: (app) => {
252
135
  PREFIXES.forEach((prefix) => {
253
- app.post(`${prefix}/api/sendToUser`, sendToUser);
136
+ app.post(`${prefix}/api/send-to-user`, onSendToUser);
137
+
138
+ // backward compatible
139
+ app.post(`${prefix}/api/sendToUser`, onSendToUser);
254
140
  });
255
141
  },
256
- exec: (data) => onSendToUser(node, data, wsServer),
142
+ exec: (data) => sendToDid({ ...data, node, wsServer }),
257
143
  },
144
+
145
+ sendToAppChannel: {
146
+ attach: (app) => {
147
+ PREFIXES.forEach((prefix) => {
148
+ app.post(`${prefix}/api/send-to-app-channel`, onSendToAppChannel);
149
+ });
150
+ },
151
+ exec: (data) => sendToAppChannel({ ...data, node, wsServer }),
152
+ },
153
+
258
154
  attach: (wsRouter) => {
259
155
  PREFIXES.forEach((prefix) => {
260
156
  wsRouter.use(`${prefix}/websocket`, wsServer.onConnect.bind(wsServer));