@abtnode/blocklet-services 1.16.5 → 1.16.6-beta-8be2fe37

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 (39) hide show
  1. package/api/libs/connect/session.js +85 -31
  2. package/api/routes/blocklet.js +24 -0
  3. package/api/routes/oauth.js +2 -2
  4. package/api/services/auth/connect/exchange-passport.js +58 -0
  5. package/api/services/auth/connect/login.js +2 -1
  6. package/api/services/auth/connect/pre-setup.js +2 -2
  7. package/api/services/auth/connect/receive-transfer-app-owner.js +385 -0
  8. package/api/services/auth/connect/transfer-app-owner.js +30 -0
  9. package/api/services/auth/index.js +6 -0
  10. package/api/services/oauth/index.js +30 -23
  11. package/build/asset-manifest.json +27 -21
  12. package/build/index.html +1 -1
  13. package/build/static/css/{204.1d1e88ad.chunk.css → 61.03a48b17.chunk.css} +1 -1
  14. package/build/static/js/162.58348adf.chunk.js +3 -0
  15. package/build/static/js/343.54712c04.chunk.js +2 -0
  16. package/build/static/js/359.c47779c2.chunk.js +2 -0
  17. package/build/static/js/371.ccef728f.chunk.js +2 -0
  18. package/build/static/js/42.a0526d16.chunk.js +2 -0
  19. package/build/static/js/547.f8830a9e.chunk.js +2 -0
  20. package/build/static/js/573.2052cb80.chunk.js +2 -0
  21. package/build/static/js/61.20fc6264.chunk.js +3 -0
  22. package/build/static/js/648.b5b229e5.chunk.js +3 -0
  23. package/build/static/js/716.874040d8.chunk.js +2 -0
  24. package/build/static/js/868.9c03fa86.chunk.js +2 -0
  25. package/build/static/js/main.88eb4897.js +3 -0
  26. package/package.json +32 -30
  27. package/build/static/js/204.df50af69.chunk.js +0 -3
  28. package/build/static/js/343.b31c2008.chunk.js +0 -2
  29. package/build/static/js/371.88127b62.chunk.js +0 -2
  30. package/build/static/js/573.2687bb44.chunk.js +0 -2
  31. package/build/static/js/648.7a2e44c8.chunk.js +0 -3
  32. package/build/static/js/712.9667cdcd.chunk.js +0 -3
  33. package/build/static/js/716.e1534c42.chunk.js +0 -2
  34. package/build/static/js/868.4d364267.chunk.js +0 -2
  35. package/build/static/js/main.3b61bb8b.js +0 -3
  36. /package/build/static/js/{712.9667cdcd.chunk.js.LICENSE.txt → 162.58348adf.chunk.js.LICENSE.txt} +0 -0
  37. /package/build/static/js/{204.df50af69.chunk.js.LICENSE.txt → 61.20fc6264.chunk.js.LICENSE.txt} +0 -0
  38. /package/build/static/js/{648.7a2e44c8.chunk.js.LICENSE.txt → 648.b5b229e5.chunk.js.LICENSE.txt} +0 -0
  39. /package/build/static/js/{main.3b61bb8b.js.LICENSE.txt → main.88eb4897.js.LICENSE.txt} +0 -0
@@ -9,6 +9,7 @@ const {
9
9
  validatePassportStatus,
10
10
  getPassportStatusEndpoint,
11
11
  getApplicationInfo,
12
+ verifyNFT,
12
13
  } = require('@abtnode/auth/lib/auth');
