@abtnode/auth 1.15.17 → 1.16.0-beta-8ee536d7

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/server.js ADDED
@@ -0,0 +1,742 @@
1
+ const get = require('lodash/get');
2
+ const pick = require('lodash/pick');
3
+ const isEmpty = require('lodash/isEmpty');
4
+ const last = require('lodash/last');
5
+ const { isNFTExpired, isNFTConsumed } = require('@abtnode/util/lib/nft');
6
+ const Client = require('@ocap/client');
7
+ const { fromPublicKey } = require('@ocap/wallet');
8
+ const { types } = require('@ocap/mcrypto');
9
+ const { fromBase58, toAddress, toHex } = require('@ocap/util');
10
+ const { toTypeInfo, isFromPublicKey, DidType, isEthereumType } = require('@arcblock/did');
11
+ const urlFriendly = require('@blocklet/meta/lib/url-friendly').default;
12
+ const getApplicationWallet = require('@blocklet/meta/lib/wallet');
13
+ const { slugify } = require('transliteration');
14
+ const { getChainInfo } = require('@blocklet/meta/lib/util');
15
+ const getBlockletInfo = require('@blocklet/meta/lib/info');
16
+ const formatContext = require('@abtnode/util/lib/format-context');
17
+ const {
18
+ ROLES,
19
+ VC_TYPE_GENERAL_PASSPORT,
20
+ VC_TYPE_NODE_PASSPORT,
21
+ NFT_TYPE_SERVER_OWNERSHIP,
22
+ SERVER_ROLES,
23
+ NFT_TYPE_SERVERLESS,
24
+ MAIN_CHAIN_ENDPOINT,
25
+ APP_STRUCT_VERSION,
26
+ } = require('@abtnode/constant');
27
+ const {
28
+ messages,
29
+ getVCFromClaims,
30
+ getUser,
31
+ validatePassportStatus,
32
+ createAuthToken,
33
+ createAuthTokenByOwnershipNFT,
34
+ createBlockletControllerAuthToken,
35
+ checkWalletVersion,
36
+ } = require('./auth');
37
+ const {
38
+ validatePassport,
39
+ isUserPassportRevoked,
40
+ getRoleFromLocalPassport,
41
+ getRoleFromExternalPassport,
42
+ createUserPassport,
43
+ } = require('./passport');
44
+ const logger = require('./logger');
45
+
46
+ const secret = process.env.ABT_NODE_SESSION_SECRET;
47
+ const LAUNCH_BLOCKLET_TOKEN_EXPIRE = '1d';
48
+
49
+ // External token should expire after 20 min
50
+ // Assuming the blocklet installation will take no more than 20 min
51
+ const EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE = '20m';
52
+
53
+ const BLOCKLET_SERVER_VC_TYPES = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
54
+
55
+ const ensureLauncherIssuer = (issuers, nodeInfo) => {
56
+ const launcherDid = get(nodeInfo, 'launcher.did');
57
+ if (launcherDid) {
58
+ issuers.push(launcherDid);
59
+ }
60
+ };
61
+
62
+ const getTrustedIssuers = (nodeInfo) => {
63
+ const trustedPassports = (nodeInfo.trustedPassports || []).map((x) => x.issuerDid);
64
+ return [nodeInfo.did, ...trustedPassports].filter(Boolean);
65
+ };
66
+
67
+ const authenticateByVc = async ({
68
+ node,
69
+ locale,
70
+ userDid,
71
+ claims,
72
+ challenge,
73
+ requireNodeInitialized = true,
74
+ launchBlocklet,
75
+ blocklet,
76
+ }) => {
77
+ if (requireNodeInitialized) {
78
+ if ((await node.isInitialized()) === false) {
79
+ throw new Error(messages.notInitialized[locale]);
80
+ }
81
+ }
82
+
83
+ const info = await node.getNodeInfo();
84
+ const teamDid = info.did;
85
+ const { name } = info;
86
+
87
+ const trustedIssuers = getTrustedIssuers(info);
88
+
89
+ if (launchBlocklet) {
90
+ ensureLauncherIssuer(trustedIssuers, info);
91
+ }
92
+
93
+ const { vc, types: passportTypes } = await getVCFromClaims({
94
+ claims,
95
+ challenge,
96
+ trustedIssuers,
97
+ vcTypes: BLOCKLET_SERVER_VC_TYPES,
98
+ locale,
99
+ vcId: blocklet?.controller?.vcId,
100
+ });
101
+
102
+ if (!vc) {
103
+ throw new Error(messages.missingCredentialClaim[locale]);
104
+ }
105
+
106
+ // check user approved
107
+ const user = await getUser(node, teamDid, userDid);
108
+ if (user && !user.approved) {
109
+ throw new Error(messages.notAllowed[locale]);
110
+ }
111
+
112
+ // Get user passport from vc
113
+ let passport = createUserPassport(vc);
114
+ if (user && isUserPassportRevoked(user, passport)) {
115
+ throw new Error(messages.passportRevoked[locale](name));
116
+ }
117
+
118
+ // Get role from vc
119
+ let role;
120
+ if (passportTypes.some((x) => [VC_TYPE_GENERAL_PASSPORT].includes(x))) {
121
+ await validatePassport(get(vc, 'credentialSubject.passport'));
122
+ const issuerId = get(vc, 'issuer.id');
123
+ if (issuerId === teamDid) {
124
+ role = getRoleFromLocalPassport(get(vc, 'credentialSubject.passport'));
125
+ } else {
126
+ // map external passport to local role
127
+ const { mappings = [] } = (info.trustedPassports || []).find((x) => x.issuerDid === issuerId) || {};
128
+ role = await getRoleFromExternalPassport({
129
+ passport: get(vc, 'credentialSubject.passport'),
130
+ node,
131
+ teamDid,
132
+ locale,
133
+ mappings,
134
+ });
135
+
136
+ // check status of external passport if passport has an endpoint
137
+ const endpoint = get(vc, 'credentialStatus.id');
138
+ if (endpoint) {
139
+ await validatePassportStatus({ vcId: vc.id, endpoint, locale });
140
+ }
141
+ }
142
+ } else if (passportTypes.includes(NFT_TYPE_SERVER_OWNERSHIP)) {
143
+ role = ROLES.OWNER;
144
+ } else {
145
+ logger.error('cannot get role from passport, use "guest" for default', { passportTypes, vcId: vc.id });
146
+ role = ROLES.GUEST;
147
+ }
148
+
149
+ // Recreate passport with correct role
150
+ passport = createUserPassport(vc, { role });
151
+
152
+ return { role, user, teamDid, passport };
153
+ };
154
+
155
+ const authenticateByNFT = async ({ node, claims, userDid, challenge, locale, isAuth, chainHost }) => {
156
+ const info = await node.getNodeInfo();
157
+ // serverless 应用通过 querystring 传递 chainHost
158
+ const client = new Client(chainHost || info.launcher.chainHost);
159
+
160
+ const claim = claims.find((x) => x.type === 'asset');
161
+ if (!claim) {
162
+ throw new Error(messages.missingNftClaim[locale]);
163
+ }
164
+
165
+ const fields = ['asset', 'ownerProof', 'ownerPk', 'ownerDid'];
166
+ for (const field of fields) {
167
+ if (!claim[field]) {
168
+ throw new Error(messages.invalidNftClaim[locale]);
169
+ }
170
+ }
171
+
172
+ const address = claim.asset;
173
+ const ownerDid = toAddress(claim.ownerDid);
174
+ const ownerPk = fromBase58(claim.ownerPk);
175
+ const ownerProof = fromBase58(claim.ownerProof);
176
+ if (isFromPublicKey(ownerDid, ownerPk) === false) {
177
+ throw new Error(messages.invalidNftHolder[locale]);
178
+ }
179
+
180
+ const owner = fromPublicKey(ownerPk, toTypeInfo(ownerDid));
181
+ if (owner.verify(challenge, ownerProof) === false) {
182
+ throw new Error(messages.invalidNftProof[locale]);
183
+ }
184
+
185
+ const { state } = await client.getAssetState({ address }, { ignoreFields: ['context'] });
186
+ if (!state) {
187
+ throw new Error(messages.invalidNft[locale]);
188
+ }
189
+ if (state.owner !== ownerDid) {
190
+ throw new Error(messages.invalidNftHolder[locale]);
191
+ }
192
+ if (state.issuer !== info.launcher.did) {
193
+ throw new Error(messages.invalidNftIssuer[locale]);
194
+ }
195
+
196
+ if (state.tags.includes(NFT_TYPE_SERVERLESS)) {
197
+ state.data.value = JSON.parse(state.data.value);
198
+
199
+ if (!isAuth && isNFTExpired(state)) {
200
+ throw new Error(messages.nftAlreadyExpired[locale]);
201
+ }
202
+
203
+ return {
204
+ role: SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER,
205
+ nft: state,
206
+ extra: {
207
+ controller: {
208
+ nftId: address,
209
+ nftOwner: state.owner,
210
+ chainHost,
211
+ appMaxCount: state.data.value.appMaxCount || 1,
212
+ },
213
+ },
214
+ user: {
215
+ did: userDid,
216
+ },
217
+ teamDid: info.did,
218
+ };
219
+ }
220
+
221
+ // enforce tag match
222
+ if (last(state.tags) !== info.launcher.tag) {
223
+ throw new Error(messages.tagNotMatch[locale]);
224
+ }
225
+
226
+ const user = await getUser(node, info.did, userDid);
227
+ return { role: ROLES.OWNER, teamDid: info.did, nft: state, user, passport: null, ownerDid, ownerNFT: address };
228
+ };
229
+
230
+ const authenticateBySession = async ({ node, userDid, locale, allowedRoles = ['owner', 'admin', 'member'] }) => {
231
+ const info = await node.getNodeInfo();
232
+ const user = await getUser(node, info.did, userDid);
233
+ if (!user) {
234
+ throw new Error(messages.userNotFound[locale]);
235
+ }
236
+ const passport = (user.passports || []).find((x) => x.status === 'valid' && allowedRoles.includes(x.role));
237
+ return { role: passport ? passport.role : ROLES.GUEST, teamDid: info.did, user, passport: null };
238
+ };
239
+
240
+ const getAuthVcClaim =
241
+ ({ node, launchBlocklet, blocklet, options }) =>
242
+ async ({ extraParams: { locale, passportId }, context: { didwallet } }) => {
243
+ checkWalletVersion({ didwallet, locale });
244
+
245
+ const baseClaim = {
246
+ description: messages.requestPassport[locale],
247
+ optional: false,
248
+ item: BLOCKLET_SERVER_VC_TYPES,
249
+ };
250
+
251
+ if (blocklet && blocklet?.controller?.vcId) {
252
+ return {
253
+ ...baseClaim,
254
+ target: blocklet.controller.vcId,
255
+ };
256
+ }
257
+
258
+ if (passportId) {
259
+ return {
260
+ ...baseClaim,
261
+ target: passportId,
262
+ };
263
+ }
264
+
265
+ const info = await node.getNodeInfo();
266
+ const trustedIssuers = getTrustedIssuers(info);
267
+
268
+ if (launchBlocklet) {
269
+ ensureLauncherIssuer(trustedIssuers, info);
270
+ }
271
+
272
+ return {
273
+ ...baseClaim,
274
+ trustedIssuers,
275
+ ...(options || {}),
276
+ };
277
+ };
278
+
279
+ const getAuthNFTClaim =
280
+ ({ node }) =>
281
+ async ({ extraParams: { locale, launchType, nftId }, context: { didwallet } }) => {
282
+ checkWalletVersion({ didwallet, locale });
283
+ if (launchType === 'serverless') {
284
+ if (!nftId) {
285
+ throw new Error(messages.serverlessNftIdRequired[locale]);
286
+ }
287
+
288
+ return getServerlessNFTClaim(node, nftId, locale);
289
+ }
290
+
291
+ return getOwnershipNFTClaim(node, locale);
292
+ };
293
+
294
+ const getKeyPairClaim =
295
+ (node, declare = true) =>
296
+ async ({ extraParams: { locale, appDid, wt = 'default', title }, context: { didwallet } }) => {
297
+ checkWalletVersion({ didwallet, locale });
298
+
299
+ const description = {
300
+ en: 'Please generate a new key-pair for this application',
301
+ zh: '请为应用创建新的钥匙对',
302
+ };
303
+
304
+ let appName = title;
305
+ let migrateFrom = '';
306
+
307
+ let type;
308
+ let chainInfo;
309
+
310
+ // We are rotating a key-pair for existing application
311
+ if (appDid) {
312
+ const blocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
313
+ if (!blocklet) {
314
+ throw new Error(messages.invalidBlocklet[locale]);
315
+ }
316
+
317
+ const info = await node.getNodeInfo();
318
+ const { name, wallet } = getBlockletInfo(blocklet, info.sk);
319
+ appName = name;
320
+ migrateFrom = wallet.address;
321
+ type = DidType(wallet.type);
322
+ chainInfo = getChainInfo(blocklet.configObj);
323
+ } else {
324
+ type = DidType(wt);
325
+ type.role = types.RoleType.ROLE_APPLICATION;
326
+ chainInfo = getChainInfo({});
327
+ }
328
+
329
+ const typeStr = DidType.toJSON(type);
330
+ if (isEthereumType(type)) {
331
+ chainInfo.type = 'ethereum';
332
+ chainInfo.id = '1';
333
+ }
334
+
335
+ const result = {
336
+ mfa: !process.env.DID_CONNECT_MFA_DISABLED,
337
+ description: description[locale] || description.en,
338
+ moniker: (urlFriendly(slugify(appName)) || 'application').toLowerCase(),
339
+ declare: !!declare,
340
+ chainInfo,
341
+ migrateFrom,
342
+ targetType: {
343
+ role: typeStr.role?.split('_').pop()?.toLowerCase(),
344
+ key: typeStr.pk?.toLowerCase(),
345
+ hash: typeStr.hash?.toLowerCase(),
346
+ encoding: typeStr.address?.toLowerCase(),
347
+ },
348
+ };
349
+
350
+ return result;
351
+ };
352
+
353
+ const getRotateKeyPairClaims = (node) => {
354
+ return [
355
+ {
356
+ authPrincipal: async ({ extraParams: { locale, appDid } }) => {
357
+ const description = {
358
+ en: 'Please create a new key-pair for this application',
359
+ zh: '请为应用创建新的钥匙对',
360
+ };
361
+
362
+ let chainInfo = { host: 'none', id: 'none', type: 'arcblock' };
363
+
364
+ if (!appDid) {
365
+ throw new Error(messages.missingBlockletDid[locale]);
366
+ }
367
+
368
+ const blocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
369
+ if (!blocklet) {
370
+ throw new Error(messages.invalidBlocklet[locale]);
371
+ }
372
+ if (blocklet.structVersion !== APP_STRUCT_VERSION) {
373
+ throw new Error(messages.invalidAppVersion[locale]);
374
+ }
375
+
376
+ // Try to use blocklet chain config if possible
377
+ // Since migration happens on the chain the app holds some actual assets
378
+ // We must ensure it happens on that chain
379
+ chainInfo = getChainInfo(blocklet.configObj);
380
+
381
+ // Fallback to main chain, since it is the default registry for all DID
382
+ if (chainInfo.host === 'none') {
383
+ chainInfo = { host: MAIN_CHAIN_ENDPOINT, id: 'main', type: 'arcblock' };
384
+ }
385
+
386
+ return {
387
+ chainInfo,
388
+ description: description[locale] || description.en,
389
+ target: blocklet.appDid,
390
+ };
391
+ },
392
+ },
393
+ {
394
+ keyPair: getKeyPairClaim(node),
395
+ },
396
+ ];
397
+ };
398
+
399
+ const getLaunchBlockletClaims = (node, authMethod) => {
400
+ const claims = {
401
+ blockletAppKeypair: ['keyPair', getKeyPairClaim(node)],
402
+ };
403
+
404
+ if (authMethod === 'vc') {
405
+ claims.serverPassport = ['verifiableCredential', getAuthVcClaim({ node, launchBlocklet: true })];
406
+ }
407
+
408
+ if (authMethod === 'nft') {
409
+ claims.serverNFT = ['asset', getAuthNFTClaim({ node })];
410
+ }
411
+
412
+ return claims;
413
+ };
414
+
415
+ const getSetupBlockletClaims = () => {
416
+ const description = {
417
+ en: 'Sign following message to prove that you are the owner of the app',
418
+ zh: '签名如下消息以证明你是应用的拥有者',
419
+ };
420
+
421
+ return [
422
+ {
423
+ authPrincipal: async ({ context, extraParams: { locale } }) => {
424
+ const blocklet = await context.request.getBlocklet();
425
+ return {
426
+ description: description[locale] || description.en,
427
+ target: blocklet.appDid,
428
+ };
429
+ },
430
+ },
431
+ {
432
+ signature: async ({ context, extraParams: { locale } }) => {
433
+ const blocklet = await context.request.getBlocklet();
434
+ return {
435
+ description: messages.receivePassport[locale],
436
+ data: `I am the owner of app ${blocklet.appDid}`,
437
+ type: 'mime:text/plain',
438
+ };
439
+ },
440
+ },
441
+ ];
442
+ };
443
+
444
+ const getOwnershipNFTClaim = async (node, locale) => {
445
+ const info = await node.getNodeInfo();
446
+ if (!info.ownerNft || !info.ownerNft.issuer) {
447
+ throw new Error(messages.noNft[locale]);
448
+ }
449
+
450
+ const tag = get(info, 'launcher.tag', '');
451
+ const chainHost = get(info, 'launcher.chainHost', '');
452
+ if (!tag) {
453
+ throw new Error(messages.noTag[locale]);
454
+ }
455
+ if (!chainHost) {
456
+ throw new Error(messages.noChainHost[locale]);
457
+ }
458
+
459
+ return {
460
+ description: messages.requestNft[locale],
461
+ trustedIssuers: [info.ownerNft.issuer],
462
+ tag, // tag is an unique identifier for the server in launcher
463
+ };
464
+ };
465
+
466
+ const getServerlessNFTClaim = async (node, nftId, locale) => {
467
+ const info = await node.getNodeInfo();
468
+ if (!info.ownerNft || !info.ownerNft.issuer) {
469
+ throw new Error(messages.noNft[locale]);
470
+ }
471
+
472
+ const chainHost = get(info, 'launcher.chainHost', '');
473
+
474
+ if (!chainHost) {
475
+ throw new Error(messages.noChainHost[locale]);
476
+ }
477
+
478
+ return {
479
+ description: messages.requestServerlessNFT[locale],
480
+ trustedIssuers: [info.ownerNft.issuer],
481
+ address: nftId,
482
+ };
483
+ };
484
+
485
+ const ensureBlockletPermission = async ({
486
+ authMethod,
487
+ node,
488
+ userDid,
489
+ claims,
490
+ challenge,
491
+ locale,
492
+ blocklet,
493
+ isAuth,
494
+ chainHost,
495
+ allowedRoles = ['owner', 'admin', 'member'],
496
+ }) => {
497
+ let result;
498
+ if (authMethod === 'vc') {
499
+ result = await authenticateByVc({
500
+ node,
501
+ userDid,
502
+ claims,
503
+ challenge,
504
+ requireNodeInitialized: false,
505
+ locale,
506
+ launchBlocklet: true,
507
+ blocklet,
508
+ });
509
+ } else if (authMethod === 'nft') {
510
+ result = await authenticateByNFT({
511
+ node,
512
+ locale,
513
+ userDid,
514
+ claims,
515
+ challenge,
516
+ isAuth,
517
+ chainHost,
518
+ });
519
+ } else {
520
+ result = await authenticateBySession({
521
+ node,
522
+ userDid,
523
+ locale,
524
+ allowedRoles,
525
+ });
526
+ }
527
+
528
+ const { teamDid, role } = result;
529
+ const permissions = await node.getPermissionsByRole({ teamDid, role: { name: role } });
530
+ if (!permissions.some((item) => ['mutate_blocklets'].includes(item.name))) {
531
+ throw new Error(messages.notAuthorized[locale]);
532
+ }
533
+
534
+ return result;
535
+ };
536
+
537
+ const createLaunchBlockletHandler =
538
+ (node, authMethod) =>
539
+ async ({ claims, challenge, userDid, updateSession, req, extraParams }) => {
540
+ const { locale, blockletMetaUrl, title, description, chainHost } = extraParams;
541
+ logger.info('createLaunchBlockletHandler', extraParams);
542
+
543
+ const keyPair = claims.find((x) => x.type === 'keyPair');
544
+ if (!keyPair) {
545
+ logger.error('app keyPair must be provided');
546
+ throw new Error(messages.missingKeyPair[locale]);
547
+ }
548
+
549
+ if (!blockletMetaUrl && !title && !description) {
550
+ logger.error('blockletMetaUrl | title + description must be provided');
551
+ throw new Error(messages.missingBlockletUrl[locale]);
552
+ }
553
+
554
+ if (authMethod === 'nft' && !chainHost) {
555
+ logger.error('chainHost must be provided');
556
+ throw new Error(messages.missingChainHost[locale]);
557
+ }
558
+
559
+ let blocklet;
560
+ if (blockletMetaUrl) {
561
+ blocklet = await node.getBlockletMetaFromUrl({ url: blockletMetaUrl, checkPrice: true });
562
+ if (!blocklet.meta) {
563
+ throw new Error(messages.invalidBlocklet[locale]);
564
+ }
565
+
566
+ if (!blocklet.isFree) {
567
+ if (isEmpty(extraParams?.previousWorkflowData?.downloadTokenList)) {
568
+ logger.error('downloadTokenList must be provided');
569
+ throw new Error(messages.invalidParams[locale]);
570
+ }
571
+ }
572
+ }
573
+
574
+ const { role, passport, user, extra, nft } = await ensureBlockletPermission({
575
+ authMethod,
576
+ node,
577
+ userDid,
578
+ claims,
579
+ challenge,
580
+ locale,
581
+ isAuth: false,
582
+ chainHost,
583
+ blocklet,
584
+ });
585
+
586
+ let controller;
587
+
588
+ let sessionToken = '';
589
+ if (authMethod === 'vc') {
590
+ sessionToken = createAuthToken({
591
+ did: userDid,
592
+ passport,
593
+ role,
594
+ secret,
595
+ expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
596
+ });
597
+ }
598
+ if (authMethod === 'nft') {
599
+ if (role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
600
+ controller = extra.controller;
601
+ sessionToken = createBlockletControllerAuthToken({
602
+ did: userDid,
603
+ role,
604
+ controller,
605
+ secret,
606
+ expiresIn: EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE,
607
+ });
608
+ } else {
609
+ sessionToken = createAuthTokenByOwnershipNFT({
610
+ did: userDid,
611
+ role,
612
+ secret,
613
+ expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
614
+ });
615
+ }
616
+ }
617
+
618
+ if (sessionToken) {
619
+ await updateSession({ sessionToken }, true);
620
+ }
621
+
622
+ const appSk = toHex(keyPair.secret);
623
+ const appDid = getApplicationWallet(appSk).address;
624
+ await updateSession({ appDid });
625
+
626
+ if (blocklet) {
627
+ // 检查是否已安装,这里不做升级的处理
628
+ const existedBlocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
629
+
630
+ // 如果是 serverless, 并且已经消费过了,但是没有安装,则抛出异常
631
+ if (!existedBlocklet && role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER && isNFTConsumed(nft)) {
632
+ throw new Error(messages.nftAlreadyConsume[locale]);
633
+ }
634
+
635
+ if (existedBlocklet) {
636
+ await updateSession({ isInstalled: true });
637
+ logger.info('blocklet already exists', { appDid });
638
+ return;
639
+ }
640
+ }
641
+
642
+ logger.info('start install blocklet', { blockletMetaUrl, title, description });
643
+ await node.installBlocklet(
644
+ {
645
+ url: blockletMetaUrl,
646
+ title,
647
+ description,
648
+ appSk,
649
+ delay: 1000 * 4, // delay 4 seconds to download, wait for ws connection from frontend
650
+ downloadTokenList: extraParams?.previousWorkflowData?.downloadTokenList,
651
+ controller: role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER ? controller : null,
652
+ },
653
+ formatContext(Object.assign(req, { user: { ...pick(user, ['did', 'fullName']), role } }))
654
+ );
655
+ };
656
+
657
+ const getBlockletPermissionChecker =
658
+ (node, allowedRoles = ['owner', 'admin', 'member']) =>
659
+ async ({ userDid, extraParams }) => {
660
+ const { locale = 'en', connectedDid } = extraParams;
661
+
662
+ if (!connectedDid || userDid !== connectedDid) {
663
+ throw new Error(
664
+ {
665
+ en: 'Please use current connected wallet to install blocklet',
666
+ zh: '请使用当前登录的钱包来安装应用',
667
+ }[locale]
668
+ );
669
+ }
670
+
671
+ const info = await node.getNodeInfo();
672
+ const user = await getUser(node, info.did, userDid);
673
+ const passport = (user.passports || []).find((x) => x.status === 'valid' && allowedRoles.includes(x.role));
674
+ if (!passport) {
675
+ throw new Error(
676
+ {
677
+ en: 'You do not have permission to install blocklets on this server',
678
+ zh: '你无权在此节点上安装应用',
679
+ }[locale]
680
+ );
681
+ }
682
+ };
683
+
684
+ const createRotateKeyPairHandler =
685
+ (node) =>
686
+ async ({ claims, userDid, req, extraParams }) => {
687
+ const { locale, appDid } = extraParams;
688
+ logger.info('createRotateKeyPairHandler', extraParams);
689
+
690
+ const keyPair = claims.find((x) => x.type === 'keyPair');
691
+ if (!keyPair) {
692
+ logger.error('app keyPair must be provided');
693
+ throw new Error(messages.missingKeyPair[locale]);
694
+ }
695
+
696
+ if (!appDid) {
697
+ logger.error('appDid must be provided');
698
+ throw new Error(messages.missingBlockletDid[locale]);
699
+ }
700
+
701
+ const blocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
702
+ if (!blocklet) {
703
+ throw new Error(messages.invalidBlocklet[locale]);
704
+ }
705
+
706
+ // Only the blocklet owner(identified by appDid) can rotate key pair
707
+ if (blocklet.appDid !== userDid) {
708
+ throw new Error(
709
+ {
710
+ zh: `只有 应用DID 所有者(${blocklet.appDid})可以修改钥匙对. 当前用户:${userDid}`,
711
+ en: `Only the owner of AppDID (${blocklet.appDid}) can rotate key pair. Current User: ${userDid}`,
712
+ }[locale]
713
+ );
714
+ }
715
+
716
+ await node.configBlocklet(
717
+ {
718
+ did: blocklet.meta.did,
719
+ configs: [{ key: 'BLOCKLET_APP_SK', value: toHex(keyPair.secret), secure: true }],
720
+ skipHook: true,
721
+ },
722
+ formatContext(Object.assign(req, { user: { did: userDid, fullName: 'Owner', role: 'owner' } }))
723
+ );
724
+ };
725
+
726
+ module.exports = {
727
+ getAuthVcClaim,
728
+ getKeyPairClaim,
729
+ authenticateByVc,
730
+ authenticateByNFT,
731
+ authenticateBySession,
732
+ getRotateKeyPairClaims,
733
+ getOwnershipNFTClaim,
734
+ getLaunchBlockletClaims,
735
+ createLaunchBlockletHandler,
736
+ createRotateKeyPairHandler,
737
+ ensureBlockletPermission,
738
+ getBlockletPermissionChecker,
739
+ getSetupBlockletClaims,
740
+ getTrustedIssuers,
741
+ getServerlessNFTClaim,
742
+ };