@abtnode/blocklet-services 1.6.23

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.
Files changed (76) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +84 -0
  3. package/build/asset-manifest.json +36 -0
  4. package/build/favicon.ico +0 -0
  5. package/build/images/blocklet.png +0 -0
  6. package/build/index.html +1 -0
  7. package/build/manifest.json +15 -0
  8. package/build/precache-manifest.b61da416864625d7da6840b710c5fd05.js +150 -0
  9. package/build/service-worker.js +39 -0
  10. package/build/static/css/2.73d52aac.chunk.css +2 -0
  11. package/build/static/css/2.73d52aac.chunk.css.map +1 -0
  12. package/build/static/js/2.adca7e30.chunk.js +3 -0
  13. package/build/static/js/2.adca7e30.chunk.js.LICENSE.txt +113 -0
  14. package/build/static/js/2.adca7e30.chunk.js.map +1 -0
  15. package/build/static/js/3.9cf8cde1.chunk.js +3 -0
  16. package/build/static/js/3.9cf8cde1.chunk.js.LICENSE.txt +19 -0
  17. package/build/static/js/3.9cf8cde1.chunk.js.map +1 -0
  18. package/build/static/js/4.3bcd2384.chunk.js +2 -0
  19. package/build/static/js/4.3bcd2384.chunk.js.map +1 -0
  20. package/build/static/js/5.a0b39d30.chunk.js +2 -0
  21. package/build/static/js/5.a0b39d30.chunk.js.map +1 -0
  22. package/build/static/js/6.91e31d64.chunk.js +2 -0
  23. package/build/static/js/6.91e31d64.chunk.js.map +1 -0
  24. package/build/static/js/7.aa6a9bc1.chunk.js +2 -0
  25. package/build/static/js/7.aa6a9bc1.chunk.js.map +1 -0
  26. package/build/static/js/main.0fef8e40.chunk.js +2 -0
  27. package/build/static/js/main.0fef8e40.chunk.js.map +1 -0
  28. package/build/static/js/runtime-main.8d9f8ef7.js +2 -0
  29. package/build/static/js/runtime-main.8d9f8ef7.js.map +1 -0
  30. package/build/static/media/rubik-all-400-normal.c2324103.woff +0 -0
  31. package/build/static/media/rubik-all-500-normal.000c3408.woff +0 -0
  32. package/build/static/media/rubik-all-600-normal.30df2fd0.woff +0 -0
  33. package/build/static/media/rubik-cyrillic-400-normal.aa383bbd.woff2 +0 -0
  34. package/build/static/media/rubik-cyrillic-500-normal.27a1ebd4.woff2 +0 -0
  35. package/build/static/media/rubik-cyrillic-600-normal.74d5cdba.woff2 +0 -0
  36. package/build/static/media/rubik-cyrillic-ext-400-normal.b8647475.woff2 +0 -0
  37. package/build/static/media/rubik-cyrillic-ext-500-normal.860932d9.woff2 +0 -0
  38. package/build/static/media/rubik-cyrillic-ext-600-normal.942f240f.woff2 +0 -0
  39. package/build/static/media/rubik-hebrew-400-normal.2c9e3c2a.woff2 +0 -0
  40. package/build/static/media/rubik-hebrew-500-normal.08d6e502.woff2 +0 -0
  41. package/build/static/media/rubik-hebrew-600-normal.bfa32e44.woff2 +0 -0
  42. package/build/static/media/rubik-latin-400-normal.b8fd53c5.woff2 +0 -0
  43. package/build/static/media/rubik-latin-500-normal.595f1a98.woff2 +0 -0
  44. package/build/static/media/rubik-latin-600-normal.5f06934f.woff2 +0 -0
  45. package/build/static/media/rubik-latin-ext-400-normal.3c5c378e.woff2 +0 -0
  46. package/build/static/media/rubik-latin-ext-500-normal.5663c731.woff2 +0 -0
  47. package/build/static/media/rubik-latin-ext-600-normal.ff159cb8.woff2 +0 -0
  48. package/build/static/media/ubuntu-mono-all-400-normal.e39678a8.woff +0 -0
  49. package/build/static/media/ubuntu-mono-cyrillic-400-normal.e09900e7.woff2 +0 -0
  50. package/build/static/media/ubuntu-mono-cyrillic-ext-400-normal.762d0b06.woff2 +0 -0
  51. package/build/static/media/ubuntu-mono-greek-400-normal.81c98658.woff2 +0 -0
  52. package/build/static/media/ubuntu-mono-greek-ext-400-normal.1af17851.woff2 +0 -0
  53. package/build/static/media/ubuntu-mono-latin-400-normal.469ee478.woff2 +0 -0
  54. package/build/static/media/ubuntu-mono-latin-ext-400-normal.e28ff028.woff2 +0 -0
  55. package/lib/cache.js +25 -0
  56. package/lib/index.js +443 -0
  57. package/lib/mount.js +52 -0
  58. package/lib/util.js +17 -0
  59. package/package.json +119 -0
  60. package/services/auth/index.js +290 -0
  61. package/services/auth/libs/auth.js +76 -0
  62. package/services/auth/libs/jwt.js +73 -0
  63. package/services/auth/meta.json +61 -0
  64. package/services/auth/routes/blocklet-info.js +33 -0
  65. package/services/auth/routes/env.js +33 -0
  66. package/services/auth/routes/invite.js +80 -0
  67. package/services/auth/routes/issue-passport.js +58 -0
  68. package/services/auth/routes/login.js +230 -0
  69. package/services/auth/routes/lost-passport-issue.js +5 -0
  70. package/services/auth/routes/lost-passport-list.js +6 -0
  71. package/services/auth/routes/notification.js +257 -0
  72. package/services/auth/routes/passport.js +18 -0
  73. package/services/auth/routes/session.js +27 -0
  74. package/services/auth/state/index.js +30 -0
  75. package/services/auth/state/message.js +32 -0
  76. package/services/auth/util/index.js +35 -0
