@abtnode/core 1.8.36 → 1.8.38

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.
@@ -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;
@@ -109,6 +123,8 @@ const {
109
123
  ensureMeta,
110
124
  getBlocklet,
111
125
  ensureEnvDefault,
126
+ getConfigFromPreferences,
127
+ consumeServerlessNFT,
112
128
  } = require('../../util/blocklet');
113
129
  const StoreUtil = require('../../util/store');
114
130
  const states = require('../../states');
@@ -120,6 +136,7 @@ const runMigrationScripts = require('../migration');
120
136
  const hooks = require('../hooks');
121
137
  const { formatName } = require('../../util/get-domain-for-blocklet');
122
138
  const handleInstanceInStore = require('../../util/public-to-store');
139
+ const { getNFTState } = require('../../util');
123
140
 
124
141
  const {
125
142
  isInProgress,
@@ -173,7 +190,7 @@ const getSkippedProcessIds = ({ newBlocklet, oldBlocklet, context = {} }) => {
173
190
  };
174
191
 
175
192
  const getBlockletIndex = (meta, controller) =>
176
- 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 };
177
194
 
178
195
  class BlockletManager extends BaseBlockletManager {
179
196
  /**
@@ -269,8 +286,8 @@ class BlockletManager extends BaseBlockletManager {
269
286
  }
270
287
 
271
288
  if (source === BlockletSource.registry) {
272
- const { did, controller, sync, delay } = params;
273
- return this._installFromStore({ did, controller, sync, delay }, context);
289
+ const { did, controller, sync, delay, storeUrl } = params;
290
+ return this._installFromStore({ did, controller, sync, delay, storeUrl }, context);
274
291
  }
275
292
 
276
293
  if (source === BlockletSource.custom) {
@@ -377,7 +394,7 @@ class BlockletManager extends BaseBlockletManager {
377
394
  return { meta, isFree, inStore, registryUrl };
378
395
  }
379
396
 
380
- async getBlockletByBundle({ did, name }, context) {
397
+ async getBlockletByBundle({ did, name, serverlessNftId }, context) {
381
398
  if (toBlockletDid(name) !== did) {
382
399
  throw new Error('did and name does not match');
383
400
  }
@@ -386,11 +403,15 @@ class BlockletManager extends BaseBlockletManager {
386
403
  throw new Error('user does not exist');
387
404
  }
388
405
 
406
+ if (context.user.role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER && !serverlessNftId) {
407
+ throw new Error('serverless nft id is required');
408
+ }
409
+
389
410
  let blockletDid = did;
390
411
  let isExternal = false;
391
412
 
392
- if (context.user.role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
393
- blockletDid = toExternalBlocklet(name, context.user.did, { didOnly: true });
413
+ if (serverlessNftId) {
414
+ blockletDid = toExternalBlocklet(name, serverlessNftId, { didOnly: true });
394
415
  isExternal = true;
395
416
  }
396
417
 
@@ -512,7 +533,7 @@ class BlockletManager extends BaseBlockletManager {
512
533
  const error = Array.isArray(err) ? err[0] : err;
513
534
  logger.error('Failed to start blocklet', { error, did, name: blocklet.meta.name });
514
535
  const description = `Start blocklet ${blocklet.meta.name} failed with error: ${error.message}`;
515
- states.notification.create({
536
+ this._createNotification(did, {
516
537
  title: 'Start Blocklet Failed',
517
538
  description,
518
539
  entityType: 'blocklet',
@@ -564,7 +585,12 @@ class BlockletManager extends BaseBlockletManager {
564
585
 
565
586
  if (updateStatus) {
566
587
  const res = await this.status(did, { forceSync: true });
588
+ // send notification to websocket channel
567
589
  this.emit(BlockletEvents.statusChange, res);
590
+
591
+ // send notification to wallet
592
+ this.emit(BlockletEvents.stopped, res);
593
+
568
594
  return res;
569
595
  }
570
596
 
@@ -585,7 +611,7 @@ class BlockletManager extends BaseBlockletManager {
585
611
  const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped);
586
612
  this.emit(BlockletEvents.statusChange, state);
587
613
 
588
- states.notification.create({
614
+ this._createNotification(did, {
589
615
  title: 'Blocklet Restart Failed',
590
616
  description: `Blocklet ${did} restart failed with error: ${err.message || 'queue exception'}`,
591
617
  entityType: 'blocklet',
@@ -636,7 +662,7 @@ class BlockletManager extends BaseBlockletManager {
636
662
  });
637
663
 
638
664
  const doc = await this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
639
- states.notification.create({
665
+ this._createNotification(doc.meta.did, {
640
666
  title: 'Blocklet Deleted',
641
667
  description: `Blocklet ${doc.meta.name}@${doc.meta.version} is deleted.`,
642
668
  entityType: 'blocklet',
@@ -650,7 +676,7 @@ class BlockletManager extends BaseBlockletManager {
650
676
  logger.info('blocklet is corrupted, will delete again', { did });
651
677
  const doc = await this._deleteBlocklet({ did, keepData, keepLogsDir, keepConfigs }, context);
652
678
 
653
- states.notification.create({
679
+ this._createNotification(doc.meta.did, {
654
680
  title: 'Blocklet Deleted',
655
681
  description: `Blocklet ${doc.meta.name}@${doc.meta.version} is deleted.`,
656
682
  entityType: 'blocklet',
@@ -775,7 +801,7 @@ class BlockletManager extends BaseBlockletManager {
775
801
  const newBlocklet = await this.ensureBlocklet(rootDid);
776
802
  this.emit(BlockletEvents.upgraded, { blocklet: newBlocklet, context: { ...context, createAuditLog: false } }); // trigger router refresh
777
803
 
778
- states.notification.create({
804
+ this._createNotification(newBlocklet.meta.did, {
779
805
  title: 'Component Deleted',
780
806
  description: `Component ${child.meta.name} of ${newBlocklet.meta.name} is successfully deleted.`,
781
807
  entityType: 'blocklet',
@@ -836,6 +862,7 @@ class BlockletManager extends BaseBlockletManager {
836
862
  return result;
837
863
  }
838
864
 
865
+ // Get blocklet by blockletDid or appDid
839
866
  async detail({ did, attachConfig = true, attachRuntimeInfo = true }, context) {
840
867
  if (!did) {
841
868
  throw new Error('did should not be empty');
@@ -931,69 +958,88 @@ class BlockletManager extends BaseBlockletManager {
931
958
  // run hook
932
959
  const nodeEnvironments = await states.node.getEnvironments();
933
960
  for (const x of newConfigs) {
934
- if (BLOCKLET_CONFIGURABLE_KEY[x.key] && !!childDids.length) {
935
- logger.error(`Cannot set ${x.key} to child blocklet`, [dids]);
936
- throw new Error(`Cannot set ${x.key} to child blocklet`);
937
- }
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
+ }
938
970
 
939
- if (x.key === 'BLOCKLET_APP_SK') {
940
- try {
941
- fromSecretKey(x.value);
942
- } catch {
971
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
943
972
  try {
944
- fromSecretKey(x.value, 'eth');
973
+ fromSecretKey(x.value);
945
974
  } catch {
946
- 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
+ }
947
980
  }
948
- }
949
981
 
950
- // Ensure sk is not used by other blocklets, otherwise we may encounter appDid collision
951
- const blocklets = await states.blocklet.getBlocklets({});
952
- const others = blocklets.filter((b) => b.meta.did !== did);
953
- if (others.some((b) => b.environments.find((e) => e.key === 'BLOCKLET_APP_SK').value === x.value)) {
954
- 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
+ }
955
992
  }
956
- }
957
993
 
958
- if (x.key === 'BLOCKLET_APP_NAME') {
959
- x.value = await titleSchema.validateAsync(x.value);
960
- }
961
-
962
- if (x.key === 'BLOCKLET_APP_DESCRIPTION') {
963
- x.value = await descriptionSchema.validateAsync(x.value);
964
- }
994
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_NAME) {
995
+ x.value = await titleSchema.validateAsync(x.value);
996
+ }
965
997
 
966
- if (['BLOCKLET_APP_LOGO', 'BLOCKLET_APP_LOGO_SQUARE'].includes(x.key)) {
967
- x.value = await logoSchema.validateAsync(x.value);
968
- }
998
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_DESCRIPTION) {
999
+ x.value = await descriptionSchema.validateAsync(x.value);
1000
+ }
969
1001
 
970
- if (x.key === 'BLOCKLET_WALLET_TYPE') {
971
- if (['default', 'eth'].includes(x.value) === false) {
972
- 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);
973
1008
  }
974
- }
975
1009
 
976
- if (x.key === 'BLOCKLET_DELETABLE') {
977
- if (['yes', 'no'].includes(x.value) === false) {
978
- 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
+ }
979
1014
  }
980
- }
981
1015
 
982
- if (x.key === 'BLOCKLET_PASSPORT_COLOR') {
983
- if (x.value && x.value !== 'auto') {
984
- if (x.value.length !== 7 || !isHex(x.value.slice(-6))) {
985
- 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"');
986
1019
  }
987
1020
  }
988
- }
989
1021
 
990
- if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_STORAGE_ENDPOINT) {
991
- if (isEmpty(x.value)) {
992
- 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
+ }
993
1028
  }
994
1029
 
995
- if (!urlHttp(x.value)) {
996
- 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}`);
997
1043
  }
998
1044
  }
999
1045
 
@@ -1035,18 +1081,53 @@ class BlockletManager extends BaseBlockletManager {
1035
1081
  }
1036
1082
 
1037
1083
  async updateWhoCanAccess({ did, whoCanAccess }) {
1038
- if (!(await this.hasBlocklet({ did }))) {
1039
- throw new Error('The blocklet does not exist');
1040
- }
1084
+ const dids = Array.isArray(did) ? did : [did];
1085
+
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
+ }
1041
1106
 
1042
- if (!Object.values(WHO_CAN_ACCESS).includes(whoCanAccess)) {
1043
- logger.error(`The value of whoCanAccess is invalid: ${whoCanAccess}`);
1044
- throw new Error('the value is invalid');
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;
1045
1121
  }
1046
1122
 
1047
- 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
+ }
1048
1129
 
1049
- const blocklet = await this.ensureBlocklet(did);
1130
+ const blocklet = await this.ensureBlocklet(rootDid);
1050
1131
 
1051
1132
  this.emit(BlockletEvents.updated, { meta: { did: blocklet.meta.did } });
1052
1133
 
@@ -1130,23 +1211,23 @@ class BlockletManager extends BaseBlockletManager {
1130
1211
  /**
1131
1212
  * upgrade blocklet from registry
1132
1213
  */
1133
- async upgrade({ did, registryUrl, sync }, context) {
1214
+ async upgrade({ did, storeUrl, sync }, context) {
1134
1215
  const blocklet = await states.blocklet.getBlocklet(did);
1135
1216
 
1136
- if (!registryUrl && blocklet.source === BlockletSource.url) {
1217
+ if (!storeUrl && blocklet.source === BlockletSource.url) {
1137
1218
  return this._installFromUrl({ url: blocklet.deployedFrom }, context);
1138
1219
  }
1139
1220
 
1140
- // TODO: 查看了下目前页面中的升级按钮,都是会传 registryUrl 过来的,这个函数里的逻辑感觉需要在以后做一个简化
1141
- if (!registryUrl && blocklet.source !== BlockletSource.registry) {
1142
- throw new Error('Wrong upgrade source, empty registryUrl or not installed from blocklet registry');
1221
+ // TODO: 查看了下目前页面中的升级按钮,都是会传 storeUrl 过来的,这个函数里的逻辑感觉需要在以后做一个简化
1222
+ if (!storeUrl && blocklet.source !== BlockletSource.registry) {
1223
+ throw new Error('Wrong upgrade source, empty storeUrl or not installed from blocklet registry');
1143
1224
  }
1144
1225
 
1145
- const upgradeFromRegistry = registryUrl || blocklet.deployedFrom;
1226
+ const upgradeFromStore = storeUrl || blocklet.deployedFrom;
1146
1227
 
1147
1228
  let newVersionMeta = await StoreUtil.getBlockletMeta({
1148
1229
  did: blocklet.meta.bundleDid,
1149
- registryUrl: upgradeFromRegistry,
1230
+ storeUrl: upgradeFromStore,
1150
1231
  });
1151
1232
  newVersionMeta = ensureMeta(newVersionMeta, { name: blocklet.meta.name, did: blocklet.meta.did });
1152
1233
 
@@ -1197,7 +1278,7 @@ class BlockletManager extends BaseBlockletManager {
1197
1278
  return this._upgrade({
1198
1279
  meta: newVersionMeta,
1199
1280
  source: BlockletSource.registry,
1200
- deployedFrom: upgradeFromRegistry,
1281
+ deployedFrom: upgradeFromStore,
1201
1282
  context,
1202
1283
  sync,
1203
1284
  });
@@ -1358,7 +1439,7 @@ class BlockletManager extends BaseBlockletManager {
1358
1439
  logger.error('queue failed', { entity: 'blocklet', action, did, version, name, error: err });
1359
1440
  await this._rollback(action, did, oldBlocklet);
1360
1441
  this.emit(`blocklet.${action}.failed`, { did, version, err });
1361
- states.notification.create({
1442
+ this._createNotification(did, {
1362
1443
  title: `Blocklet ${capitalize(action)} Failed`,
1363
1444
  description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message || 'queue exception'}`,
1364
1445
  entityType: 'blocklet',
@@ -1382,7 +1463,7 @@ class BlockletManager extends BaseBlockletManager {
1382
1463
 
1383
1464
  const meta = getBlockletMeta(folder);
1384
1465
  if (meta.group !== 'static' && (!meta.scripts || !meta.scripts.dev)) {
1385
- throw new Error('Incorrect blocklet manifest: missing `scripts.dev` field');
1466
+ throw new Error('Incorrect blocklet.yml: missing `scripts.dev` field');
1386
1467
  }
1387
1468
 
1388
1469
  if (rootDid) {
@@ -1757,7 +1838,7 @@ class BlockletManager extends BaseBlockletManager {
1757
1838
  message: err.message,
1758
1839
  },
1759
1840
  });
1760
- states.notification.create({
1841
+ this._createNotification(did, {
1761
1842
  title: 'Blocklet Download Failed',
1762
1843
  description: `Blocklet ${name}@${version} download failed with error: ${err.message}`,
1763
1844
  entityType: 'blocklet',
@@ -1895,7 +1976,7 @@ class BlockletManager extends BaseBlockletManager {
1895
1976
  await this.deleteProcess({ did }, context);
1896
1977
  await states.blocklet.setBlockletStatus(did, BlockletStatus.error);
1897
1978
 
1898
- states.notification.create({
1979
+ this._createNotification(did, {
1899
1980
  title: 'Blocklet Start Failed',
1900
1981
  description: `Blocklet ${name} start failed: ${error.message}`,
1901
1982
  entityType: 'blocklet',
@@ -1975,16 +2056,19 @@ class BlockletManager extends BaseBlockletManager {
1975
2056
  * @memberof BlockletManager
1976
2057
  */
1977
2058
  async _installFromStore(params, context) {
1978
- const { did, registry, sync, delay, controller } = params;
2059
+ const { did, storeUrl, sync, delay, controller } = params;
1979
2060
 
1980
2061
  logger.debug('start install blocklet', { did });
1981
2062
  if (!isValidDid(did)) {
1982
2063
  throw new Error('Blocklet did is invalid');
1983
2064
  }
1984
2065
 
1985
- const registryUrl = registry || (await states.node.getBlockletRegistry());
1986
- const info = await StoreUtil.getRegistryMeta(registryUrl);
1987
- const meta = await StoreUtil.getBlockletMeta({ did, registryUrl });
2066
+ if (!storeUrl) {
2067
+ throw new Error('registry url should not be empty');
2068
+ }
2069
+
2070
+ const info = await StoreUtil.getRegistryMeta(storeUrl);
2071
+ const meta = await StoreUtil.getBlockletMeta({ did, storeUrl });
1988
2072
  if (!meta) {
1989
2073
  throw new Error('Can not install blocklet that not found in registry');
1990
2074
  }
@@ -2013,7 +2097,7 @@ class BlockletManager extends BaseBlockletManager {
2013
2097
  return this._install({
2014
2098
  meta: ensureMeta(meta, { did: blockletDid, name: blockletName }),
2015
2099
  source: BlockletSource.registry,
2016
- deployedFrom: info.cdnUrl || registryUrl,
2100
+ deployedFrom: info.cdnUrl || storeUrl,
2017
2101
  sync,
2018
2102
  delay,
2019
2103
  controller,
@@ -2056,10 +2140,10 @@ class BlockletManager extends BaseBlockletManager {
2056
2140
  if (inStore) {
2057
2141
  const exist = await states.blocklet.getBlocklet(blockletDid);
2058
2142
  if (exist) {
2059
- return this.upgrade({ did: blockletDid, registryUrl, sync, delay }, context);
2143
+ return this.upgrade({ did: blockletDid, storeUrl: registryUrl, sync, delay }, context);
2060
2144
  }
2061
2145
 
2062
- return this._installFromStore({ did: bundleDid, registry: registryUrl, controller, sync, delay }, context);
2146
+ return this._installFromStore({ did: bundleDid, storeUrl: registryUrl, controller, sync, delay }, context);
2063
2147
  }
2064
2148
 
2065
2149
  const meta = ensureMeta(bundleMeta, { name: blockletName, did: blockletDid });
@@ -2514,6 +2598,11 @@ class BlockletManager extends BaseBlockletManager {
2514
2598
  await refreshAccessibleExternalNodeIp(nodeInfo);
2515
2599
  },
2516
2600
  },
2601
+ {
2602
+ name: 'delete-expired-external-blocklet',
2603
+ time: '0 */30 * * * *', // 30min
2604
+ fn: () => this._deleteExpiredExternalBlocklet(),
2605
+ },
2517
2606
  ];
2518
2607
  }
2519
2608
 
@@ -2633,7 +2722,7 @@ class BlockletManager extends BaseBlockletManager {
2633
2722
  logger.error('failed to remove blocklet on install error', { did: meta.did, error: e });
2634
2723
  }
2635
2724
 
2636
- states.notification.create({
2725
+ this._createNotification(did, {
2637
2726
  title: 'Blocklet Install Failed',
2638
2727
  description: `Blocklet ${name}@${version} install failed with error: ${err.message || 'queue exception'}`,
2639
2728
  entityType: 'blocklet',
@@ -2646,7 +2735,7 @@ class BlockletManager extends BaseBlockletManager {
2646
2735
  return blocklet1;
2647
2736
  } catch (err) {
2648
2737
  logger.error('failed to install blocklet', { name, did, version, error: err });
2649
- states.notification.create({
2738
+ this._createNotification(did, {
2650
2739
  title: 'Blocklet Install Failed',
2651
2740
  description: `Blocklet ${name}@${version} install failed with error: ${err.message || 'queue exception'}`,
2652
2741
  entityType: 'blocklet',
@@ -2744,7 +2833,7 @@ class BlockletManager extends BaseBlockletManager {
2744
2833
  downgrade: BlockletEvents.downgradeFailed,
2745
2834
  };
2746
2835
  this.emit(eventNames[action], { blocklet: oldBlocklet, context });
2747
- states.notification.create({
2836
+ this._createNotification(did, {
2748
2837
  title: `Blocklet ${capitalize(action)} Failed`,
2749
2838
  description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message || 'queue exception'}`,
2750
2839
  entityType: 'blocklet',
@@ -2935,7 +3024,12 @@ class BlockletManager extends BaseBlockletManager {
2935
3024
  });
2936
3025
  }
2937
3026
 
2938
- 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, {
2939
3033
  title: 'Blocklet Installed',
2940
3034
  description: `Blocklet ${meta.name}@${meta.version} is installed successfully. (Source: ${
2941
3035
  deployedFrom || fromBlockletSource(source)
@@ -2958,7 +3052,7 @@ class BlockletManager extends BaseBlockletManager {
2958
3052
  message: err.message,
2959
3053
  },
2960
3054
  });
2961
- states.notification.create({
3055
+ this._createNotification(did, {
2962
3056
  title: 'Blocklet Install Failed',
2963
3057
  description: `Blocklet ${meta.name}@${meta.version} install failed with error: ${err.message}`,
2964
3058
  entityType: 'blocklet',
@@ -3053,7 +3147,7 @@ class BlockletManager extends BaseBlockletManager {
3053
3147
  downgrade: BlockletEvents.downgraded,
3054
3148
  };
3055
3149
  this.emit(eventNames[action], { blocklet, context });
3056
- states.notification.create({
3150
+ this._createNotification(did, {
3057
3151
  title: `Blocklet ${capitalize(action)} Success`,
3058
3152
  description: `Blocklet ${name}@${version} ${action} successfully. (Source: ${
3059
3153
  deployedFrom || fromBlockletSource(source)
@@ -3081,9 +3175,9 @@ class BlockletManager extends BaseBlockletManager {
3081
3175
  upgrade: BlockletEvents.upgradeFailed,
3082
3176
  downgrade: BlockletEvents.downgradeFailed,
3083
3177
  };
3084
- this.emit(eventNames[action], { blocklet: oldBlocklet, context });
3178
+ this.emit(eventNames[action], { blocklet: { ...oldBlocklet, error: { message: err.message } }, context });
3085
3179
 
3086
- states.notification.create({
3180
+ this._createNotification(did, {
3087
3181
  title: `Blocklet ${capitalize(action)} Failed`,
3088
3182
  description: `Blocklet ${name}@${version} ${action} failed with error: ${err.message}`,
3089
3183
  entityType: 'blocklet',
@@ -3490,11 +3584,11 @@ class BlockletManager extends BaseBlockletManager {
3490
3584
  }
3491
3585
 
3492
3586
  async _setConfigsFromMeta(did, childDid) {
3493
- const blocklet = await states.blocklet.getBlocklet(did);
3587
+ const blocklet = await getBlocklet({ states, dataDirs: this.dataDirs, did, validateEnv: false, ensureDirs: false });
3494
3588
 
3495
3589
  if (!childDid) {
3496
3590
  await forEachBlocklet(blocklet, async (b, { ancestors }) => {
3497
- const environments = get(b.meta, 'environments', []);
3591
+ const environments = [...get(b.meta, 'environments', []), ...getConfigFromPreferences(b)];
3498
3592
 
3499
3593
  // remove default if ancestors has a value
3500
3594
  ensureEnvDefault(environments, ancestors);
@@ -3507,7 +3601,7 @@ class BlockletManager extends BaseBlockletManager {
3507
3601
  await forEachBlocklet(child, async (b, { ancestors }) => {
3508
3602
  await states.blockletExtras.setConfigs(
3509
3603
  [blocklet.meta.did, ...ancestors.map((x) => x.meta.did), b.meta.did],
3510
- get(b.meta, 'environments', [])
3604
+ [...get(b.meta, 'environments', []), ...getConfigFromPreferences(child)]
3511
3605
  );
3512
3606
  });
3513
3607
  }
@@ -3556,13 +3650,13 @@ class BlockletManager extends BaseBlockletManager {
3556
3650
  }
3557
3651
 
3558
3652
  async _getLatestBlockletVersionFromStore({ blocklet, version }) {
3559
- const { deployedFrom: registryUrl } = blocklet;
3653
+ const { deployedFrom: storeUrl } = blocklet;
3560
3654
  const { did, bundleDid } = blocklet.meta;
3561
3655
 
3562
3656
  let versions = this.cachedBlockletVersions.get(did);
3563
3657
 
3564
3658
  if (!versions) {
3565
- const item = await StoreUtil.getBlockletMeta({ did: bundleDid, registryUrl });
3659
+ const item = await StoreUtil.getBlockletMeta({ did: bundleDid, storeUrl });
3566
3660
 
3567
3661
  if (!item) {
3568
3662
  return null;
@@ -3579,23 +3673,7 @@ class BlockletManager extends BaseBlockletManager {
3579
3673
  return null;
3580
3674
  }
3581
3675
 
3582
- // When new version found from the store where the blocklet was installed from, we should use that store first
3583
- if (blocklet.source === BlockletSource.registry && blocklet.deployedFrom) {
3584
- const latestFromSameRegistry = versions.find((x) => x.registryUrl === blocklet.deployedFrom);
3585
- if (latestFromSameRegistry) {
3586
- return latestFromSameRegistry;
3587
- }
3588
- }
3589
-
3590
- // Otherwise try upgrading from other store
3591
- let latestBlockletVersion = versions[0];
3592
- versions.forEach((item) => {
3593
- if (semver.lt(latestBlockletVersion.version, item.version)) {
3594
- latestBlockletVersion = item;
3595
- }
3596
- });
3597
-
3598
- return latestBlockletVersion;
3676
+ return versions[0];
3599
3677
  }
3600
3678
 
3601
3679
  async _getLatestBlockletVersionFromUrl({ blocklet, version }) {
@@ -3654,6 +3732,64 @@ class BlockletManager extends BaseBlockletManager {
3654
3732
 
3655
3733
  await forEachBlocklet(blocklet, postInstall, { parallel: true });
3656
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
+ }
3657
3793
  }
3658
3794
 
3659
3795
  module.exports = BlockletManager;