@abtnode/auth 1.15.17 → 1.16.0-beta-b16cb035

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/lib/auth.js CHANGED
@@ -1,28 +1,41 @@
1
+ const path = require('path');
1
2
  const jwt = require('jsonwebtoken');
2
3
  const semver = require('semver');
3
4
  const joinUrl = require('url-join');
4
5
  const get = require('lodash/get');
5
6
  const { verifyPresentation, createCredentialList } = require('@arcblock/vc');
7
+ const formatContext = require('@abtnode/util/lib/format-context');
6
8
  const Mcrypto = require('@ocap/mcrypto');
9
+ const Client = require('@ocap/client');
7
10
  const { fromSecretKey, WalletType } = require('@ocap/wallet');
8
11
  const getBlockletInfo = require('@blocklet/meta/lib/info');
9
- const { PASSPORT_STATUS, NFT_TYPE_NODE_PASSPORT } = require('@abtnode/constant');
12
+ const {
13
+ PASSPORT_STATUS,
14
+ VC_TYPE_NODE_PASSPORT,
15
+ ROLES,
16
+ NODE_DATA_DIR_NAME,
17
+ AUTH_CERT_TYPE,
18
+ WELLKNOWN_BLOCKLET_ADMIN_PATH,
19
+ } = require('@abtnode/constant');
10
20
  const axios = require('@abtnode/util/lib/axios');
21
+ const { extractUserAvatar, parseUserAvatar } = require('@abtnode/util/lib/user-avatar');
22
+
11
23
  const logger = require('./logger');
12
24
  const verifySignature = require('./util/verify-signature');
13
25
  const {
14
26
  createPassport,
15
27
  createPassportVC,
16
- createPassportSvg,
17
28
  upsertToPassports,
18
29
  createUserPassport,
19
30
  getRoleFromLocalPassport,
20
31
  } = require('./passport');
21
32
 