@@ -0,0 +1,58 @@
1
+ const joinUrl = require('url-join');
2
+ const {
3
+ createIssuePassportRequest,
4
+ handleIssuePassportResponse,
5
+ checkWalletVersion,
6
+ beforeIssuePassportRequest,
7
+ } = require('@abtnode/auth/lib/auth');
8
+
9
+ module.exports = function createRoutes(node, authenticator, login, opts) {
10
+ return {
11
+ action: 'issue-passport',
12
+
13
+ onStart: async ({ extraParams, req }) => {
14
+ const { locale = 'en', id } = extraParams;
15
+ const teamDid = req.headers['x-blocklet-did'];
16
+
17
+ await beforeIssuePassportRequest({ node, teamDid, locale, id });
18
+ },
19
+
20
+ claims: {
21
+ signature: async ({ extraParams, context: { request, abtwallet } }) => {
22
+ const { id, locale } = extraParams;
23
+ checkWalletVersion({ abtwallet, locale });
24
+ const nodeInfo = await node.getNodeInfo();
25
+ const teamDid = request.headers['x-blocklet-did'];
26
+
27
+ return createIssuePassportRequest({
28
+ node,
29
+ nodeInfo,
30
+ teamDid,
31
+ id,
32
+ locale,
33
+ });
34
+ },
35
+ },
36
+
37
+ onAuth: async ({ userDid, userPk, claims, extraParams, req, baseUrl }) => {
38
+ const { locale, id } = extraParams;
39
+ const nodeInfo = await node.getNodeInfo();
40
+ const teamDid = req.headers['x-blocklet-did'];
41
+ const statusEndpointBaseUrl = joinUrl(baseUrl, opts.prefix);
42
+ const endpoint = baseUrl;
43
+
44
+ return handleIssuePassportResponse({
45
+ node,
46
+ nodeInfo,
47
+ teamDid,
48
+ userDid,
49
+ userPk,
50
+ id,
51
+ locale,
52
+ claims,
53
+ statusEndpointBaseUrl,
54
+ endpoint,
55
+ });
56
+ },
57
+ };
58
+ };
@@ -0,0 +1,230 @@
1
+ /* eslint-disable arrow-parens */
2
+ const get = require('lodash/get');
3
+ const joinUrl = require('url-join');
4
+ const {
5
+ messages,
6
+ getVCFromClaims,
7
+ checkWalletVersion,
8
+ getUser,
9
+ getPassportStatusEndpoint,
10
+ validatePassportStatus,
11
+ } = require('@abtnode/auth/lib/auth');
12
+ const { ROLES, VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT } = require('@abtnode/constant');
13
+ const {
14
+ createPassportVC,
15
+ createPassport,
16
+ validatePassport,
17
+ isUserPassportRevoked,
18
+ upsertToPassports,
19
+ getRoleFromLocalPassport,
20
+ getRoleFromExternalPassport,
21
+ createUserPassport,
22
+ } = require('@abtnode/auth/lib/passport');
23
+ const logger = require('@abtnode/logger')(require('../meta.json').name);
24
+
25
+ const vcTypes = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
26
+
27
+ /**
28
+ * @returns {Array} config
29
+ * @returns {Boolean} config[0] is invited user only
30
+ * @returns {String} config[1] default role
31
+ * @returns {Boolean} config[2] issue passport
32
+ */
33
+ const isInvitedUserOnly = async (invitedUserOnly, node, teamDid) => {
34
+ const count = await node.getUsersCount({ teamDid });
35
+ if (invitedUserOnly === 'not-first') {
36
+ return count > 0 ? [true] : [false, ROLES.OWNER, true];
37
+ }
38
+
39
+ if (invitedUserOnly === true || invitedUserOnly === 'yes') {
40
+ return [true];
41
+ }
42
+
43
+ return [false, ROLES.GUEST];
44
+ };
45
+
46
+ module.exports = function createRoutes(node, authenticator, login, opts) {
47
+ return {
48
+ action: 'login',
49
+ claims: {
50
+ profile: async ({ extraParams, context }) => {
51
+ const { locale } = extraParams;
52
+
53
+ const config = await context.request.getServiceConfig();
54
+ const profileFields = get(config, 'profileFields');
55
+
56
+ return {
57
+ fields: profileFields || ['fullName', 'avatar'],
58
+ description: messages.description[locale],
59
+ };
60
+ },
61
+
62
+ verifiableCredential: async ({ context, extraParams: { locale } }) => {
63
+ const { request, abtwallet } = context;
64
+ const { wallet, did: teamDid } = await request.getBlockletInfo();
65
+
66
+ checkWalletVersion({ abtwallet, locale });
67
+
68
+ const blocklet = await request.getBlocklet();
69
+ const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
70
+ const trustedIssuers = [wallet.address, ...trustedPassports].filter(Boolean);
71
+
72
+ const config = (await request.getServiceConfig()) || {};
73
+ const [invitedUserOnly] = await isInvitedUserOnly(config.invitedUserOnly, node, teamDid);
74
+
75
+ return {
76
+ description: messages.requestPassport[locale],
77
+ item: vcTypes,
78
+ trustedIssuers,
79
+ optional: !invitedUserOnly,
80
+ };
81
+ },
82
+ },
83
+
84
+ // eslint-disable-next-line consistent-return
85
+ onAuth: async ({ claims, challenge, userDid, userPk, token, storage, extraParams, req, baseUrl }) => {
86
+ const { locale } = extraParams;
87
+ const blocklet = await req.getBlocklet();
88
+ const { wallet, name, passportColor, did: teamDid } = await req.getBlockletInfo();
89
+ const teamAppDid = wallet.address;
90
+
91
+ // check user approved
92
+ const user = await getUser(node, teamDid, userDid);
93
+ if (user && !user.approved) {
94
+ throw new Error(messages.notAllowed[locale]);
95
+ }
96
+
97
+ // Get passport
98
+ const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
99
+ const trustedIssuers = [teamAppDid, ...trustedPassports].filter(Boolean);
100
+ let { vc } = await getVCFromClaims({
101
+ claims,
102
+ challenge,
103
+ trustedIssuers,
104
+ vcTypes,
105
+ locale,
106
+ });
107
+
108
+ const config = (await req.getServiceConfig()) || {};
109
+ const [invitedUserOnly, defaultRole, issuePassport] = await isInvitedUserOnly(
110
+ config.invitedUserOnly,
111
+ node,
112
+ teamDid
113
+ );
114
+
115
+ if (invitedUserOnly && !vc) {
116
+ throw new Error(messages.missingCredentialClaim[locale]);
117
+ }
118
+
119
+ // issue passport for the first login user in a invite-only team
120
+ if (issuePassport) {
121
+ logger.info('issue passport to user at the login workflow', { role: defaultRole });
122
+ vc = createPassportVC({
123
+ issuerName: name,
124
+ issuerWallet: wallet,
125
+ ownerDid: userDid,
126
+ passport: await createPassport({
127
+ name: defaultRole,
128
+ node,
129
+ teamDid,
130
+ locale,
131
+ endpoint: baseUrl,
132
+ }),
133
+ endpoint: getPassportStatusEndpoint({
134
+ baseUrl: joinUrl(baseUrl, opts.prefix),
135
+ userDid,
136
+ teamDid,
137
+ }),
138
+ ownerProfile: user,
139
+ preferredColor: passportColor,
140
+ });
141
+ }
142
+
143
+ // Get user passport from vc
144
+ let passport = vc ? createUserPassport(vc) : null;
145
+ if (user && passport && isUserPassportRevoked(user, passport)) {
146
+ throw new Error(messages.passportRevoked[locale](name));
147
+ }
148
+
149
+ // Get role
150
+ let role = ROLES.GUEST;
151
+ if (vc) {
152
+ await validatePassport(get(vc, 'credentialSubject.passport'));
153
+ const issuerId = get(vc, 'issuer.id');
154
+ if (issuerId === teamAppDid) {
155
+ role = getRoleFromLocalPassport(get(vc, 'credentialSubject.passport'));
156
+ } else {
157
+ // map external passport to local role
158
+ const { mappings = [] } = (blocklet.trustedPassports || []).find((x) => x.issuerDid === issuerId) || {};
159
+ role = await getRoleFromExternalPassport({
160
+ passport: get(vc, 'credentialSubject.passport'),
161
+ node,
162
+ teamDid,
163
+ locale,
164
+ mappings,
165
+ });
166
+
167
+ // check status of external passport if passport has an endpoint
168
+ const endpoint = get(vc, 'credentialStatus.id');
169
+ if (endpoint) {
170
+ await validatePassportStatus({ vcId: vc.id, endpoint, locale });
171
+ }
172
+ }
173
+ }
174
+
175
+ // Recreate passport with correct role
176
+ passport = vc ? createUserPassport(vc, { role }) : null;
177
+
178
+ // Update profile
179
+ try {
180
+ const profile = claims.find((x) => x.type === 'profile');
181
+ if (user) {
182
+ // Update user
183
+ await node.updateUser({
184
+ teamDid,
185
+ user: {
186
+ ...profile,
187
+ did: userDid,
188
+ pk: userPk,
189
+ locale,
190
+ passports: upsertToPassports(user.passports || [], passport),
191
+ lastLoginAt: new Date().toISOString(),
192
+ },
193
+ });
194
+ } else {
195
+ // Create user
196
+ await node.addUser({
197
+ teamDid,
198
+ user: {
199
+ ...profile,
200
+ did: userDid,
201
+ pk: userPk,
202
+ approved: true,
203
+ locale,
204
+ passports: [passport].filter(Boolean),
205
+ firstLoginAt: new Date().toISOString(),
206
+ lastLoginAt: new Date().toISOString(),
207
+ },
208
+ });
209
+ }
210
+
211
+ // Generate new session token that client can save to localStorage
212
+ const loginToken = await login(userDid, { passport, role });
213
+ await storage.update(token, { did: userDid, loginToken });
214
+ logger.info('login.success', { userDid, role });
215
+
216
+ // issue passport for the first login user in a invite-only team
217
+ if (issuePassport) {
218
+ return {
219
+ disposition: 'attachment',
220
+ type: 'VerifiableCredential',
221
+ data: vc,
222
+ };
223
+ }
224
+ } catch (err) {
225
+ logger.error('login.error', { error: err, userDid });
226
+ throw new Error(err.message);
227
+ }
228
+ },
229
+ };
230
+ };
@@ -0,0 +1,5 @@
1
+ const { createLostPassportIssueRoute, TEAM_TYPES } = require('@abtnode/auth/lib/lost-passport');
2
+
3
+ module.exports = function createRoutes(node, authenticator, login, opts) {
4
+ return createLostPassportIssueRoute({ node, type: TEAM_TYPES.BLOCKLET, authServicePrefix: opts.prefix });
5
+ };
@@ -0,0 +1,6 @@
1
+ const { createLostPassportListRoute } = require('@abtnode/auth/lib/lost-passport');
2
+
3
+ // eslint-disable-next-line no-unused-vars
4
+ module.exports = function createRoutes(node, authenticator, login, opts) {
5
+ return createLostPassportListRoute({ node, type: 'blocklet' });
6
+ };
@@ -0,0 +1,257 @@
1
+ /* eslint-disable arrow-parens */
2
+ const JWT = require('@arcblock/jwt');
3
+ const { WsServer } = require('@arcblock/ws');
4
+ const uuid = require('uuid');
5
+ const Cron = require('@abtnode/cron');
6
+ const {
7
+ validateNotification,
8
+ validateReceiver,
9
+ validateMessage,
10
+ } = require('@blocklet/sdk/lib/validators/notification');
11
+ // eslint-disable-next-line global-require
12
+ const logger = require('@abtnode/logger')(`${require('../meta.json').name}:notification`);
13
+ const { getTeamInfo } = require('@abtnode/auth/lib/auth');
14
+ const { NODE_MODES } = require('@abtnode/constant');
15
+
16
+ const states = require('../state');
17
+ const { getBlockletLogo } = require('../util');
18
+
19
+ const getDid = (jwt) => jwt.iss.replace(/^did:abt:/, '');
20
+
21
+ const authenticate = (req, cb) => {
22
+ const { searchParams } = new URL(req.url, `http://${req.headers.host || 'unknown'}`);
23
+ const token = searchParams.get('token');
24
+ const pk = searchParams.get('pk');
25
+
26
+ if (!token) {
27
+ cb(new Error('token not found'), null);
28
+ return;
29
+ }
30
+
31
+ if (!JWT.verify(token, pk)) {
32
+ cb(new Error('token verify failed'));
33
+ return;
34
+ }
35
+
36
+ cb(null, { did: getDid(JWT.decode(token)) });
37
+ };
38
+
39
+ /**
40
+ *
41
+ * @param {ABTNode} node
42
+ * @param {Object} payload
43
+ * {Object} sender: blocklet
44
+ * {String} sender.did: blocklet did
45
+ * {String} sender.token: for the verification
46
+ * {Array|String} receiver: user did
47
+ * {Array|Object} notification
48
+ * @returns
49
+ */
50
+ const onSendToUser = async (node, payload, wsServer) => {
51
+ const { sender, receiver, notification } = payload;
52
+
53
+ await validateReceiver(receiver);
54
+
55
+ const nodeInfo = await node.getNodeInfo();
56
+
57
+ if (nodeInfo.mode !== NODE_MODES.DEBUG) {
58
+ await validateNotification(notification);
59
+ }
60
+
61
+ let senderInfo;
62
+ try {
63
+ senderInfo = await getTeamInfo({ node, nodeInfo, teamDid: sender.did });
64
+ } catch (err) {
65
+ if (err.message === 'Blocklet state must be an object') {
66
+ err.message = `Sender blocklet does not exist: ${sender.did}`;
67
+ }
68
+ throw err;
69
+ }
70
+
71
+ const { wallet, name, type: senderType } = senderInfo;
72
+ if (!JWT.verify(sender.token, wallet.publicKey)) {
73
+ throw new Error(`Invalid authentication token for sender blocklet: ${sender.did}`);
74
+ }
75
+
76
+ if (sender.appDid !== wallet.address) {
77
+ throw new Error(`Invalid app did, expected: ${wallet.address}, actual: ${sender.appDid}`);
78
+ }
79
+
80
+ let blocklet;
81
+ if (senderType === 'blocklet') {
82
+ blocklet = await node.getBlocklet({ did: sender.did, attachRuntimeInfo: false });
83
+ }
84
+
85
+ // parse notifications
86
+
87
+ const notifications = [].concat(notification);
88
+
89
+ notifications.forEach((x) => {
90
+ x.id = uuid.v4();
91
+ x.sender = {
92
+ did: sender.appDid,
93
+ name,
94
+ icon: senderType === 'blocklet' ? getBlockletLogo({ blocklet }) : null, // deprecated: did wallet will find icon locally by sender appDid
95
+ };
96
+ x.createdAt = new Date();
97
+ });
98
+
99
+ // parse receivers
100
+
101
+ const receivers = [].concat(receiver);
102
+
103
+ // send notification
104
+
105
+ const EVENT_NAME = 'message';
106
+ const createTask = async (did, data) => {
107
+ try {
108
+ await new Promise((resolve, reject) => {
109
+ try {
110
+ wsServer.broadcast(did, EVENT_NAME, data, ({ count } = {}) => {
111
+ if (count <= 0) {
112
+ logger.info('Online client for the user was not found', { userDid: did });
113
+ reject(new Error('Online client for the user was not found'));
114
+ } else {
115
+ resolve();
116
+ }
117
+ });
118
+ } catch (error) {
119
+ logger.error('Failed on broadcast message', { error });
120
+ reject(error);
121
+ }
122
+ });
123
+ } catch {
124
+ // failed on broadcast or online client not found
125
+ await states.message.insert({ did, event: EVENT_NAME, data });
126
+ }
127
+ };
128
+
129
+ const tasks = [];
130
+ receivers.forEach((did) => {
131
+ notifications.forEach((data) => {
132
+ tasks.push(createTask(did, data));
133
+ });
134
+ });
135
+
136
+ // FIXME: Enhance task reliability. e.g. resend after timeout or error; add message status: sent, received, staged
137
+ await Promise.allSettled(tasks);
138
+ };
139
+
140
+ const verify = async ({ topic, payload }) => {
141
+ const did = getDid(JWT.decode(payload.token));
142
+
143
+ // Support the web wallet to continue to run for one day
144
+ //
145
+ // if abtnode is restarted when there is a wallet wallet connection
146
+ // then the web wallet will auto reconnect to the abtnode and auto rejoin the channels
147
+ // the web wallet will reuse the JWT token when rejoin channel
148
+ // so we need to support token is valid for one day
149
+ const tolerance = 3600 * 24;
150
+
151
+ if (!JWT.verify(payload.token, payload.pk, { tolerance })) {
152
+ throw new Error(`verify did failed: ${did}`);
153
+ }
154
+
155
+ if (did !== topic) {
156
+ throw new Error(`verified did and topic does not match. did: ${did}, topic: ${topic}`);
157
+ }
158
+
159
+ if (payload.message) {
160
+ await validateMessage(payload.message);
161
+ }
162
+ };
163
+
164
+ const receiveMessage = async ({ topic, event, payload, wsServer, node }) => {
165
+ if (event !== 'message') {
166
+ throw new Error(`Invalid event. expect: "message". got: "${event}"`);
167
+ }
168
+
169
+ await validateMessage(payload);
170
+
171
+ const { receiver, ...data } = payload;
172
+
173
+ // FIXME: use node.getBlocklet() when support index by appID
174
+ const blocklet = await node.getBlocklet({ did: receiver.did, attachConfig: false });
175
+ if (!blocklet) {
176
+ throw new Error('App is not installed in the server');
177
+ }
178
+
179
+ logger.info('send message to blocklet', { sender: topic, receiver });
180
+ wsServer.broadcast(payload.receiver.did, 'message', {
181
+ ...data,
182
+ sender: {
183
+ did: topic,
184
+ },
185
+ });
186
+ };
187
+
188
+ const sendCachedMessages = async (wsServer, payload) => {
189
+ try {
190
+ const did = getDid(JWT.decode(payload.token));
191
+
192
+ const messages = await states.message.find({ did });
193
+ if (!messages.length) {
194
+ return;
195
+ }
196
+
197
+ messages.forEach(({ did: d, event, data }) => {
198
+ wsServer.broadcast(d, event, data);
199
+ });
200
+
201
+ await states.message.remove({ did }, { multi: true });
202
+ } catch (error) {
203
+ logger.error('Error on sending cached messages', { error });
204
+ }
205
+ };
206
+
207
+ const init = (node, httpRouter, wsRouter, opts) => {
208
+ // Create ws server
209
+ const wsServer = new WsServer({
210
+ logger,
211
+ authenticate,
212
+ hooks: {
213
+ preJoinChannel: (param) => verify({ ...param, wsServer, node }),
214
+ postJoinChannel: async ({ topic, payload }) => {
215
+ logger.info('Subscribe notification success', { account: topic });
216
+ await sendCachedMessages(wsServer, payload);
217
+ if (payload.message) {
218
+ await receiveMessage({ topic, event: 'message', payload: payload.message, wsServer, node });
219
+ }
220
+ },
221
+ receiveMessage: (param) => receiveMessage({ ...param, wsServer, node }),
222
+ },
223
+ });
224
+
225
+ // Let first worker process do something as master
226
+ if (process.env.NODE_APP_INSTANCE === '0') {
227
+ // cron
228
+ Cron.init({
229
+ jobs: [
230
+ {
231
+ name: 'prune messages',
232
+ time: '0 0 0 * * 1', // every Monday per week
233
+ fn: () => states.message.prune(),
234
+ },
235
+ ],
236
+ onError: (error, name) => {
237
+ logger.error('Run job failed', { name, error });
238
+ },
239
+ });
240
+ }
241
+
242
+ // mount
243
+ wsRouter.use(`${opts.prefix}/websocket`, wsServer.onConnect.bind(wsServer));
244
+
245
+ httpRouter.post(`${opts.prefix}/api/sendToUser`, async (req, res) => {
246
+ try {
247
+ await onSendToUser(node, req.body.data, wsServer);
248
+ res.status(200).send('');
249
+ } catch (error) {
250
+ logger.error('Send message to user failed', { error });
251
+ res.statusMessage = error.message;
252
+ res.status(400).send(error.message);
253
+ }
254
+ });
255
+ };
256
+
257
+ module.exports = { init };
@@ -0,0 +1,18 @@
1
+ const { getPassportStatus } = require('@abtnode/auth/lib/auth');
2
+
3
+ module.exports = {
4
+ init(server, node, opts) {
5
+ server.get(`${opts.prefix}/api/passport/status`, async (req, res) => {
6
+ const { vcId, userDid, locale } = req.query;
7
+ const teamDid = req.headers['x-blocklet-did'];
8
+
9
+ if (teamDid !== req.query.teamDid) {
10
+ throw new Error('teamDid is invalid');
11
+ }
12
+
13
+ const status = await getPassportStatus({ node, teamDid, userDid, vcId, locale });
14
+
15
+ res.json(status);
16
+ });
17
+ },
18
+ };
@@ -0,0 +1,27 @@
1
+ const nocache = require('nocache');
2
+
3
+ module.exports = {
4
+ init(server, node, opts) {
5
+ const prefix = `${opts.prefix}/api/did/session`;
6
+
7
+ const handler = async (req, res) => {
8
+ if (!req.user) {
9
+ res.json({ user: null });
10
+ return;
11
+ }
12
+
13
+ const user = { ...req.user };
14
+ if (user.role) {
15
+ const teamDid = req.getBlockletDid();
16
+ // FIXME: this code may have performance issue
17
+ const rbac = await node.getRBAC(teamDid);
18
+ user.permissions = await rbac.getScope(req.user.role);
19
+ }
20
+
21
+ res.json({ user });
22
+ };
23
+
24
+ server.get(prefix, nocache(), handler);
25
+ server.post(prefix, nocache(), handler);
26
+ },
27
+ };
@@ -0,0 +1,30 @@
1
+ const BaseState = require('@abtnode/core/lib/states/base');
2
+ const MessageState = require('./message');
3
+
4
+ const states = {};
5
+
6
+ // eslint-disable-next-line no-unused-vars
7
+ const init = (dataDir, options = {}) => {
8
+ const messageState = new MessageState(dataDir);
9
+
10
+ Object.assign(states, {
11
+ message: messageState,
12
+ });
13
+ };
14
+
15
+ module.exports = new Proxy(
16
+ {},
17
+ {
18
+ get(target, prop) {
19
+ if (prop === 'init') {
20
+ return init;
21
+ }
22
+
23
+ if (states[prop] instanceof BaseState) {
24
+ return states[prop];
25
+ }
26
+
27
+ throw new Error(`State ${String(prop)} may not be initialized`);
28
+ },
29
+ }
30
+ );
@@ -0,0 +1,32 @@
1
+ const BaseState = require('@abtnode/core/lib/states/base');
2
+ const logger = require('@abtnode/logger')('auth-service:states:message');
3
+
4
+ class MessageState extends BaseState {
5
+ constructor(baseDir, options = {}) {
6
+ super(baseDir, { filename: 'message.db', ...options });
7
+
8
+ this.db.ensureIndex({ fieldName: 'did' }, (error) => {
9
+ if (error) {
10
+ logger.error('ensure index failed', { error });
11
+ }
12
+ });
13
+ }
14
+
15
+ insert(doc, ...args) {
16
+ return super.insert(
17
+ {
18
+ ...doc,
19
+ created: Date.now(),
20
+ updated: Date.now(),
21
+ },
22
+ ...args
23
+ );
24
+ }
25
+
26
+ prune(time) {
27
+ const t = time || 30 * 24 * 60 * 60 * 1000; // 30 day
28
+ return this.remove({ created: { $lt: Date.now() - t } });
29
+ }
30
+ }
31
+
32
+ module.exports = MessageState;
@@ -0,0 +1,35 @@
1
+ const { BlockletSource } = require('@blocklet/meta/lib/constants');
2
+
3
+ const getLogoFromLogoUrl = (blocklet) => {
4
+ try {
5
+ if (blocklet.source === BlockletSource.registry) {
6
+ return new URL(blocklet.meta.logoUrl, blocklet.deployedFrom).href;
7
+ }
8
+ return blocklet.meta.logoUrl || null;
9
+ } catch (error) {
10
+ return null;
11
+ }
12
+ };
13
+
14
+ const getBlockletLogo = ({ baseUrl, blocklet }) => {
15
+ if (!baseUrl) {
16
+ return getLogoFromLogoUrl(blocklet);
17
+ }
18
+
19
+ let logo = `${baseUrl}/images/blocklet.png`;
20
+ try {
21
+ if (blocklet.meta.logoUrl) {
22
+ logo = getLogoFromLogoUrl(blocklet);
23
+ } else if (blocklet.meta.logo) {
24
+ logo = `${baseUrl}/blocklet/logo/${blocklet.meta.did}`;
25
+ }
26
+ } catch (err) {
27
+ // Do nothing
28
+ }
29
+
30
+ return logo;
31
+ };
32
+
33
+ module.exports = {
34
+ getBlockletLogo,
35
+ };