13
14
  const {
14
15
  NODE_SERVICES,
@@ -18,6 +19,7 @@ const {
18
19
  VC_TYPE_NODE_PASSPORT,
19
20
  WHO_CAN_ACCESS,
20
21
  WHO_CAN_ACCESS_PREFIX_ROLES,
22
+ MAIN_CHAIN_ENDPOINT,
21
23
  } = require('@abtnode/constant');
22
24
  const {
23
25
  validatePassport,
@@ -122,17 +124,20 @@ const validateRole = async ({ role, authConfig, locale, node, teamDid }) => {
122
124
  }
123
125
  };
124
126
 
127
+ const checkAppOwner = ({ role, blocklet, userDid, locale = 'en' }) => {
128
+ if (role === ROLES.OWNER && blocklet.settings.initialized && userDid !== blocklet.settings.owner.did) {
129
+ throw new Error(messages.notAppOwner[locale]);
130
+ }
131
+ };
132
+
125
133
  module.exports = {
126
134
  login: {
127
- onConnect: async ({ node, request, userDid, locale, passportId = '', componentId }) => {
135
+ onConnect: async ({ node, request, userDid, locale, passportId = '', componentId, action }) => {
128
136
  const blocklet = await request.getBlocklet();
129
137
  const config = await request.getServiceConfig(NODE_SERVICES.AUTH, { componentId });
130
138
  const { did: teamDid } = await request.getBlockletInfo();
131
139
 
132
140
  const profileFields = get(config, 'profileFields');
133
- const [invitedUserOnly] = config ? await isInvitedUserOnly(config, node, teamDid) : [false];
134
- const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
135
- const trustedIssuers = [...getBlockletAppIdList(blocklet), ...trustedPassports].filter(Boolean);
136
141
 
137
142
  const claims = {
138
143
  profile: {
@@ -140,16 +145,31 @@ module.exports = {
140
145
  description: messages.description[locale],
141
146
  items: profileFields || ['fullName', 'avatar'],
142
147
  },
143
- verifiableCredential: {
148
+ };
149
+ if (action === 'login') {
150
+ const [invitedUserOnly] = config ? await isInvitedUserOnly(config, node, teamDid) : [false];
151
+ const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
152
+ const trustedIssuers = [...getBlockletAppIdList(blocklet), ...trustedPassports].filter(Boolean);
153
+ claims.verifiableCredential = {
144
154
  type: 'verifiableCredential',
145
155
  description: messages.requestPassport[locale],
146
156
  item: vcTypes,
147
157
  trustedIssuers,
148
158
  optional: !invitedUserOnly,
149
- },
150
- };
151
- if (passportId) {
152
- claims.verifiableCredential.target = passportId;
159
+ };
160
+ if (passportId) {
161
+ claims.verifiableCredential.target = passportId;
162
+ }
163
+ }
164
+
165
+ if (action === 'exchangePassport') {
166
+ const trustedFactories = (blocklet.trustedFactories || []).map((x) => x.factoryAddress);
167
+ claims.asset = {
168
+ type: 'asset',
169
+ description: messages.requestNFT[locale],
170
+ filters: trustedFactories.map((x) => ({ trustedParents: [x] })),
171
+ optional: false,
172
+ };
153
173
  }
154
174
 
155
175
  let user = await getRawUser(blocklet.meta.did, userDid, {
@@ -178,6 +198,7 @@ module.exports = {
178
198
  baseUrl,
179
199
  createSessionToken,
180
200
  componentId,
201
+ action,
181
202
  }) => {
182
203
  const blocklet = await request.getBlocklet();
183
204
  const { wallet, name, passportColor, did: teamDid } = await request.getBlockletInfo();
@@ -201,20 +222,40 @@ module.exports = {
201
222
  // Get auth config
202
223
  const authConfig = (await request.getServiceConfig(NODE_SERVICES.AUTH, { componentId })) || {};
203
224
 
225
+ let vc;
226
+ let nftState;
227
+ let invitedUserOnly = false;
228
+ let defaultRole = ROLES.GUEST;
229
+ let defaultTtl = 0;
230
+ let issuePassport = false;
231
+
204
232
  // Get passport vc
205
- const inputVC = await getPassportVc({ blocklet, claims, challenge, locale });
233
+ if (action === 'login') {
234
+ vc = await getPassportVc({ blocklet, claims, challenge, locale });
235
+ [invitedUserOnly, defaultRole, issuePassport] = await isInvitedUserOnly(authConfig, node, teamDid);
236
+ if (invitedUserOnly && !vc) {
237
+ throw new Error(messages.missingCredentialClaim[locale]);
238
+ }
239
+ } else if (action === 'exchangePassport') {
240
+ const claim = claims.find((x) => x.type === 'asset');
241
+ const { users } = await node.getUsers({ teamDid, query: { connectedDid: claim.asset } });
242
+ if (users.length) {
243
+ throw new Error(messages.nftAlreadyUsed[locale]);
244
+ }
206
245
 
207
- let vc = inputVC;
246
+ nftState = await verifyNFT({ claims, challenge, locale, chainHost: MAIN_CHAIN_ENDPOINT });
247
+ const matchFactory = blocklet.trustedFactories.find((x) => x.factoryAddress === nftState.parent);
248
+ if (!matchFactory) {
249
+ throw new Error(messages.invalidNftParent[locale]);
250
+ }
208
251
 
209
- // Validate passport required
210
- const [invitedUserOnly, defaultRole, issuePassport] = await isInvitedUserOnly(authConfig, node, teamDid);
211
- if (invitedUserOnly && !vc) {
212
- throw new Error(messages.missingCredentialClaim[locale]);
252
+ defaultRole = matchFactory.passport.role;
253
+ defaultTtl = matchFactory.passport.ttl;
254
+ issuePassport = true;
213
255
  }
214
256
 
215
- // Issue passport for the first login user in a invite-only team
216
257
  if (issuePassport) {
217
- logger.info('issue passport to user at the login workflow', { role: defaultRole });
258
+ logger.info(`issue passport to user at the ${action} workflow`, { role: defaultRole });
218
259
  const profile = claims.find((x) => x.type === 'profile');
219
260
  vc = createPassportVC({
220
261
  issuerName: name,
@@ -234,6 +275,7 @@ module.exports = {
234
275
  }),
235
276
  ownerProfile: profile,
236
277
  preferredColor: passportColor,
278
+ expirationDate: defaultTtl ? new Date(Date.now() + defaultTtl * 1000).toISOString() : undefined,
237
279
  });
238
280
  }
239
281
 
@@ -247,10 +289,15 @@ module.exports = {
247
289
  const role = await getRoleFromVC({ vc, node, locale, blocklet, teamDid });
248
290
  await validateRole({ role, authConfig, locale, node, teamDid });
249
291
 
292
+ checkAppOwner({ role, blocklet, userDid, locale });
293
+
250
294
  // Recreate passport with correct role
251
295
  passport = vc ? createUserPassport(vc, { role }) : null;
252
296
 
253
- const currentTime = new Date().toISOString();
297
+ const now = new Date().toISOString();
298
+ const connectedNft = nftState
299
+ ? { provider: 'nft', did: nftState.address, owner: nftState.owner, firstLoginAt: now, lastLoginAt: now }
300
+ : null;
254
301
 
255
302
  // Update profile
256
303
  const passportForLog = passport || { name: 'Guest', role: 'guest' };
@@ -262,12 +309,12 @@ module.exports = {
262
309
  locale,
263
310
  lastUsedPassport: passport,
264
311
  lastLoginIp: get(request, 'headers[x-real-ip]') || '',
265
- connectedAccount: { provider: 'wallet' },
312
+ connectedAccount: [{ provider: 'wallet', did: realDid }, connectedNft].filter(Boolean),
266
313
  }),
267
314
  });
268
315
  await node.createAuditLog(
269
316
  {
270
- action: 'login',
317
+ action,
271
318
  args: { teamDid, userDid: realDid, passport: passportForLog },
272
319
  context: formatContext(Object.assign(request, { user: doc })),
273
320
  result: doc,
@@ -290,8 +337,8 @@ module.exports = {
290
337
  approved: true,
291
338
  locale,
292
339
  passports: [passport].filter(Boolean),
293
- firstLoginAt: currentTime,
294
- lastLoginAt: currentTime,
340
+ firstLoginAt: now,
341
+ lastLoginAt: now,
295
342
  lastLoginIp: get(request, 'headers[x-real-ip]') || '',
296
343
  extraConfigs: {
297
344
  sourceProvider: 'wallet',
@@ -300,10 +347,11 @@ module.exports = {
300
347
  provider: 'wallet',
301
348
  did: realDid,
302
349
  pk: realPk,
303
- firstLoginAt: currentTime,
304
- lastLoginAt: currentTime,
350
+ firstLoginAt: now,
351
+ lastLoginAt: now,
305
352
  },
306
- ],
353
+ connectedNft,
354
+ ].filter(Boolean),
307
355
  },
308
356
  },
309
357
  });
@@ -320,11 +368,11 @@ module.exports = {
320
368
 
321
369
  // Generate new session token that client can save to localStorage
322
370
  const sessionToken = await createSessionToken(realDid, { passport, role });
323
- logger.info('login.success', { userDid: realDid, role });
371
+ logger.info(`${action}.success`, { userDid: realDid, role });
324
372
 
325
373
  if (
326
374
  // if user provides owner passport AND app does not have owner, set this user to owner
327
- (inputVC && role === ROLES.OWNER && !blocklet.settings?.owner) ||
375
+ (vc && role === ROLES.OWNER && !blocklet.settings?.owner) ||
328
376
  // if the user will receive a owner passport AND app does not have owner, set this user to owner
329
377
  (issuePassport && defaultRole === ROLES.OWNER && !blocklet.settings?.owner)
330
378
  ) {
@@ -531,6 +579,8 @@ module.exports = {
531
579
  const role = await getRoleFromVC({ vc, node, locale, blocklet, teamDid });
532
580
  await validateRole({ role, authConfig, locale, node, teamDid });
533
581
 
582
+ checkAppOwner({ role, blocklet, userDid, locale });
583
+
534
584
  // Recreate passport with correct role
535
585
  passport = vc ? createUserPassport(vc, { role }) : null;
536
586
 
@@ -569,7 +619,7 @@ module.exports = {
569
619
  onConnect: async ({ node, request, userDid, locale, passportId = '', componentId }) => {
570
620
  const translations = {
571
621
  en: {
572
- notFound: "Can't get bind account infomation",
622
+ notFound: "Can't get bind account information",
573
623
  alreadyBindOAuth: 'already bind with another account',
574
624
  alreadyBindWallet: 'Current account is already bind a wallet account',
575
625
  alreadyMainAccount: 'Current account is already a main account',
@@ -665,8 +715,8 @@ module.exports = {
665
715
 
666
716
  const { dataDir } = await getApplicationInfo({ node, nodeInfo, teamDid });
667
717
 
668
- const profileold = claims.find((x) => x.type === 'profile') || { avatar: null };
669
- const avatar = await extractUserAvatar(oauthUser.avatar || profileold.avatar, { dataDir });
718
+ const profileOld = claims.find((x) => x.type === 'profile') || { avatar: null };
719
+ const avatar = await extractUserAvatar(oauthUser.avatar || profileOld.avatar, { dataDir });
670
720
  const profile = {
671
721
  fullName: oauthUser.fullName,
672
722
  avatar,
@@ -770,7 +820,7 @@ module.exports = {
770
820
  optional: false,
771
821
  };
772
822
  },
773
- keyPair: getKeyPairClaim(node, false),
823
+ keyPair: getKeyPairClaim(node, { declare: false }),
774
824
  },
775
825
  ],
776
826
 
@@ -804,4 +854,8 @@ module.exports = {
804
854
  return { blocklet, keyPair, user: { role, did: userDid } };
805
855
  },
806
856
  },
857
+
858
+ utils: {
859
+ checkAppOwner,
860
+ },
807
861
  };
@@ -351,5 +351,29 @@ module.exports = {
351
351
  res.status(500).send(error.message);
352
352
  }
353
353
  });
354
+
355
+ server.get(`${prefix}/api/blocklet/transfer`, async (req, res) => {
356
+ const { transferId } = req.query;
357
+
358
+ if (!transferId) {
359
+ res.status(400).send('transferId is required');
360
+ return;
361
+ }
362
+
363
+ try {
364
+ const blocklet = await req.getBlocklet();
365
+ const transfer = await node.getTransferAppOwnerSession({ appDid: blocklet.appDid, transferId });
366
+
367
+ if (!transfer || Date.now() > new Date(transfer.expireDate).getTime()) {
368
+ res.status(404).send('Transfer session not found or has been used');
369
+ return;
370
+ }
371
+
372
+ res.json(transfer);
373
+ } catch (err) {
374
+ logger.error('failed to get transfer info', { transferId, error: err });
375
+ res.status(500).send(err.message);
376
+ }
377
+ });
354
378
  },
355
379
  };
@@ -130,7 +130,7 @@ async function login(req, node, options) {
130
130
  lastLoginIp,
131
131
  },
132
132
  });
133
- await declareAccount({ wallet: userWallet, moniker: oauthInfo.nickname });
133
+ await declareAccount({ wallet: userWallet, moniker: oauthInfo.nickname, blocklet });
134
134
  }
135
135
 
136
136
  const { createSessionToken } = initJwt(node, options);
@@ -248,7 +248,7 @@ async function invite(req, node, options) {
248
248
  }
249
249
  ),
250
250
  });