33
+ const createPassportSvg = require('./util/create-passport-svg');
34
+
22
35
  const messages = {
23
36
  description: {
24
- en: 'Login with your DID Wallet',
25
- zh: ' DID 钱包登录',
37
+ en: 'Connect your DID Wallet',
38
+ zh: '连接 DID 钱包',
26
39
  },
27
40
  requestProfile: {
28
41
  en: 'Please provide following information to continue',
@@ -36,6 +49,10 @@ const messages = {
36
49
  en: 'Please provide passport',
37
50
  zh: '请提供通行证',
38
51
  },
52
+ requestOwnerPassport: {
53
+ en: 'Please provide owner passport',
54
+ zh: '请提供节点的所有者通行证',
55
+ },
39
56
  requestBlockletNft: {
40
57
  en: 'Please provide Blocklet Purchase NFT',
41
58
  zh: '请提供 Blocklet Purchase NFT',
@@ -46,6 +63,10 @@ const messages = {
46
63
  },
47
64
 
48
65
  // error
66
+ actionForbidden: {
67
+ en: 'You are not allowed perform this action',
68
+ zh: '你没有权限执行此操作',
69
+ },
49
70
 
50
71
  notInitialized: {
51
72
  en: 'This node is not initialized, login is disabled',
@@ -75,6 +96,10 @@ const messages = {
75
96
  en: (types) => `Invalid credential type, expect ${types.join(' or ')}`,
76
97
  zh: (types) => `无效的凭证类型,必须是 ${types.join(' 或 ')}`,
77
98
  },
99
+ invalidCredentialId: {
100
+ en: (ids) => `Invalid credential type, expect ${[].concat(ids).join(' or ')}`,
101
+ zh: (ids) => `无效的凭证,ID 必须是 ${[].concat(ids).join(' 或 ')}`,
102
+ },
78
103
  invalidCredentialHolder: {
79
104
  en: 'Invalid credential holder',
80
105
  zh: '无效的凭证持有者',
@@ -91,12 +116,16 @@ const messages = {
91
116
  en: 'The account does not match the owner account of this passport, please use the DID wallet that contains the owner account of this passport to receive.',
92
117
  zh: '该账号与此通行证的所有者账号不匹配,请使用包含此通行证所有者账号的 DID 钱包领取。',
93
118
  },
119
+ userMismatch: {
120
+ en: 'User mismatch, please use connected DID wallet to continue.',
121
+ zh: '用户不匹配,请使用当前会话连接的钱包操作',
122
+ },
94
123
  lowVersion: {
95
124
  en: 'Your DID wallet version is too low, please upgrade to the latest version',
96
125
  zh: '你的 DID 钱包版本过低,请升级至最新版本',
97
126
  },
98
127
  passportStatusCheckFailed: {
99
- en: (message) => `Passport status check failed:${message}`,
128
+ en: (message) => `Passport status check failed: ${message}`,
100
129
  zh: (message) => `通行证状态检测失败:${message}`,
101
130
  },
102
131
  unKnownStatus: {
@@ -127,6 +156,22 @@ const messages = {
127
156
  en: 'Invalid Params',
128
157
  zh: '无效的参数',
129
158
  },
159
+ missingKeyPair: {
160
+ en: 'Missing app key pair',
161
+ zh: '缺少应用钥匙对',
162
+ },
163
+ missingBlockletUrl: {
164
+ en: 'Missing blocklet url',
165
+ zh: '缺少应用下载地址',
166
+ },
167
+ missingBlockletDid: {
168
+ en: 'Missing blocklet did',
169
+ zh: '缺少应用 ID',
170
+ },
171
+ missingChainHost: {
172
+ en: 'Missing chain host',
173
+ zh: '缺少链的端点地址',
174
+ },
130
175
  invalidBlocklet: {
131
176
  en: 'Invalid Blocklet',
132
177
  zh: '无效的 Blocklet',
@@ -139,10 +184,101 @@ const messages = {
139
184
  en: 'Invalid Blocklet VC',
140
185
  zh: '无效的 Blocklet VC',
141
186
  },
187
+ invalidAppVersion: {
188
+ en: 'App key-pair rotating can only be performed on the latest version',
189
+ zh: '只有最新版应用可以变更钥匙对',
190
+ },
191
+
192
+ // NFT related
193
+ missingProfileClaim: {
194
+ en: 'Owner profile not provided',
195
+ zh: '节点所有者信息必须提供',
196
+ },
197
+ invalidNftClaim: {
198
+ en: 'Invalid Asset Claim: ownerPk, ownerDid and ownerProof are required',
199
+ zh: '无效的 NFT Claim:ownerPk、ownerDid、ownerProof 是必填的',
200
+ },
201
+ invalidNft: {
202
+ en: 'Invalid server ownership NFT state',
203
+ zh: '无效的节点所有权 NFT',
204
+ },
205
+ invalidNftHolder: {
206
+ en: 'Invalid server ownership NFT holder',
207
+ zh: '无效的节点所有权 NFT 持有者',
208
+ },
209
+ invalidNftProof: {
210
+ en: 'Invalid server ownership NFT signature proof',
211
+ zh: '无效的节点所有权 NFT 签名',
212
+ },
213
+ invalidNftIssuer: {
214
+ en: 'Invalid server ownership NFT issuer',
215
+ zh: '无效的节点所有权 NFT 颁发者',
216
+ },
217
+ tagNotMatch: {
218
+ en: 'This NFT is for another blocklet server',
219
+ zh: '您所提供的所有权 NFT 不属于当前节点',
220
+ },
221
+ requestNft: {
222
+ en: 'Please provide server ownership NFT',
223
+ zh: '请提供节点所有权 NFT',
224
+ },
225
+ requestServerlessNFT: {
226
+ en: 'Please provide serverless NFT',
227
+ zh: '请提供无服务 NFT',
228
+ },
229
+ serverlessNftIdRequired: {
230
+ en: 'Serverless NFT ID is required',
231
+ zh: '无服务 NFT ID 是必须的',
232
+ },
233
+ nftAlreadyConsume: {
234
+ en: 'This NFT has already been used',
235
+ zh: '该 NFT 已经被使用过了',
236
+ },
237
+ nftAlreadyExpired: {
238
+ en: 'This NFT has expired',
239
+ zh: '该 NFT 已经过期',
240
+ },
241
+ missingNftClaim: {
242
+ en: 'Ownership NFT not provided',
243
+ zh: '节点所有权 NFT 必须提供',
244
+ },
245
+ noNft: {
246
+ en: 'This server is not initialized to accept ownership NFT',
247
+ zh: '该节点不能用这种方式成为管理员',
248
+ },
249
+ noTag: {
250
+ en: 'Tag not found from server launcher info',
251
+ zh: '节点初始化信息异常:缺少标签',
252
+ },
253
+ noChainHost: {
254
+ en: 'chainHost not found from server launcher info',
255
+ zh: '节点初始化信息异常:缺少链地址',
256
+ },
257
+ alreadyTransferred: {
258
+ en: 'The Blocklet Server already belongs to {owner}',
259
+ zh: '该节点已经属于 {owner}',
260
+ },
261
+ delegateTransferOwnerNFT: {
262
+ en: 'Sign the delegation transaction to authorize blocklet server to transfer your ownership NFT to new owner when he/she claims the transfer.',
263
+ zh: '签名下面的交易以授权节点转移所有权以及代表所有权的 NFT。',
264
+ },
265
+ notAllowedTransferToSelf: {
266
+ en: 'Not allowed to transfer the Server to yourself',
267
+ zh: '不能将节点转移给自己',
268
+ },
269
+ tagRequired: {
270
+ en: 'tag is required',
271
+ zh: 'tag 不能为空',
272
+ },
142
273
  };
143
274
 
144
275
  const PASSPORT_STATUS_KEY = 'passport-status';
145
276
 
277
+ const TEAM_TYPE = {
278
+ NODE: 'node',
279
+ BLOCKLET: 'blocklet',
280
+ };
281
+
146
282
  const getPassportStatusEndpoint = ({ baseUrl, userDid, teamDid }) => {
147
283
  if (!baseUrl) {
148
284
  return null;
@@ -172,24 +308,37 @@ const getRandomMessage = (len = 16) => {
172
308
  return hex.replace(/^0x/, '').toUpperCase();
173
309
  };
174
310
 
175
- const getTeamInfo = async ({ node, nodeInfo, teamDid }) => {
311
+ const getApplicationInfo = async ({ node, nodeInfo, teamDid }) => {
176
312
  let type;
177
313
  let name;
178
314
  let wallet;
315
+ let permanentWallet;
179
316
  let description;
317
+ let passportColor;
318
+ let owner;
319
+ let dataDir;
180
320
 
181
321
  if (teamDid === nodeInfo.did) {
182
322
  name = nodeInfo.name;
183
323
  description = nodeInfo.description;
184
- wallet = getNodeWallet(nodeInfo.sk);
185
- type = 'node';
324
+ const _wallet = getNodeWallet(nodeInfo.sk);
325
+ wallet = _wallet;
326
+ permanentWallet = _wallet;
327
+ type = TEAM_TYPE.NODE;
328
+ passportColor = 'default';
329
+ owner = nodeInfo.nodeOwner;
330
+ dataDir = path.join(node.dataDirs.data, NODE_DATA_DIR_NAME);
186
331
  } else {
187
332
  const blocklet = await node.getBlocklet({ did: teamDid, attachRuntimeInfo: false });
188
333
  const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
189
334
  name = blockletInfo.name;
190
335
  description = blockletInfo.description;
191
336
  wallet = blockletInfo.wallet;
192
- type = 'blocklet';
337
+ permanentWallet = blockletInfo.permanentWallet;
338
+ passportColor = blockletInfo.passportColor;
339
+ type = TEAM_TYPE.BLOCKLET;
340
+ owner = get(blocklet, 'settings.owner');
341
+ dataDir = blocklet.env.dataDir;
193
342
  }
194
343
 
195
344
  return {
@@ -197,7 +346,10 @@ const getTeamInfo = async ({ node, nodeInfo, teamDid }) => {
197
346
  name,
198
347
  description,
199
348
  wallet,
200
- did: teamDid,
349
+ permanentWallet,
350
+ passportColor,
351
+ owner,
352
+ dataDir,
201
353
  };
202
354
  };
203
355
 
@@ -219,9 +371,21 @@ const createAuthToken = ({ did, passport, role, secret, expiresIn } = {}) => {
219
371
  return token;
220
372
  };
221
373
 
222
- const createAuthTokenByOwnershipNFT = ({ role, secret, expiresIn } = {}) => {
374
+ const createBlockletControllerAuthToken = ({ did, role, controller, secret, expiresIn } = {}) => {
375
+ const payload = {
376
+ type: AUTH_CERT_TYPE.BLOCKLET_CONTROLLER,
377
+ did,
378
+ role,
379
+ controller,
380
+ };
381
+
382
+ return jwt.sign(payload, secret, { expiresIn });
383
+ };
384
+
385
+ const createAuthTokenByOwnershipNFT = ({ did, role, secret, expiresIn } = {}) => {
223
386
  const payload = {
224
- type: 'ownership_nft',
387
+ type: AUTH_CERT_TYPE.OWNERSHIP_NFT,
388
+ did,
225
389
  role,
226
390
  };
227
391
 
@@ -234,8 +398,7 @@ const getUser = async (node, teamDid, userDid) => {
234
398
  };
235
399
 
236
400
  const beforeInvitationRequest = async ({ node, teamDid, inviteId, locale = 'en' }) => {
237
- const invitations = await node.getInvitations({ teamDid });
238
- const inviteInfo = invitations.find((d) => d.inviteId === inviteId);
401
+ const inviteInfo = await node.getInvitation({ teamDid, inviteId });
239
402
 
240
403
  if (!inviteInfo) {
241
404
  throw new Error(
@@ -245,17 +408,30 @@ const beforeInvitationRequest = async ({ node, teamDid, inviteId, locale = 'en'
245
408
  }[locale]
246
409
  );
247
410
  }
411
+
412
+ const count = await node.getUsersCount({ teamDid });
413
+ if (count === 0) {
414
+ throw new Error(
415
+ {
416
+ en: 'The Application has no owner attached',
417
+ zh: '应用未绑定所有者',
418
+ }[locale]
419
+ );
420
+ }
248
421
  };
249
422
 
250
423
  const createInvitationRequest = async ({ node, nodeInfo, teamDid, inviteId, locale = 'en' }) => {
251
424
  // verify invite id
252
- const invitations = await node.getInvitations({ teamDid });
253
- const inviteInfo = invitations.find((d) => d.inviteId === inviteId);
425
+ const inviteInfo = await node.getInvitation({ teamDid, inviteId });
254
426
  if (!inviteInfo) {
255
427
  throw new Error('The invitation does not exist or has been used');
256
428
  }
257
429
 
258
- const { name: issuerName, wallet: issuerWallet } = await getTeamInfo({ node, nodeInfo, teamDid });
430
+ const {
431
+ name: issuerName,
432
+ wallet: issuerWallet,
433
+ passportColor,
434
+ } = await getApplicationInfo({ node, nodeInfo, teamDid });
259
435
 
260
436
  const passport = await createPassport({
261
437
  name: inviteInfo.role,
@@ -270,12 +446,19 @@ const createInvitationRequest = async ({ node, nodeInfo, teamDid, inviteId, loca
270
446
  type: 'mime:text/plain',
271
447
  display: JSON.stringify({
272
448
  type: 'svg',
273
- content: createPassportSvg({ issuer: issuerName, title: passport.title, issuerDid: issuerWallet.address }),
449
+ content: createPassportSvg({
450
+ issuer: issuerName,
451
+ title: passport.title,
452
+ issuerDid: issuerWallet.address,
453
+ ownerName: 'Your Name',
454
+ preferredColor: passportColor,
455
+ }),
274
456
  }),
275
457
  };
276
458
  };
277
459
 
278
460
  const handleInvitationResponse = async ({
461
+ req = {},
279
462
  node,
280
463
  nodeInfo,
281
464
  teamDid,
@@ -286,6 +469,7 @@ const handleInvitationResponse = async ({
286
469
  claims,
287
470
  statusEndpointBaseUrl,
288
471
  endpoint,
472
+ newNftOwner,
289
473
  }) => {
290
474
  if (!nodeInfo.nodeOwner) {
291
475
  throw new Error(messages.notInitialized[locale]);
@@ -294,9 +478,48 @@ const handleInvitationResponse = async ({
294
478
  const claim = claims.find((x) => x.type === 'signature');
295
479
  verifySignature(claim, userDid, userPk, locale);
296
480
 
297
- const { name: issuerName, wallet: issuerWallet, type: issuerType } = await getTeamInfo({ node, nodeInfo, teamDid });
481
+ const inviteInfo = await node.getInvitation({ teamDid, inviteId });
482
+ if (!inviteInfo) {
483
+ throw new Error(`The invitation does not exist: ${inviteId}`);
484
+ }
485
+
486
+ if (inviteInfo.role === 'owner' && userDid === nodeInfo.nodeOwner.did) {
487
+ throw new Error(messages.notAllowedTransferToSelf[locale]);
488
+ }
489
+
490
+ await node.checkInvitation({ teamDid, inviteId });
491
+
492
+ if (inviteInfo.role === 'owner' && get(nodeInfo, 'ownerNft.holder')) {
493
+ // 这种情况下是 Transfer 有 Owner NFT 的 Blocklet Server
494
+ const client = new Client(nodeInfo.launcher.chainHost);
495
+ const ownerNftDid = get(nodeInfo, 'ownerNft.did');
496
+
497
+ const { state: assetState } = await client.getAssetState({ address: ownerNftDid });
498
+ if (assetState.owner !== newNftOwner) {
499
+ const hash = await client.transfer({
500
+ delegator: get(nodeInfo, 'ownerNft.holder'),
501
+ to: newNftOwner,
502
+ assets: [ownerNftDid],
503
+ wallet: getNodeWallet(nodeInfo.sk),
504
+ });
505
+
506
+ logger.info('transferred nft', { hash, nft: ownerNftDid });
507
+ await node.updateNftHolder(newNftOwner);
508
+ logger.info('updated owner nft holder', { holder: newNftOwner, nft: ownerNftDid });
509
+ }
510
+ }
511
+
512
+ const {
513
+ name: issuerName,
514
+ wallet: issuerWallet,
515
+ type: issuerType,
516
+ passportColor,
517
+ dataDir,
518
+ } = await getApplicationInfo({ node, nodeInfo, teamDid });
519
+
520
+ const { remark } = inviteInfo;
298
521
 
299
- const inviteInfo = await node.processInvitation({ teamDid, inviteId });
522
+ const profile = claims.find((x) => x.type === 'profile');
300
523
 
301
524
  const vcParams = {
302
525
  issuerName,
@@ -314,10 +537,12 @@ const handleInvitationResponse = async ({
314
537
  userDid,
315
538
  teamDid,
316
539
  }),
317
- types: teamDid === nodeInfo.did ? [NFT_TYPE_NODE_PASSPORT] : [],
540
+ types: teamDid === nodeInfo.did ? [VC_TYPE_NODE_PASSPORT] : [],
541
+ ownerProfile: profile,
542
+ preferredColor: passportColor,
318
543
  };
319
544
 
320
- if (issuerType === 'node') {
545
+ if (issuerType === TEAM_TYPE.NODE) {
321
546
  vcParams.tag = nodeInfo.did;
322
547
  }
323
548
 
@@ -326,27 +551,61 @@ const handleInvitationResponse = async ({
326
551
  const role = getRoleFromLocalPassport(get(vc, 'credentialSubject.passport'));
327
552
  const passport = createUserPassport(vc, { role });
328
553
 
329
- const profile = claims.find((x) => x.type === 'profile');
330
-
331
554
  const user = await getUser(node, teamDid, userDid);
332
555
 
556
+ if (role === 'owner') {
557
+ if (user && user.role === 'owner') {
558
+ throw new Error(messages.alreadyTransferred[locale](userDid));
559
+ }
560
+
561
+ await node.updateNodeOwner({ nodeOwner: { did: userDid, pk: userPk } });
562
+
563
+ const originalOwner = await getUser(node, teamDid, nodeInfo.nodeOwner.did);
564
+ const originalOwnerPassport = (originalOwner.passports || []).find((p) => p.role === 'owner');
565
+ if (originalOwnerPassport) {
566
+ await node.revokeUserPassport({ teamDid, userDid: nodeInfo.nodeOwner.did, passportId: originalOwnerPassport.id });
567
+ logger.info('passport revoked on server transfer claiming', {
568
+ teamDid,
569
+ userDid: nodeInfo.nodeOwner.did,
570
+ passportId: originalOwnerPassport.id,
571
+ });
572
+ }
573
+ }
574
+
575
+ const avatar = await extractUserAvatar(get(profile, 'avatar'), {
576
+ dataDir,
577
+ });
578
+
333
579
  if (user) {
334
- await node.updateUser({
580
+ const doc = await node.updateUser({
335
581
  teamDid,
336
582
  user: {
337
583
  ...profile,
584
+ avatar,
338
585
  did: userDid,
339
586
  pk: userPk,
340
587
  locale,
341
588
  passports: upsertToPassports(user.passports || [], passport),
342
589
  lastLoginAt: new Date().toISOString(),
590
+ lastLoginIp: get(req, 'headers[x-real-ip]') || '',
591
+ remark,
343
592
  },
344
593
  });
594
+ await node.createAuditLog(
595
+ {
596
+ action: 'updateUser',
597
+ args: { teamDid, userDid, passport, inviteId, reason: 'accepted invitation' },
598
+ context: formatContext(Object.assign(req, { user })),
599
+ result: doc,
600
+ },
601
+ node
602
+ );
345
603
  } else {
346
- await node.addUser({
604
+ const doc = await node.addUser({
347
605
  teamDid,
348
606
  user: {
349
607
  ...profile,
608
+ avatar,
350
609
  did: userDid,
351
610
  pk: userPk,
352
611
  approved: true,
@@ -354,11 +613,30 @@ const handleInvitationResponse = async ({
354
613
  passports: [passport],
355
614
  firstLoginAt: new Date().toISOString(),
356
615
  lastLoginAt: new Date().toISOString(),
616
+ lastLoginIp: get(req, 'headers[x-real-ip]') || '',
617
+ remark,
357
618
  },
358
619
  });
620
+ await node.createAuditLog(
621
+ {
622
+ action: 'addUser',
623
+ args: { teamDid, userDid, passport, inviteId, reason: 'accepted invitation' },
624
+ context: formatContext(Object.assign(req, { user: doc })),
625
+ result: doc,
626
+ },
627
+ node
628
+ );
359
629
  }
360
630
 
361
- logger.info('login.success', { userDid });
631
+ logger.info('invite success', { userDid });
632
+
633
+ // await node.closeInvitation({ teamDid, inviteId, status: 'success', receiver: { did: userDid, role } });
634
+ await node.closeInvitation({
635
+ teamDid,
636
+ inviteId,
637
+ status: 'success',
638
+ receiver: { did: userDid, role, timeout: 1000 * 9999 },
639
+ });
362
640
 
363
641
  return {
364
642
  passport,
@@ -388,14 +666,25 @@ const createIssuePassportRequest = async ({ node, nodeInfo, teamDid, id, locale
388
666
  if (!id) {
389
667
  throw new Error('The issuance id does not exist');
390
668
  }
391
-
392
669
  const list = await node.getPassportIssuances({ teamDid });
393
670
  const issuanceInfo = list.find((x) => x.id === id);
394
671
  if (!issuanceInfo) {
395
672
  throw new Error('The issuance does not exist or has been used');
396
673
  }
397
674
 
398
- const { name: issuerName, wallet: issuerWallet } = await getTeamInfo({ node, nodeInfo, teamDid });
675
+ const {
676
+ name: issuerName,
677
+ wallet: issuerWallet,
678
+ passportColor,
679
+ owner: teamOwner,
680
+ dataDir,
681
+ } = await getApplicationInfo({ node, nodeInfo, teamDid });
682
+
683
+ if (issuanceInfo.name === ROLES.OWNER && !!teamOwner) {
684
+ throw new Error('Cannot receive owner passport because the owner already exists');
685
+ }
686
+
687
+ const user = await getUser(node, teamDid, issuanceInfo.ownerDid);
399
688
 
400
689
  const passport = await createPassport({
401
690
  name: issuanceInfo.name,
@@ -410,7 +699,14 @@ const createIssuePassportRequest = async ({ node, nodeInfo, teamDid, id, locale
410
699
  type: 'mime:text/plain',
411
700
  display: JSON.stringify({
412
701
  type: 'svg',
413
- content: createPassportSvg({ issuer: issuerName, title: passport.title, issuerDid: issuerWallet.address }),
702
+ content: createPassportSvg({
703
+ issuer: issuerName,
704
+ title: passport.title,
705
+ issuerDid: issuerWallet.address,
706
+ ownerName: get(user, 'fullName', 'Your Name'),
707
+ ownerAvatarUrl: await parseUserAvatar(get(user, 'avatar', ''), { dataDir }),
708
+ preferredColor: passportColor,
709
+ }),
414
710
  }),
415
711
  };
416
712
  };
@@ -420,6 +716,7 @@ const createIssuePassportRequest = async ({ node, nodeInfo, teamDid, id, locale
420
716
  * @param {string} statusEndpointBaseUrl passport status endpoint base url
421
717
  */
422
718
  const handleIssuePassportResponse = async ({
719
+ req = {},
423
720
  node,
424
721
  nodeInfo,
425
722
  teamDid,
@@ -429,6 +726,7 @@ const handleIssuePassportResponse = async ({
429
726
  locale = 'en',
430
727
  claims,
431
728
  statusEndpointBaseUrl,
729
+ updateSession,
432
730
  endpoint,
433
731
  }) => {
434
732
  // verify signature
@@ -445,17 +743,32 @@ const handleIssuePassportResponse = async ({
445
743
  );
446
744
  }
447
745
 
448
- const { name: issuerName, wallet: issuerWallet, type: issuerType } = await getTeamInfo({ node, nodeInfo, teamDid });
746
+ const {
747
+ name: issuerName,
748
+ wallet: issuerWallet,
749
+ type: issuerType,
750
+ passportColor,
751
+ owner: teamOwner,
752
+ dataDir,
753
+ } = await getApplicationInfo({ node, nodeInfo, teamDid });
449
754
 
450
755
  // get issuanceInfo from session
451
756
  const list = await node.getPassportIssuances({ teamDid });
452
757
  const issuanceInfo = list.find((x) => x.id === id);
453
758
  const { name, ownerDid } = issuanceInfo;
454
759
 
760
+ if (name === ROLES.OWNER && !!teamOwner) {
761
+ throw new Error('Cannot receive Owner Passport because the owner already exists');
762
+ }
763
+
455
764
  if (ownerDid !== userDid) {
456
765
  throw new Error(messages.notOwner[locale]);
457
766
  }
458
767
 
768
+ if (user) {
769
+ user.avatar = await parseUserAvatar(user.avatar, { dataDir });
770
+ }
771
+
459
772
  const vcParams = {
460
773
  issuerName,
461
774
  issuerWallet,
@@ -472,10 +785,12 @@ const handleIssuePassportResponse = async ({
472
785
  userDid,
473
786
  teamDid,
474
787
  }),
475
- types: teamDid === nodeInfo.did ? [NFT_TYPE_NODE_PASSPORT] : [],
788
+ types: teamDid === nodeInfo.did ? [VC_TYPE_NODE_PASSPORT] : [],
789
+ ownerProfile: user,
790
+ preferredColor: passportColor,
476
791
  };
477
792
 
478
- if (issuerType === 'node') {
793
+ if (issuerType === TEAM_TYPE.NODE) {
479
794
  vcParams.tag = nodeInfo.did;
480
795
  }
481
796
 
@@ -496,7 +811,23 @@ const handleIssuePassportResponse = async ({
496
811
  }
497
812
 
498
813
  // delete session
499
- await node.processPassportIssuance({ teamDid, sessionId: id });
814
+ const result = await node.processPassportIssuance({ teamDid, sessionId: id });
815
+ await node.createAuditLog(
816
+ {
817
+ action: 'processPassportIssuance',
818
+ args: { teamDid, userDid, ...result, sessionId: id, reason: 'claimed passport' },
819
+ context: formatContext(Object.assign(req, { user })),
820
+ result,
821
+ },
822
+ node
823
+ );
824
+
825
+ if (name === ROLES.OWNER && issuerType === TEAM_TYPE.BLOCKLET) {
826
+ logger.info('Bind owner for blocklet', { teamDid, userDid });
827
+ await node.setBlockletInitialized({ did: teamDid, owner: { did: userDid, pk: userPk } });
828
+ }
829
+
830
+ await updateSession({ passportId: vc.id });
500
831
 
501
832
  return {
502
833
  disposition: 'attachment',
@@ -505,10 +836,10 @@ const handleIssuePassportResponse = async ({
505
836
  };
506
837
  };
507
838
 
508
- const getVCFromClaims = async ({ claims, challenge, trustedIssuers, vcTypes, locale = 'en' }) => {
509
- const credential = claims.find(
510
- (x) => x.type === 'verifiableCredential' && vcTypes.some((item) => x.item.includes(item))
511
- );
839
+ const getVCFromClaims = async ({ claims, challenge, trustedIssuers, vcTypes, locale = 'en', vcId }) => {
840
+ const credential = claims
841
+ .filter(Boolean) // FIXES: https://github.com/ArcBlock/did-connect/issues/74
842
+ .find((x) => x.type === 'verifiableCredential' && vcTypes.some((item) => x.item.includes(item)));
512
843
 
513
844
  if (!credential || !credential.presentation) {
514
845
  return {};
@@ -534,6 +865,11 @@ const getVCFromClaims = async ({ claims, challenge, trustedIssuers, vcTypes, loc
534
865
  : [presentation.verifiableCredential];
535
866
  const vc = JSON.parse(credentials[0]);
536
867
 
868
+ // verify vc id
869
+ if (vcId && vc.id !== vcId) {
870
+ throw new Error(messages.invalidCredentialId[locale](vcId));
871
+ }
872
+
537
873
  // verify vc type
538
874
  const types = [].concat(vc.type);
539
875
  if (!types.some((x) => vcTypes.includes(x))) {
@@ -546,8 +882,8 @@ const getVCFromClaims = async ({ claims, challenge, trustedIssuers, vcTypes, loc
546
882
  };
547
883
  };
548
884
 
549
- const checkWalletVersion = ({ abtwallet, locale = 'en' }) => {
550
- const { os, version } = abtwallet;
885
+ const checkWalletVersion = ({ didwallet, locale = 'en' }) => {
886
+ const { os, version } = didwallet;
551
887
 
552
888
  const defaultExpected = '3.0.0';
553
889
 
@@ -566,11 +902,21 @@ const checkWalletVersion = ({ abtwallet, locale = 'en' }) => {
566
902
 
567
903
  const getPassportStatus = async ({ node, teamDid, userDid, vcId, locale = 'en' }) => {
568
904
  const nodeInfo = await node.getNodeInfo();
569
- const { wallet: issuerWallet, name: issuerName, type: issuerType } = await getTeamInfo({ node, nodeInfo, teamDid });
905
+ const {
906
+ wallet: issuerWallet,
907
+ name: issuerName,
908
+ type: issuerType,
909
+ } = await getApplicationInfo({ node, nodeInfo, teamDid });
570
910
 
571
911
  const actionLabel = {
572
- zh: `${issuerType === 'node' ? '管理节点' : '查看 Blocklet'}`,
573
- en: `${issuerType === 'node' ? 'Manage Node' : 'View Blocklet'}`,
912
+ open: {
913
+ zh: `${issuerType === TEAM_TYPE.NODE ? '管理节点' : '查看 Blocklet'}`,
914
+ en: `${issuerType === TEAM_TYPE.NODE ? 'Manage Node' : 'View Blocklet'}`,
915
+ },
916
+ manage: {
917
+ zh: '管理 Blocklet',
918
+ en: 'Manage Blocklet',
919
+ },
574
920
  };
575
921
 
576
922
  const user = await node.getUser({ teamDid, user: { did: userDid } });
@@ -598,18 +944,33 @@ const getPassportStatus = async ({ node, teamDid, userDid, vcId, locale = 'en' }
598
944
 
599
945
  const actionList = [];
600
946
  if (passport.endpoint) {
947
+ const claims = [];
948
+
949
+ // open blocklet
950
+ claims.push({
951
+ id: passport.endpoint,
952
+ type: 'navigate',
953
+ name: issuerType === TEAM_TYPE.NODE ? 'open-node' : 'open-blocklet',
954
+ scope: 'public',
955
+ label: actionLabel.open[locale],
956
+ });
957
+
958
+ // manage blocklet
959
+ const { name } = passport;
960
+ if ([ROLES.OWNER, ROLES.ADMIN].includes(name) && issuerType === TEAM_TYPE.BLOCKLET) {
961
+ claims.push({
962
+ id: joinUrl(passport.endpoint, WELLKNOWN_BLOCKLET_ADMIN_PATH),
963
+ type: 'navigate',
964
+ name: 'open-blocklet-admin',
965
+ scope: 'public',
966
+ label: actionLabel.manage[locale],
967
+ });
968
+ }
969
+
601
970
  actionList.push(
602
971
  ...createCredentialList({
603
972
  issuer,
604
- claims: [
605
- {
606
- id: passport.endpoint,
607
- type: 'navigate',
608
- name: issuerType === 'node' ? 'open-node' : 'open-blocklet',
609
- scope: 'public',
610
- label: actionLabel[locale],
611
- },
612
- ],
973
+ claims,
613
974
  })
614
975
  );
615
976
  }
@@ -682,7 +1043,7 @@ const setUserInfoHeaders = (req) => {
682
1043
 
683
1044
  module.exports = {
684
1045
  getUser,
685
- getTeamInfo,
1046
+ getApplicationInfo,
686
1047
  createAuthToken,
687
1048
  createAuthTokenByOwnershipNFT,
688
1049
  beforeInvitationRequest,
@@ -698,4 +1059,6 @@ module.exports = {
698
1059
  getPassportStatus,
699
1060
  validatePassportStatus,
700
1061
  setUserInfoHeaders,
1062
+ createBlockletControllerAuthToken,
1063
+ TEAM_TYPE,
701
1064
  };