@abtnode/core 1.8.37 → 1.8.39

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/api/team.js CHANGED
@@ -180,6 +180,7 @@ class TeamAPI extends EventEmitter {
180
180
  'lastLoginAt',
181
181
  'remark',
182
182
  'avatar',
183
+ 'locale',
183
184
  ])
184
185
  // eslint-disable-next-line function-paren-newline
185
186
  ),
@@ -692,11 +693,7 @@ class TeamAPI extends EventEmitter {
692
693
  // Access Control
693
694
 
694
695
  async getRoles({ teamDid }) {
695
- const rbac = await this.getRBAC(teamDid);
696
-
697
- const roles = await rbac.getRoles();
698
-
699
- return roles.map((d) => pick(d, ['name', 'grants', 'title', 'description']));
696
+ return this.teamManager.getRoles(teamDid);
700
697
  }
701
698
 
702
699
  async createRole({ teamDid, name, description, title, childName, permissions = [] }) {
@@ -13,6 +13,7 @@ const capitalize = require('lodash/capitalize');
13
13
  const { Throttle } = require('stream-throttle');
14
14
  const LRU = require('lru-cache');
15
15
  const joi = require('joi');
16
+ const { isNFTExpired } = require('@abtnode/util/lib/nft');
16
17
  const { sign } = require('@arcblock/jwt');
17
18
  const { isValid: isValidDid } = require('@arcblock/did');
18
19
  const { verifyPresentation } = require('@arcblock/vc');
@@ -25,7 +26,12 @@ const logger = require('@abtnode/logger')('@abtnode/core:blocklet:manager');
25
26
  const downloadFile = require('@abtnode/util/lib/download-file');
26
27
  const Lock = require('@abtnode/util/lib/lock');
27
28
  const { getVcFromPresentation } = require('@abtnode/util/lib/vc');
28
- const { VC_TYPE_BLOCKLET_PURCHASE, WHO_CAN_ACCESS, SERVER_ROLES } = require('@abtnode/constant');
29
+ const {
30
+ VC_TYPE_BLOCKLET_PURCHASE,
31
+ WHO_CAN_ACCESS,
32
+ SERVER_ROLES,
33
+ WHO_CAN_ACCESS_PREFIX_ROLES,
34
+ } = require('@abtnode/constant');
29
35
 
30
36
  const getBlockletEngine = require('@blocklet/meta/lib/engine');
31
37
  const {
@@ -40,13 +46,21 @@ const {
40
46
  forEachChild,
41
47
  getComponentId,
42
48
  getComponentBundleId,
49
+ isPreferenceKey,
50
+ getRolesFromAuthConfig,
43
51
  } = require('@blocklet/meta/lib/util');
44
52
  const getComponentProcessId = require('@blocklet/meta/lib/get-component-process-id');
45
53
  const validateBlockletEntry = require('@blocklet/meta/lib/entry');
46
54
  const toBlockletDid = require('@blocklet/meta/lib/did');
47
55
  const { validateMeta } = require('@blocklet/meta/lib/validate');
48
56
  const { update: updateMetaFile } = require('@blocklet/meta/lib/file');
49
- const { titleSchema, descriptionSchema, mountPointSchema, logoSchema } = require('@blocklet/meta/lib/schema');
57
+ const {
58
+ titleSchema,
59
+ descriptionSchema,
60
+ mountPointSchema,
61
+ logoSchema,
62
+ environmentNameSchema,
63
+ } = require('@blocklet/meta/lib/schema');
50
64
  const hasReservedKey = require('@blocklet/meta/lib/has-reserved-key');
51
65
 
52
66
  const { toExternalBlocklet } = toBlockletDid;
@@ -110,6 +124,7 @@ const {
110
124
  getBlocklet,
111
125
  ensureEnvDefault,
112
126
  getConfigFromPreferences,
127
+ consumeServerlessNFT,
113
128
  } = require('../../util/blocklet');
114
129
  const StoreUtil = require('../../util/store');
115
130
  const states = require('../../states');
@@ -121,6 +136,7 @@ const runMigrationScripts = require('../migration');
121
136
  const hooks = require('../hooks');
122
137
  const { formatName } = require('../../util/get-domain-for-blocklet');
123
138
  const handleInstanceInStore = require('../../util/public-to-store');
139
+ const { getNFTState } = require('../../util');
124
140
 
125
141
  const {
126
142
  isInProgress,
@@ -174,7 +190,7 @@ const getSkippedProcessIds = ({ newBlocklet, oldBlocklet, context = {} }) => {
174
190
  };
175
191
 
176
192
  const getBlockletIndex = (meta, controller) =>
177
- controller ? toExternalBlocklet(meta.name, controller.id) : { did: meta.did, name: meta.name };
193
+ controller ? toExternalBlocklet(meta.name, controller.nftId) : { did: meta.did, name: meta.name };
178
194
 
179
195
  class BlockletManager extends BaseBlockletManager {
180
196
  /**
@@ -378,7 +394,7 @@ class BlockletManager extends BaseBlockletManager {
378
394
  return { meta, isFree, inStore, registryUrl };
379
395
  }
380
396
 
381
- async getBlockletByBundle({ did, name }, context) {
397
+ async getBlockletByBundle({ did, name, serverlessNftId }, context) {
382
398
  if (toBlockletDid(name) !== did) {
383
399
  throw new Error('did and name does not match');
384
400
  }
@@ -387,11 +403,15 @@ class BlockletManager extends BaseBlockletManager {
387
403
  throw new Error('user does not exist');
388
404
  }
389
405
 
406
+ if (context.user.role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER && !serverlessNftId) {
407
+ throw new Error('serverless nft id is required');
408
+ }
409
+
390
410
  let blockletDid = did;
391
411
  let isExternal = false;
392
412
 
393
- if (context.user.role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
394
- blockletDid = toExternalBlocklet(name, context.user.did, { didOnly: true });
413
+ if (serverlessNftId) {
414
+ blockletDid = toExternalBlocklet(name, serverlessNftId, { didOnly: true });
395
415
  isExternal = true;
396
416
  }
397
417
 
@@ -513,7 +533,7 @@ class BlockletManager extends BaseBlockletManager {
513
533
  const error = Array.isArray(err) ? err[0] : err;
514
534
  logger.error('Failed to start blocklet', { error, did, name: blocklet.meta.name });
515
535
  const description = `Start blocklet ${blocklet.meta.name} failed with error: ${error.message}`;
516
- states.notification.create({
536
+ this._createNotification(did, {
517
537
  title: 'Start Blocklet Failed',
518
538
  description,
519
539
  entityType: 'blocklet',
@@ -565,7 +585,12 @@ class BlockletManager extends BaseBlockletManager {
565
585
 
566
586
  if (updateStatus) {
567
587
  const res = await this.status(did, { forceSync: true });
588
+ // send notification to websocket channel
568
589
  this.emit(BlockletEvents.statusChange, res);
590
+
591
+ // send notification to wallet
592
+ this.emit(BlockletEvents.stopped, res);
593
+
569
594
  return res;
570
595
  }
571
596
 
@@ -586,7 +611,7 @@ class BlockletManager extends BaseBlockletManager {
586
611
  const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped);
587
612
  this.emit(BlockletEvents.statusChange, state);
588
613
 
589
- states.notification.create({
614
+ this._createNotification(did, {
590
615
  title: 'Blocklet Restart Failed',
591
616
  description: `Blocklet ${did} restart failed with error: ${err.message || 'queue exception'}`,
592
617
  entityType: 'blocklet',
@@ -637,7 +662,7 @@ class BlockletManager extends BaseBlockletManager {
637
662
  });
638
663
 
639
664
  const doc = await this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
640
- states.notification.create({
665
+ this._createNotification(doc.meta.did, {
641
666
  title: 'Blocklet Deleted',
642
667
  description: `Blocklet ${doc.meta.name}@${doc.meta.version} is deleted.`,
643
668
  entityType: 'blocklet',
@@ -651,7 +676,7 @@ class BlockletManager extends BaseBlockletManager {
651
676
  logger.info('blocklet is corrupted, will delete again', { did });
652
677
  const doc = await this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
653
678
 
654
- states.notification.create({
679
+ this._createNotification(doc.meta.did, {
655
680
  title: 'Blocklet Deleted',
656
681
  description: `Blocklet ${doc.meta.name}@${doc.meta.version} is deleted.`,
657
682
  entityType: 'blocklet',
@@ -776,7 +801,7 @@ class BlockletManager extends BaseBlockletManager {
776
801
  const newBlocklet = await this.ensureBlocklet(rootDid);
777
802
  this.emit(BlockletEvents.upgraded, { blocklet: newBlocklet, context: { ...context, createAuditLog: false } }); // trigger router refresh
778
803
 
779
- states.notification.create({
804
+ this._createNotification(newBlocklet.meta.did, {
780
805
  title: 'Component Deleted',
781
806
  description: `Component ${child.meta.name} of ${newBlocklet.meta.name} is successfully deleted.`,
782
807
  entityType: 'blocklet',
@@ -933,69 +958,88 @@ class BlockletManager extends BaseBlockletManager {
933
958
  // run hook
934
959
  const nodeEnvironments = await states.node.getEnvironments();
935
960
  for (const x of newConfigs) {
936
- if (BLOCKLET_CONFIGURABLE_KEY[x.key] && !!childDids.length) {
937
- logger.error(`Cannot set ${x.key} to child blocklet`, [dids]);
938
- throw new Error(`Cannot set ${x.key} to child blocklet`);
939
- }
961
+ if (x.custom === true) {
962
+ // custom key
963
+ await environmentNameSchema.validateAsync(x.key);
964
+ } else if (BLOCKLET_CONFIGURABLE_KEY[x.key] && x.key.startsWith('BLOCKLET_')) {
965
+ // app key
966
+ if (childDids.length) {
967
+ logger.error(`Cannot set ${x.key} to child blocklet`, [dids]);
968
+ throw new Error(`Cannot set ${x.key} to child blocklet`);
969
+ }
940
970
 
941
- if (x.key === 'BLOCKLET_APP_SK') {
942
- try {
943
- fromSecretKey(x.value);
944
- } catch {
971
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
945
972
  try {
946
- fromSecretKey(x.value, 'eth');
973
+ fromSecretKey(x.value);
947
974
  } catch {
948
- throw new Error('Invalid custom blocklet secret key');
975
+ try {
976
+ fromSecretKey(x.value, 'eth');
977
+ } catch {
978
+ throw new Error('Invalid custom blocklet secret key');
979
+ }
949
980
  }
950
- }
951
981
 
952
- // Ensure sk is not used by other blocklets, otherwise we may encounter appDid collision
953
- const blocklets = await states.blocklet.getBlocklets({});
954
- const others = blocklets.filter((b) => b.meta.did !== did);
955
- if (others.some((b) => b.environments.find((e) => e.key === 'BLOCKLET_APP_SK').value === x.value)) {
956
- throw new Error('Invalid custom blocklet secret key: already used by another blocklet');
982
+ // Ensure sk is not used by other blocklets, otherwise we may encounter appDid collision
983
+ const blocklets = await states.blocklet.getBlocklets({});
984
+ const others = blocklets.filter((b) => b.meta.did !== did);
985
+ if (
986
+ others.some(
987
+ (b) => b.environments.find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK).value === x.value
988
+ )
989
+ ) {
990
+ throw new Error('Invalid custom blocklet secret key: already used by another blocklet');
991
+ }
957
992
  }
958
- }
959
-
960
- if (x.key === 'BLOCKLET_APP_NAME') {
961
- x.value = await titleSchema.validateAsync(x.value);
962
- }
963
993
 
964
- if (x.key === 'BLOCKLET_APP_DESCRIPTION') {
965
- x.value = await descriptionSchema.validateAsync(x.value);
966
- }
994
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_NAME) {
995
+ x.value = await titleSchema.validateAsync(x.value);
996
+ }
967
997
 
968
- if (['BLOCKLET_APP_LOGO', 'BLOCKLET_APP_LOGO_SQUARE'].includes(x.key)) {
969
- x.value = await logoSchema.validateAsync(x.value);
970
- }
998
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_DESCRIPTION) {
999
+ x.value = await descriptionSchema.validateAsync(x.value);
1000
+ }
971
1001
 
972
- if (x.key === 'BLOCKLET_WALLET_TYPE') {
973
- if (['default', 'eth'].includes(x.value) === false) {
974
- throw new Error('Invalid blocklet wallet type, only "default" and "eth" are supported');
1002
+ if (
1003
+ [BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO, BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE].includes(
1004
+ x.key
1005
+ )
1006
+ ) {
1007
+ x.value = await logoSchema.validateAsync(x.value);
975
1008
  }
976
- }
977
1009
 
978
- if (x.key === 'BLOCKLET_DELETABLE') {
979
- if (['yes', 'no'].includes(x.value) === false) {
980
- throw new Error('BLOCKLET_DELETABLE must be either "yes" or "no"');
1010
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE) {
1011
+ if (['default', 'eth'].includes(x.value) === false) {
1012
+ throw new Error('Invalid blocklet wallet type, only "default" and "eth" are supported');
1013
+ }
981
1014
  }
982
- }
983
1015
 
984
- if (x.key === 'BLOCKLET_PASSPORT_COLOR') {
985
- if (x.value && x.value !== 'auto') {
986
- if (x.value.length !== 7 || !isHex(x.value.slice(-6))) {
987
- throw new Error('BLOCKLET_PASSPORT_COLOR must be a hex encoded color, eg. #ffeeaa');
1016
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_DELETABLE) {
1017
+ if (['yes', 'no'].includes(x.value) === false) {
1018
+ throw new Error('BLOCKLET_DELETABLE must be either "yes" or "no"');
988
1019
  }
989
1020
  }
990
- }
991
1021
 
992
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_STORAGE_ENDPOINT) {
993
- if (isEmpty(x.value)) {
994
- throw new Error(`${x.key} can not be empty`);
1022
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_PASSPORT_COLOR) {
1023
+ if (x.value && x.value !== 'auto') {
1024
+ if (x.value.length !== 7 || !isHex(x.value.slice(-6))) {
1025
+ throw new Error('BLOCKLET_PASSPORT_COLOR must be a hex encoded color, eg. #ffeeaa');
1026
+ }
1027
+ }
995
1028
  }
996
1029
 
997
- if (!urlHttp(x.value)) {
998
- throw new Error(`${x.key}(${x.value}) is not a valid http address`);
1030
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT) {
1031
+ if (isEmpty(x.value)) {
1032
+ throw new Error(`${x.key} can not be empty`);
1033
+ }
1034
+
1035
+ if (!urlHttp(x.value)) {
1036
+ throw new Error(`${x.key}(${x.value}) is not a valid http address`);
1037
+ }
1038
+ }
1039
+ } else if (!BLOCKLET_CONFIGURABLE_KEY[x.key] && !isPreferenceKey(x)) {
1040
+ if (!(blocklet.meta.environments || []).some((y) => y.name === x.key)) {
1041
+ // forbid unknown format key
1042
+ throw new Error(`unknown format key: ${x.key}`);
999
1043
  }
1000
1044
  }
1001
1045
 
@@ -1037,18 +1081,53 @@ class BlockletManager extends BaseBlockletManager {
1037
1081
  }
1038
1082
 
1039
1083
  async updateWhoCanAccess({ did, whoCanAccess }) {
1040
- if (!(await this.hasBlocklet({ did }))) {
1041
- throw new Error('The blocklet does not exist');
1042
- }
1084
+ const dids = Array.isArray(did) ? did : [did];
1043
1085
 
1044
- if (!Object.values(WHO_CAN_ACCESS).includes(whoCanAccess)) {
1045
- logger.error(`The value of whoCanAccess is invalid: ${whoCanAccess}`);
1046
- throw new Error('the value is invalid');
1086
+ const [rootDid] = dids;
1087
+
1088
+ const isApp = dids.length === 1;
1089
+
1090
+ try {
1091
+ // check exist
1092
+ if (!(await this.hasBlocklet({ did: rootDid }))) {
1093
+ throw new Error('The blocklet does not exist');
1094
+ }
1095
+
1096
+ // validate input
1097
+ if (
1098
+ !whoCanAccess.startsWith(WHO_CAN_ACCESS_PREFIX_ROLES) &&
1099
+ !Object.values(WHO_CAN_ACCESS).includes(whoCanAccess)
1100
+ ) {
1101
+ throw new Error(`The value of whoCanAccess is invalid: ${whoCanAccess}`);
1102
+ } else if (whoCanAccess.startsWith(WHO_CAN_ACCESS_PREFIX_ROLES)) {
1103
+ if (!whoCanAccess.substring(WHO_CAN_ACCESS_PREFIX_ROLES.length).trim()) {
1104
+ throw new Error('Roles in whoCanAccess cannot be empty');
1105
+ }
1106
+
1107
+ if (whoCanAccess.length > 200) {
1108
+ throw new Error('The length of whoCanAccess should not exceed 200');
1109
+ }
1110
+
1111
+ const roleNames = (await this.teamManager.getRoles(rootDid)).map((x) => x.name);
1112
+ const accessRoleNames = getRolesFromAuthConfig({ whoCanAccess });
1113
+ const noExistNames = accessRoleNames.filter((x) => !roleNames.includes(x));
1114
+ if (noExistNames.length) {
1115
+ throw new Error(`Found no exist role names: ${noExistNames.join(',')}`);
1116
+ }
1117
+ }
1118
+ } catch (error) {
1119
+ logger.error(error.message);
1120
+ throw error;
1047
1121
  }
1048
1122
 
1049
- await states.blockletExtras.setSettings(did, { whoCanAccess });
1123
+ if (isApp) {
1124
+ await states.blockletExtras.setSettings(rootDid, { whoCanAccess });
1125
+ } else {
1126
+ const configs = [{ key: BLOCKLET_CONFIGURABLE_KEY.COMPONENT_ACCESS_WHO, value: whoCanAccess }];
1127
+ await states.blockletExtras.setConfigs(dids, configs);
1128
+ }
1050
1129
 
1051
- const blocklet = await this.ensureBlocklet(did);
1130
+ const blocklet = await this.ensureBlocklet(rootDid);
1052
1131
 
1053
1132
  this.emit(BlockletEvents.updated, { meta: { did: blocklet.meta.did } });
1054
1133
 
@@ -1360,7 +1439,7 @@ class BlockletManager extends BaseBlockletManager {
1360
1439
  logger.error('queue failed', { entity: 'blocklet', action, did, version, name, error: err });
1361
1440
  await this._rollback(action, did, oldBlocklet);
1362
1441
  this.emit(`blocklet.${action}.failed`, { did, version, err });
1363
- states.notification.create({
1442
+ this._createNotification(did, {
1364
1443
  title: `Blocklet ${capitalize(action)} Failed`,
1365
1444
  description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message || 'queue exception'}`,
1366
1445
  entityType: 'blocklet',
@@ -1759,7 +1838,7 @@ class BlockletManager extends BaseBlockletManager {
1759
1838
  message: err.message,
1760
1839
  },
1761
1840
  });
1762
- states.notification.create({
1841
+ this._createNotification(did, {
1763
1842
  title: 'Blocklet Download Failed',
1764
1843
  description: `Blocklet ${name}@${version} download failed with error: ${err.message}`,
1765
1844
  entityType: 'blocklet',
@@ -1897,7 +1976,7 @@ class BlockletManager extends BaseBlockletManager {
1897
1976
  await this.deleteProcess({ did }, context);
1898
1977
  await states.blocklet.setBlockletStatus(did, BlockletStatus.error);
1899
1978
 
1900
- states.notification.create({
1979
+ this._createNotification(did, {
1901
1980
  title: 'Blocklet Start Failed',
1902
1981
  description: `Blocklet ${name} start failed: ${error.message}`,
1903
1982
  entityType: 'blocklet',
@@ -2519,6 +2598,11 @@ class BlockletManager extends BaseBlockletManager {
2519
2598
  await refreshAccessibleExternalNodeIp(nodeInfo);
2520
2599
  },
2521
2600
  },
2601
+ {
2602
+ name: 'delete-expired-external-blocklet',
2603
+ time: '0 */30 * * * *', // 30min
2604
+ fn: () => this._deleteExpiredExternalBlocklet(),
2605
+ },
2522
2606
  ];
2523
2607
  }
2524
2608
 
@@ -2638,7 +2722,7 @@ class BlockletManager extends BaseBlockletManager {
2638
2722
  logger.error('failed to remove blocklet on install error', { did: meta.did, error: e });
2639
2723
  }
2640
2724
 
2641
- states.notification.create({
2725
+ this._createNotification(did, {
2642
2726
  title: 'Blocklet Install Failed',
2643
2727
  description: `Blocklet ${name}@${version} install failed with error: ${err.message || 'queue exception'}`,
2644
2728
  entityType: 'blocklet',
@@ -2651,7 +2735,7 @@ class BlockletManager extends BaseBlockletManager {
2651
2735
  return blocklet1;
2652
2736
  } catch (err) {
2653
2737
  logger.error('failed to install blocklet', { name, did, version, error: err });
2654
- states.notification.create({
2738
+ this._createNotification(did, {
2655
2739
  title: 'Blocklet Install Failed',
2656
2740
  description: `Blocklet ${name}@${version} install failed with error: ${err.message || 'queue exception'}`,
2657
2741
  entityType: 'blocklet',
@@ -2749,7 +2833,7 @@ class BlockletManager extends BaseBlockletManager {
2749
2833
  downgrade: BlockletEvents.downgradeFailed,
2750
2834
  };
2751
2835
  this.emit(eventNames[action], { blocklet: oldBlocklet, context });
2752
- states.notification.create({
2836
+ this._createNotification(did, {
2753
2837
  title: `Blocklet ${capitalize(action)} Failed`,
2754
2838
  description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message || 'queue exception'}`,
2755
2839
  entityType: 'blocklet',
@@ -2940,7 +3024,12 @@ class BlockletManager extends BaseBlockletManager {
2940
3024
  });
2941
3025
  }
2942
3026
 
2943
- states.notification.create({
3027
+ if (blocklet.controller && process.env.NODE_ENV !== 'test') {
3028
+ const nodeInfo = await states.node.read();
3029
+ await consumeServerlessNFT({ nftId: blocklet.controller.nftId, nodeInfo, blocklet });
3030
+ }
3031
+
3032
+ this._createNotification(did, {
2944
3033
  title: 'Blocklet Installed',
2945
3034
  description: `Blocklet ${meta.name}@${meta.version} is installed successfully. (Source: ${
2946
3035
  deployedFrom || fromBlockletSource(source)
@@ -2963,7 +3052,7 @@ class BlockletManager extends BaseBlockletManager {
2963
3052
  message: err.message,
2964
3053
  },
2965
3054
  });
2966
- states.notification.create({
3055
+ this._createNotification(did, {
2967
3056
  title: 'Blocklet Install Failed',
2968
3057
  description: `Blocklet ${meta.name}@${meta.version} install failed with error: ${err.message}`,
2969
3058
  entityType: 'blocklet',
@@ -3058,7 +3147,7 @@ class BlockletManager extends BaseBlockletManager {
3058
3147
  downgrade: BlockletEvents.downgraded,
3059
3148
  };
3060
3149
  this.emit(eventNames[action], { blocklet, context });
3061
- states.notification.create({
3150
+ this._createNotification(did, {
3062
3151
  title: `Blocklet ${capitalize(action)} Success`,
3063
3152
  description: `Blocklet ${name}@${version} ${action} successfully. (Source: ${
3064
3153
  deployedFrom || fromBlockletSource(source)
@@ -3086,9 +3175,9 @@ class BlockletManager extends BaseBlockletManager {
3086
3175
  upgrade: BlockletEvents.upgradeFailed,
3087
3176
  downgrade: BlockletEvents.downgradeFailed,
3088
3177
  };
3089
- this.emit(eventNames[action], { blocklet: oldBlocklet, context });
3178
+ this.emit(eventNames[action], { blocklet: { ...oldBlocklet, error: { message: err.message } }, context });
3090
3179
 
3091
- states.notification.create({
3180
+ this._createNotification(did, {
3092
3181
  title: `Blocklet ${capitalize(action)} Failed`,
3093
3182
  description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message}`,
3094
3183
  entityType: 'blocklet',
@@ -3643,6 +3732,64 @@ class BlockletManager extends BaseBlockletManager {
3643
3732
 
3644
3733
  await forEachBlocklet(blocklet, postInstall, { parallel: true });
3645
3734
  }
3735
+
3736
+ async _createNotification(did, notification) {
3737
+ try {
3738
+ const isExternal = await states.blocklet.isExternalBlocklet(did);
3739
+
3740
+ if (isExternal) {
3741
+ return;
3742
+ }
3743
+
3744
+ await states.notification.create(notification);
3745
+ } catch (error) {
3746
+ logger.error('create notification failed', { error });
3747
+ }
3748
+ }
3749
+
3750
+ async _deleteExpiredExternalBlocklet() {
3751
+ try {
3752
+ logger.info('start check expired external blocklet');
3753
+ const blocklets = await states.blocklet.getBlocklets({
3754
+ controller: {
3755
+ $exists: true,
3756
+ },
3757
+ });
3758
+
3759
+ const nodeInfo = await states.node.read();
3760
+
3761
+ const tasks = blocklets.map(async (blocklet) => {
3762
+ try {
3763
+ const assetState = await getNFTState(nodeInfo.launcher.chainHost, blocklet.controller.nftId);
3764
+ const isExpired = isNFTExpired(assetState);
3765
+ if (isExpired) {
3766
+ logger.info('the blocklet already expired', {
3767
+ blockletId: blocklet._id,
3768
+ nftId: blocklet.controller.nftId,
3769
+ });
3770
+
3771
+ // FIXME: 后面需要考虑将数据保留一段时间
3772
+ await this.delete({ did: blocklet.meta.did, keepData: false, keepConfigs: false, keepLogsDir: false });
3773
+ logger.info('the expired blocklet already deleted', {
3774
+ blockletId: blocklet._id,
3775
+ nftId: blocklet.controller.nftId,
3776
+ });
3777
+ }
3778
+ } catch (error) {
3779
+ logger.error('get asset state failed when check expired external blocklet', {
3780
+ blockletId: blocklet._id,
3781
+ nftId: blocklet.controller.nftId,
3782
+ });
3783
+ }
3784
+ });
3785
+
3786
+ await Promise.all(tasks);
3787
+
3788
+ logger.info('check expired external blocklet end');
3789
+ } catch (error) {
3790
+ logger.info('check expired external blocklet failed', { error });
3791
+ }
3792
+ }
3646
3793
  }
3647
3794
 
3648
3795
  module.exports = BlockletManager;
package/lib/index.js CHANGED
@@ -6,7 +6,6 @@ const Cron = require('@abtnode/cron');
6
6
 
7
7
  const logger = require('@abtnode/logger')('@abtnode/core');
8
8
  const { fromBlockletStatus, toBlockletStatus, fromBlockletSource, toBlockletSource } = require('@blocklet/constant');
9
- const { getServiceMetas } = require('@blocklet/meta/lib/service');
10
9
  const { listProviders } = require('@abtnode/router-provider');
11
10
  const { DEFAULT_CERTIFICATE_EMAIL, EVENTS } = require('@abtnode/constant');
12
11
 
@@ -384,9 +383,6 @@ function ABTNode(options) {
384
383
  ),
385
384
  endSession: (params, context) => states.session.end(params.id, context),
386
385
 
387
- // Services
388
- getServices: (params, context) => getServiceMetas({ stringifySchema: true }, context),
389
-
390
386
  // Utilities: moved here because some deps require native build
391
387
  getSysInfo,
392
388
  getSysInfoEmitter: (interval) => new SysInfoEmitter(interval),
@@ -106,6 +106,21 @@ class BlockletState extends BaseState {
106
106
  });
107
107
  }
108
108
 
109
+ async isExternalBlocklet(did) {
110
+ if (!did) {
111
+ return false;
112
+ }
113
+
114
+ const exist = await this.findOne({
115
+ $or: [{ 'meta.did': did }, { appDid: did }],
116
+ controller: {
117
+ $exists: true,
118
+ },
119
+ });
120
+
121
+ return !!exist;
122
+ }
123
+
109
124
  async getBlockletStatus(did) {
110
125
  return new Promise((resolve, reject) => {
111
126
  if (!did) {
@@ -76,44 +76,30 @@ class NotificationState extends BaseState {
76
76
  }
77
77
 
78
78
  // eslint-disable-next-line no-unused-vars
79
- read({ id }, context) {
79
+ async read({ id }, context) {
80
80
  const idList = id.split(',').map((x) => x.trim());
81
81
  logger.info('mark notification as read', { idList });
82
82
 
83
- return new Promise((resolve, reject) => {
84
- this.update(
85
- { _id: { $in: idList } },
86
- { $set: { read: true } },
87
- { multi: true, upsert: false, returnUpdatedDocs: false },
88
- (err, numAffected) => {
89
- if (err) {
90
- return reject(err);
91
- }
83
+ const [numAffected] = await this.update(
84
+ { _id: { $in: idList } },
85
+ { $set: { read: true } },
86
+ { multi: true, upsert: false, returnUpdatedDocs: false }
87
+ );
92
88
 
93
- return resolve(numAffected);
94
- }
95
- );
96
- });
89
+ return numAffected;
97
90
  }
98
91
 
99
92
  // eslint-disable-next-line no-unused-vars
100
- unread({ id }, context) {
93
+ async unread({ id }, context) {
101
94
  const idList = Array.isArray(id) ? id : [id];
102
95
 
103
- return new Promise((resolve, reject) => {
104
- this.update(
105
- { _id: { $in: idList } },
106
- { $set: { read: false } },
107
- { multi: true, upsert: false, returnUpdatedDocs: false },
108
- (err, numAffected) => {
109
- if (err) {
110
- return reject(err);
111
- }
96
+ const [numAffected] = await this.update(
97
+ { _id: { $in: idList } },
98
+ { $set: { read: false } },
99
+ { multi: true, upsert: false, returnUpdatedDocs: false }
100
+ );
112
101
 
113
- return resolve(numAffected);
114
- }
115
- );
116
- });
102
+ return numAffected;
117
103
  }
118
104
  }
119
105
 
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const { EventEmitter } = require('events');
6
6
  const upperFirst = require('lodash/upperFirst');
7
7
  const get = require('lodash/get');
8
+ const pick = require('lodash/pick');
8
9
 
9
10
  const { createRBAC, MemoryStorage, NedbStorage } = require('@abtnode/rbac');
10
11
  const logger = require('@abtnode/logger')('@abtnode/core:team:manager');
@@ -314,6 +315,14 @@ class TeamManager extends EventEmitter {
314
315
  return owner;
315
316
  }
316
317
 
318
+ async getRoles(did) {
319
+ const rbac = await this.getRBAC(did);
320
+
321
+ const roles = await rbac.getRoles();
322
+
323
+ return roles.map((d) => pick(d, ['name', 'grants', 'title', 'description']));
324
+ }
325
+
317
326
  async initTeam(did) {
318
327
  if (!did) {
319
328
  logger.error('initTeam: did does not exist');
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
+ const joinURL = require('url-join');
5
6
  const shelljs = require('shelljs');
6
7
  const os = require('os');
7
8
  const tar = require('tar');
@@ -10,8 +11,12 @@ const streamToPromise = require('stream-to-promise');
10
11
  const { Throttle } = require('stream-throttle');
11
12
  const ssri = require('ssri');
12
13
  const diff = require('deep-diff');
14
+ const axios = require('@abtnode/util/lib/axios');
15
+ const { stableStringify } = require('@arcblock/vc');
13
16
 
14
- const { toHex } = require('@ocap/util');
17
+ const { fromSecretKey, WalletType } = require('@ocap/wallet');
18
+ const { toHex, toBase58 } = require('@ocap/util');
19
+ const { types } = require('@ocap/mcrypto');
15
20
  const { isValid: isValidDid } = require('@arcblock/did');
16
21
  const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
17
22
  const pm2 = require('@abtnode/util/lib/async-pm2');
@@ -1257,7 +1262,7 @@ const needBlockletDownload = (blocklet, oldBlocklet) => {
1257
1262
  return true;
1258
1263
  }
1259
1264
 
1260
- return !(get(oldBlocklet, 'meta.dist.integrity') === get(blocklet, 'meta.dist.integrity'));
1265
+ return get(oldBlocklet, 'meta.dist.integrity') !== get(blocklet, 'meta.dist.integrity');
1261
1266
  };
1262
1267
 
1263
1268
  const findAvailableDid = (meta, siblings) => {
@@ -1436,7 +1441,35 @@ const getConfigFromPreferences = (blocklet) => {
1436
1441
  return result;
1437
1442
  };
1438
1443
 
1444
+ const consumeServerlessNFT = async ({ nftId, nodeInfo, blocklet }) => {
1445
+ try {
1446
+ const { url } = nodeInfo.launcher;
1447
+ const type = WalletType({
1448
+ role: types.RoleType.ROLE_APPLICATION,
1449
+ pk: types.KeyType.ED25519,
1450
+ hash: types.HashType.SHA3,
1451
+ });
1452
+ const wallet = fromSecretKey(nodeInfo.sk, type);
1453
+ const appURL = blocklet.environments.find((item) => item.key === 'BLOCKLET_APP_URL').value;
1454
+
1455
+ const body = { nftId, appURL };
1456
+
1457
+ const { data } = await axios.post(joinURL(url, '/api/serverless/consume'), body, {
1458
+ headers: {
1459
+ 'x-sig': toBase58(wallet.sign(stableStringify(body))),
1460
+ },
1461
+ });
1462
+
1463
+ logger.error('consume serverless nft success', { nftId, hash: data.hash });
1464
+ } catch (error) {
1465
+ logger.error('consume serverless nft failed', { nftId, error });
1466
+
1467
+ throw new Error(`consume nft ${nftId} failed`);
1468
+ }
1469
+ };
1470
+
1439
1471
  module.exports = {
1472
+ consumeServerlessNFT,
1440
1473
  forEachBlocklet,
1441
1474
  getBlockletMetaFromUrl: (url) => getBlockletMetaFromUrl(url, { logger }),
1442
1475
  parseChildrenFromMeta,
package/lib/util/index.js CHANGED
@@ -394,6 +394,49 @@ const getDelegateState = async (chainHost, address) => {
394
394
  return get(result.data, 'data.getDelegateState.state');
395
395
  };
396
396
 
397
+ const getNFTState = async (chainHost, nftId) => {
398
+ const url = joinUrl(new URL(chainHost).origin, '/api/gql/');
399
+
400
+ const result = await axios.post(
401
+ url,
402
+ JSON.stringify({
403
+ query: `{
404
+ getAssetState(address: "${nftId}") {
405
+ state {
406
+ address
407
+ data {
408
+ typeUrl
409
+ value
410
+ }
411
+ display {
412
+ type
413
+ content
414
+ }
415
+ issuer
416
+ owner
417
+ parent
418
+ tags
419
+ }
420
+ }
421
+ }`,
422
+ }),
423
+ {
424
+ headers: {
425
+ 'Content-Type': 'application/json',
426
+ Accept: 'application/json',
427
+ },
428
+ timeout: 60 * 1000,
429
+ }
430
+ );
431
+
432
+ const state = get(result, 'data.data.getAssetState.state');
433
+ if (state && state.data.typeUrl === 'json') {
434
+ state.data.value = JSON.parse(state.data.value);
435
+ }
436
+
437
+ return state;
438
+ };
439
+
397
440
  const lib = {
398
441
  validateOwner,
399
442
  getProviderFromNodeInfo,
@@ -428,6 +471,7 @@ const lib = {
428
471
  memoizeAsync,
429
472
  getStateCrons,
430
473
  getDelegateState,
474
+ getNFTState,
431
475
  };
432
476
 
433
477
  module.exports = lib;
@@ -4,11 +4,9 @@ const { didExtension } = require('@blocklet/meta/lib/extension');
4
4
  const Joi = JOI.extend(didExtension);
5
5
 
6
6
  const blockletController = Joi.object({
7
- id: Joi.DID().required(), // userDid
8
7
  nftId: Joi.DID().required(),
9
8
  nftOwner: Joi.DID().required(),
10
9
  appMaxCount: Joi.number().required().min(1),
11
- expireDate: Joi.date(),
12
10
  }).options({ stripUnknown: true });
13
11
 
14
12
  module.exports = {
@@ -10,6 +10,10 @@ const roleNameSchema = JOI.string()
10
10
  throw new Error('role name cannot start with "blocklet"');
11
11
  }
12
12
 
13
+ if (/[^a-zA-z0-9]/.test(value)) {
14
+ throw new Error('role name can include only numbers or letters');
15
+ }
16
+
13
17
  return value;
14
18
  });
15
19
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.8.37",
6
+ "version": "1.8.39",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,32 +19,32 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@abtnode/auth": "1.8.37",
23
- "@abtnode/certificate-manager": "1.8.37",
24
- "@abtnode/constant": "1.8.37",
25
- "@abtnode/cron": "1.8.37",
26
- "@abtnode/db": "1.8.37",
27
- "@abtnode/logger": "1.8.37",
28
- "@abtnode/queue": "1.8.37",
29
- "@abtnode/rbac": "1.8.37",
30
- "@abtnode/router-provider": "1.8.37",
31
- "@abtnode/static-server": "1.8.37",
32
- "@abtnode/timemachine": "1.8.37",
33
- "@abtnode/util": "1.8.37",
34
- "@arcblock/did": "1.18.15",
22
+ "@abtnode/auth": "1.8.39",
23
+ "@abtnode/certificate-manager": "1.8.39",
24
+ "@abtnode/constant": "1.8.39",
25
+ "@abtnode/cron": "1.8.39",
26
+ "@abtnode/db": "1.8.39",
27
+ "@abtnode/logger": "1.8.39",
28
+ "@abtnode/queue": "1.8.39",
29
+ "@abtnode/rbac": "1.8.39",
30
+ "@abtnode/router-provider": "1.8.39",
31
+ "@abtnode/static-server": "1.8.39",
32
+ "@abtnode/timemachine": "1.8.39",
33
+ "@abtnode/util": "1.8.39",
34
+ "@arcblock/did": "1.18.18",
35
35
  "@arcblock/did-motif": "^1.1.10",
36
- "@arcblock/did-util": "1.18.15",
37
- "@arcblock/event-hub": "1.18.15",
38
- "@arcblock/jwt": "^1.18.15",
36
+ "@arcblock/did-util": "1.18.18",
37
+ "@arcblock/event-hub": "1.18.18",
38
+ "@arcblock/jwt": "^1.18.18",
39
39
  "@arcblock/pm2-events": "^0.0.5",
40
- "@arcblock/vc": "1.18.15",
41
- "@blocklet/constant": "1.8.37",
42
- "@blocklet/meta": "1.8.37",
43
- "@blocklet/sdk": "1.8.37",
40
+ "@arcblock/vc": "1.18.18",
41
+ "@blocklet/constant": "1.8.39",
42
+ "@blocklet/meta": "1.8.39",
43
+ "@blocklet/sdk": "1.8.39",
44
44
  "@fidm/x509": "^1.2.1",
45
- "@ocap/mcrypto": "1.18.15",
46
- "@ocap/util": "1.18.15",
47
- "@ocap/wallet": "1.18.15",
45
+ "@ocap/mcrypto": "1.18.18",
46
+ "@ocap/util": "1.18.18",
47
+ "@ocap/wallet": "1.18.18",
48
48
  "@slack/webhook": "^5.0.4",
49
49
  "axios": "^0.27.2",
50
50
  "axon": "^2.0.3",
@@ -82,5 +82,5 @@
82
82
  "express": "^4.18.2",
83
83
  "jest": "^27.5.1"
84
84
  },
85
- "gitHead": "f26f451c6e2b1168b36f78269eafdf3f671236bf"
85
+ "gitHead": "cf2223c4d9a999993b2be1c943dd75b4221593c3"
86
86
  }