251
- await declareAccount({ wallet: userWallet, moniker: oauthInfo.nickname });
251
+ await declareAccount({ wallet: userWallet, moniker: oauthInfo.nickname, blocklet });
252
252
  }
253
253
 
254
254
  const { createSessionToken } = initJwt(node, options);
@@ -0,0 +1,58 @@
1
+ const logger = require('@abtnode/logger')(require('../../../../package.json').name);
2
+
3
+ const { messages } = require('@abtnode/auth/lib/auth');
4
+ const { login } = require('../../../libs/connect/session');
5
+
6
+ const { onConnect, onApprove } = login;
7
+
8
+ module.exports = function createRoutes(node, authenticator, createSessionToken) {
9
+ return {
10
+ action: 'exchange-passport',
11
+
12
+ onStart: async ({ extraParams, request }) => {
13
+ const { locale = 'en' } = extraParams;
14
+ const blocklet = await request.getBlocklet();
15
+ if (blocklet.trustedFactories.length === 0) {
16
+ throw new Error(messages.notAllowed[locale]);
17
+ }
18
+ },
19
+
20
+ onConnect: async ({ req, userDid, extraParams: { locale, passportId = '', componentId } }) => {
21
+ return onConnect({ node, request: req, userDid, locale, passportId, componentId, action: 'exchangePassport' });
22
+ },
23
+
24
+ onAuth: async ({
25
+ claims,
26
+ challenge,
27
+ userDid,
28
+ userPk,
29
+ updateSession,
30
+ extraParams: { locale, componentId },
31
+ req,
32
+ baseUrl,
33
+ }) => {
34
+ try {
35
+ const result = await onApprove({
36
+ node,
37
+ request: req,
38
+ locale,
39
+ challenge,
40
+ userDid,
41
+ userPk,
42
+ baseUrl,
43
+ claims,
44
+ createSessionToken,
45
+ componentId,
46
+ action: 'exchangePassport',
47
+ });
48
+
49
+ await updateSession({ sessionToken: result.sessionToken }, true);
50
+
51
+ return result;
52
+ } catch (err) {
53
+ logger.error('login.error', { error: err, userDid });
54
+ throw new Error(err.message);
55
+ }
56
+ },
57
+ };
58
+ };
@@ -8,7 +8,7 @@ module.exports = function createRoutes(node, authenticator, createSessionToken)
8
8
  return {
9
9
  action: 'login',
10
10
  onConnect: async ({ req, userDid, extraParams: { locale, passportId = '', componentId } }) => {
11
- return onConnect({ node, request: req, userDid, locale, passportId, componentId });
11
+ return onConnect({ node, request: req, userDid, locale, passportId, componentId, action: 'login' });
12
12
  },
13
13
 
14
14
  onAuth: async ({
@@ -33,6 +33,7 @@ module.exports = function createRoutes(node, authenticator, createSessionToken)
33
33
  claims,
34
34
  createSessionToken,
35
35
  componentId,
36
+ action: 'login',
36
37
  });
37
38
 
38
39
  await updateSession({ sessionToken: result.sessionToken }, true);
@@ -1,5 +1,5 @@
1
1
  const pick = require('lodash/pick');
2
- const { getSetupBlockletClaims } = require('@abtnode/auth/lib/server');
2
+ const { getAppDidOwnerClaims } = require('@abtnode/auth/lib/server');
3
3
  const verifySignature = require('@abtnode/auth/lib/util/verify-signature');
4
4
 
5
5
  const logger = require('@abtnode/logger')(require('../../../../package.json').name);
@@ -8,7 +8,7 @@ module.exports = function createRoutes() {
8
8
  return {
9
9
  action: 'pre-setup',
10
10
  authPrincipal: false,
11
- claims: getSetupBlockletClaims(),
11
+ claims: getAppDidOwnerClaims(),
12
12
  onAuth: async ({ claims, userDid, userPk, extraParams: { locale } }) => {
13
13
  const claim = claims.find((x) => x.type === 'signature');
14
14
  verifySignature(claim, userDid, userPk, locale);