@abtnode/core 1.6.13 → 1.6.17

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.
@@ -6,9 +6,11 @@ const os = require('os');
6
6
  const tar = require('tar');
7
7
  const get = require('lodash/get');
8
8
  const intersection = require('lodash/intersection');
9
+ const intersectionBy = require('lodash/intersectionBy');
9
10
  const streamToPromise = require('stream-to-promise');
10
11
  const { Throttle } = require('stream-throttle');
11
12
  const ssri = require('ssri');
13
+ const diff = require('deep-diff');
12
14
 
13
15
  const { toHex } = require('@ocap/util');
14
16
  const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
@@ -17,7 +19,10 @@ const sleep = require('@abtnode/util/lib/sleep');
17
19
  const ensureEndpointHealthy = require('@abtnode/util/lib/ensure-endpoint-healthy');
18
20
  const CustomError = require('@abtnode/util/lib/custom-error');
19
21
  const getFolderSize = require('@abtnode/util/lib/get-folder-size');
22
+ const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
23
+ const hashFiles = require('@abtnode/util/lib/hash-files');
20
24
  const { BLOCKLET_MAX_MEM_LIMIT_IN_MB } = require('@abtnode/constant');
25
+
21
26
  const {
22
27
  BlockletStatus,
23
28
  BlockletSource,
@@ -29,6 +34,7 @@ const {
29
34
  BLOCKLET_DEFAULT_PORT_NAME,
30
35
  BLOCKLET_INTERFACE_TYPE_WEB,
31
36
  BLOCKLET_CONFIGURABLE_KEY,
37
+ BLOCKLET_DYNAMIC_PATH_PREFIX,
32
38
  fromBlockletStatus,
33
39
  } = require('@blocklet/meta/lib/constants');
34
40
  const verifyMultiSig = require('@blocklet/meta/lib/verify-multi-sig');
@@ -41,6 +47,7 @@ const { forEachBlocklet } = require('@blocklet/meta/lib/util');
41
47
  const { validate: validateEngine, get: getEngine } = require('../blocklet/manager/engine');
42
48
 
43
49
  const isRequirementsSatisfied = require('./requirement');
50
+ const { getDidDomainForBlocklet } = require('./get-domain-for-blocklet');
44
51
  const { getServices } = require('./service');
45
52
  const {
46
53
  isBeforeInstalled,
@@ -60,8 +67,6 @@ const getBlockletEngineNameByPlatform = (blockletMeta) => getBlockletEngine(bloc
60
67
 
61
68
  const noop = () => {};
62
69
 
63
- const asyncFs = fs.promises;
64
-
65
70
  const statusMap = {
66
71
  online: BlockletStatus.running,
67
72
  launching: BlockletStatus.starting,
@@ -74,17 +79,19 @@ const PRIVATE_NODE_ENVS = [
74
79
  // 'NEDB_MULTI_PORT', // FIXME: 排查 abtnode 对外提供的 SDK(比如 @abtnode/queue), SDK 中不要自动使用 NEDB_MULTI_PORT 环境变量
75
80
  'ABT_NODE_UPDATER_PORT',
76
81
  'ABT_NODE_SESSION_TTL',
77
- 'ABT_NODE_MODE',
78
82
  'ABT_NODE_ROUTER_PROVIDER',
79
83
  'ABT_NODE_DATA_DIR',
80
84
  'ABT_NODE_TOKEN_SECRET',
81
85
  'ABT_NODE_SK',
82
86
  'ABT_NODE_SESSION_SECRET',
83
- 'ABT_NODE_NAME',
84
- 'ABT_NODE_DESCRIPTION',
85
87
  'ABT_NODE_BASE_URL',
86
88
  'ABT_NODE_LOG_LEVEL',
87
89
  'ABT_NODE_LOG_DIR',
90
+ // in /core/cli/bin/blocklet.js
91
+ 'CLI_MODE',
92
+ 'ABT_NODE_HOME',
93
+ 'PM2_HOME',
94
+ 'ABT_NODE_CONFIG_FILE',
88
95
  ];
89
96
 
90
97
  /**
@@ -115,19 +122,9 @@ const getBlockletDirs = (blocklet, { rootBlocklet, dataDirs, ensure = false } =
115
122
  logsDir = path.join(dataDirs.logs, rootBlocklet.meta.name);
116
123
  }
117
124
 
118
- if (ensure) {
119
- try {
120
- fs.mkdirSync(dataDir, { recursive: true });
121
- fs.mkdirSync(logsDir, { recursive: true });
122
- fs.mkdirSync(cacheDir, { recursive: true });
123
- } catch (err) {
124
- logger.error('make blocklet dir failed', { error: err });
125
- }
126
- }
127
-
128
125
  // get app dirs
129
126
 
130
- const { version, main, group } = blocklet.meta;
127
+ const { main, group } = blocklet.meta;
131
128
 
132
129
  const startFromDevEntry =
133
130
  blocklet.mode === BLOCKLET_MODES.DEVELOPMENT && blocklet.meta.scripts && blocklet.meta.scripts.dev;
@@ -142,13 +139,24 @@ const getBlockletDirs = (blocklet, { rootBlocklet, dataDirs, ensure = false } =
142
139
  if (blocklet.source === BlockletSource.local) {
143
140
  appDir = blocklet.deployedFrom;
144
141
  } else {
145
- appDir = path.join(dataDirs.blocklets, name, version);
142
+ appDir = getBundleDir(dataDirs.blocklets, blocklet.meta);
146
143
  }
147
144
 
148
145
  if (!appDir) {
149
146
  throw new Error('Can not determine blocklet directory, maybe invalid deployment from local blocklets');
150
147
  }
151
148
 
149
+ if (ensure) {
150
+ try {
151
+ fs.mkdirSync(dataDir, { recursive: true });
152
+ fs.mkdirSync(logsDir, { recursive: true });
153
+ fs.mkdirSync(cacheDir, { recursive: true });
154
+ fs.mkdirSync(appDir, { recursive: true }); // prevent getDiskInfo failed from custom blocklet
155
+ } catch (err) {
156
+ logger.error('make blocklet dir failed', { error: err });
157
+ }
158
+ }
159
+
152
160
  mainDir = appDir;
153
161
 
154
162
  if (!startFromDevEntry && !isBeforeInstalled(rootBlocklet.status)) {
@@ -243,12 +251,16 @@ const getRootSystemEnvironments = (blocklet, nodeInfo) => {
243
251
  const appName = title || name || result.name;
244
252
  const appDescription = description || result.description;
245
253
 
254
+ // FIXME: we should use https here when possible, eg, when did-gateway is available
255
+ const appUrl = `http://${getDidDomainForBlocklet({ appId, didDomain: nodeInfo.didDomain })}`;
256
+
246
257
  return {
247
258
  BLOCKLET_DID: did,
248
259
  BLOCKLET_APP_SK: appSk,
249
260
  BLOCKLET_APP_ID: appId,
250
261
  BLOCKLET_APP_NAME: appName,
251
262
  BLOCKLET_APP_DESCRIPTION: appDescription,
263
+ BLOCKLET_APP_URL: appUrl,
252
264
  };
253
265
  };
254
266
 
@@ -428,11 +440,13 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
428
440
  const options = {
429
441
  namespace: 'blocklets',
430
442
  name: appId,
431
- max_memory_restart: `${maxMemoryRestart}M`,
443
+ cwd: appCwd,
432
444
  time: true,
433
445
  output: path.join(logsDir, 'output.log'),
434
446
  error: path.join(logsDir, 'error.log'),
435
- cwd: appCwd,
447
+ wait_ready: process.env.NODE_ENV !== 'test',
448
+ listen_timeout: 5000,
449
+ max_memory_restart: `${maxMemoryRestart}M`,
436
450
  max_restarts: b.mode === BLOCKLET_MODES.DEVELOPMENT ? 0 : 3,
437
451
  env: {
438
452
  ...env,
@@ -460,17 +474,17 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
460
474
  const engine = getEngine(blockletEngineInfo.interpreter);
461
475
  options.interpreter = engine.interpreter === 'node' ? '' : engine.interpreter;
462
476
  options.interpreterArgs = engine.args || '';
463
-
464
477
  options.script = blockletEngineInfo.script || appMain;
465
-
466
- logger.debug('start.blocklet.engine.info', { blockletEngineInfo });
467
- logger.debug('start.blocklet.max_memory_restart', { maxMemoryRestart });
468
478
  }
469
479
 
470
480
  await pm2.startAsync(options);
471
- logger.info('blocklet started', {
472
- appId,
473
- });
481
+
482
+ const status = await getProcessState(appId);
483
+ if (status === BlockletStatus.error) {
484
+ throw new Error(`${appId} is not running within 5 seconds`);
485
+ }
486
+
487
+ logger.info('blocklet started', { appId, status });
474
488
  },
475
489
  { parallel: true }
476
490
  );
@@ -533,6 +547,10 @@ const getBlockletStatusFromProcess = async (blocklet) => {
533
547
 
534
548
  const list = await Promise.all(tasks);
535
549
 
550
+ if (!list.length) {
551
+ return blocklet.status;
552
+ }
553
+
536
554
  return getRootBlockletStatus(list);
537
555
  };
538
556
 
@@ -611,26 +629,76 @@ const reloadProcess = (appId) =>
611
629
  });
612
630
  });
613
631
 
614
- const getChildrenMeta = async (meta) => {
615
- const children = [];
616
- if (meta.children && meta.children.length) {
617
- for (const child of meta.children) {
618
- const m = await getBlockletMetaFromUrl(child.resolved);
619
- if (m.name !== child.name) {
620
- logger.error('Resolved child blocklet name does not match in the configuration', {
621
- expected: child.name,
622
- resolved: m.name,
623
- });
624
- throw new Error(
625
- `Child blocklet name does not match in the configuration. expected: ${child.name}, resolved: ${m.name}`
626
- );
627
- }
628
- validateBlockletMeta(m, { ensureDist: true });
629
- children.push(m);
632
+ const findWebInterface = (blocklet) => {
633
+ const meta = blocklet.meta || blocklet || {};
634
+ const { interfaces = [] } = meta;
635
+
636
+ if (!Array.isArray(interfaces)) {
637
+ return null;
638
+ }
639
+
640
+ return interfaces.find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
641
+ };
642
+
643
+ /**
644
+ * this function has side effect on children
645
+ */
646
+ const parseChildren = async (src, { children, dynamic } = {}) => {
647
+ const configs = Array.isArray(src) ? src : src.children || [];
648
+
649
+ if (children) {
650
+ children.forEach((x) => {
651
+ x.dynamic = !!dynamic;
652
+ });
653
+ mergeMeta(configs, children);
654
+ return children;
655
+ }
656
+
657
+ if (!configs || !configs.length) {
658
+ return [];
659
+ }
660
+
661
+ const results = [];
662
+
663
+ for (const config of configs) {
664
+ const mountPoint = config.mountPoint || config.mountPoints[0].root.prefix;
665
+ if (!mountPoint) {
666
+ throw new Error(`MountPoint does not found in child ${config.name}`);
630
667
  }
668
+
669
+ const m = await getBlockletMetaFromUrl(config.resolved);
670
+ if (m.name !== config.name) {
671
+ logger.error('Resolved child blocklet name does not match in the configuration', {
672
+ expected: config.name,
673
+ resolved: m.name,
674
+ });
675
+ throw new Error(
676
+ `Child blocklet name does not match in the configuration. expected: ${config.name}, resolved: ${m.name}`
677
+ );
678
+ }
679
+ validateBlockletMeta(m, { ensureDist: true });
680
+
681
+ const webInterface = findWebInterface(m);
682
+ if (!webInterface) {
683
+ throw new Error(`Web interface does not found in child ${config.name}`);
684
+ }
685
+
686
+ const rule = webInterface.prefix;
687
+ if (rule !== BLOCKLET_DYNAMIC_PATH_PREFIX && normalizePathPrefix(rule) !== normalizePathPrefix(mountPoint)) {
688
+ throw new Error(`Prefix does not match in child ${config.name}. expected: ${rule}, resolved: ${mountPoint}`);
689
+ }
690
+
691
+ results.push({
692
+ mountPoint,
693
+ meta: m,
694
+ dynamic: !!dynamic,
695
+ sourceUrl: config.resolved,
696
+ });
631
697
  }
632
698
 
633
- return children;
699
+ mergeMeta(configs, results);
700
+
701
+ return results;
634
702
  };
635
703
 
636
704
  const validateBlocklet = (blocklet) =>
@@ -728,7 +796,7 @@ const verifyIntegrity = async ({ file, integrity: expected }) => {
728
796
  return true;
729
797
  };
730
798
 
731
- const pruneBlockletBundle = async (blocklets, installDir) => {
799
+ const pruneBlockletBundle = async ({ blocklets, installDir, blockletSettings }) => {
732
800
  for (const blocklet of blocklets) {
733
801
  if (
734
802
  [
@@ -746,53 +814,104 @@ const pruneBlockletBundle = async (blocklets, installDir) => {
746
814
  }
747
815
  }
748
816
 
749
- // blockletMap: { <name/version>: true }
750
- const blockletMap = blocklets.reduce((map, b) => {
751
- map[`${b.meta.name}/${b.meta.version}`] = true;
752
- for (const child of b.children || []) {
753
- map[`${child.meta.name}/${child.meta.version}`] = true;
817
+ // blockletMap: { <[scope/]name/version>: true }
818
+ const blockletMap = {};
819
+ for (const blocklet of blocklets) {
820
+ blockletMap[`${blocklet.meta.name}/${blocklet.meta.version}`] = true;
821
+ for (const child of blocklet.children || []) {
822
+ blockletMap[`${child.meta.name}/${child.meta.version}`] = true;
754
823
  }
755
- return map;
756
- }, {});
824
+ }
825
+ for (const setting of blockletSettings) {
826
+ for (const child of setting.children || []) {
827
+ blockletMap[`${child.meta.name}/${child.meta.version}`] = true;
828
+ }
829
+ }
757
830
 
758
- // appDirs: [{ key: <name/version>, dir: appDir }]
831
+ // appDirs: [{ key: <[scope/]name/version>, dir: appDir }]
759
832
  const appDirs = [];
760
833
 
761
834
  // fill appDirs
762
835
  try {
763
- const fillAppDirs = async (dir) => {
764
- if (fs.existsSync(path.join(dir, 'blocklet.yml'))) {
836
+ // @return root/scope/bundle/version
837
+ const getNextLevel = (level, name) => {
838
+ if (level === 'root') {
839
+ if (name.startsWith('@')) {
840
+ return 'scope';
841
+ }
842
+ return 'bundle';
843
+ }
844
+ if (level === 'scope') {
845
+ return 'bundle';
846
+ }
847
+ if (level === 'bundle') {
848
+ return 'version';
849
+ }
850
+ throw new Error(`Invalid level ${level}`);
851
+ };
852
+
853
+ const fillAppDirs = async (dir, level = 'root') => {
854
+ if (level === 'version') {
855
+ if (!fs.existsSync(path.join(dir, 'blocklet.yml'))) {
856
+ logger.error('blocklet.yml does not exist in blocklet bundle dir', { dir });
857
+ return;
858
+ }
859
+
765
860
  appDirs.push({
766
861
  key: path.relative(installDir, dir),
767
862
  dir,
768
863
  });
864
+
769
865
  return;
770
866
  }
867
+
771
868
  const nextDirs = [];
772
- for (const x of await asyncFs.readdir(dir)) {
773
- const nextDir = path.join(dir, x);
774
- // if blocklet.yml does not exist in dir but non-folder file exists in dir, stop finding
775
- if (!fs.lstatSync(nextDir).isDirectory()) {
776
- logger.error('blocklet.yml does not exist in blocklet bundle dir', { dir });
777
- return;
869
+ for (const x of await fs.promises.readdir(dir)) {
870
+ if (!fs.lstatSync(path.join(dir, x)).isDirectory()) {
871
+ logger.error('pruneBlockletBundle: invalid file in bundle storage', { dir, file: x });
872
+ // eslint-disable-next-line no-continue
873
+ continue;
778
874
  }
779
- nextDirs.push(nextDir);
875
+ nextDirs.push(x);
780
876
  }
781
877
 
782
878
  for (const x of nextDirs) {
783
- await fillAppDirs(x);
879
+ await fillAppDirs(path.join(dir, x), getNextLevel(level, x));
784
880
  }
785
881
  };
786
- await fillAppDirs(installDir);
882
+ await fillAppDirs(installDir, 'root');
787
883
  } catch (error) {
788
884
  logger.error('fill app dirs failed', { error });
789
885
  }
790
886
 
887
+ const ensureBundleDirRemoved = async (dir) => {
888
+ const relativeDir = path.relative(installDir, dir);
889
+ const arr = relativeDir.split('/').filter(Boolean);
890
+ const { length } = arr;
891
+ const bundleName = arr[length - 2];
892
+ const scopeName = length > 2 ? arr[length - 3] : '';
893
+ const bundleDir = path.join(installDir, scopeName, bundleName);
894
+ const isEmpty = (await fs.promises.readdir(bundleDir)).length === 0;
895
+ if (isEmpty) {
896
+ logger.info('Remove bundle folder', { bundleDir });
897
+ await fs.remove(bundleDir);
898
+ }
899
+ if (scopeName) {
900
+ const scopeDir = path.join(installDir, scopeName);
901
+ const isScopeEmpty = (await fs.promises.readdir(scopeDir)).length === 0;
902
+ if (isScopeEmpty) {
903
+ logger.info('Remove scope folder', { scopeDir });
904
+ await fs.remove(scopeDir);
905
+ }
906
+ }
907
+ };
908
+
791
909
  // remove trash
792
910
  for (const app of appDirs) {
793
911
  if (!blockletMap[app.key]) {
794
- logger.info('Remove blocklet bundle', { dir: app.dir });
912
+ logger.info('Remove app folder', { dir: app.dir });
795
913
  await fs.remove(app.dir);
914
+ await ensureBundleDirRemoved(app.dir);
796
915
  }
797
916
  }
798
917
 
@@ -830,15 +949,25 @@ const getRuntimeInfo = async (appId) => {
830
949
  };
831
950
  };
832
951
 
833
- const mergeMeta = (meta, childrenMeta = []) => {
952
+ /**
953
+ * merge services
954
+ * from meta.children[].mountPoints[].services
955
+ * to childrenMeta[].interfaces[].services
956
+ *
957
+ * @param {array<child>|object{children:array}} source e.g. [<config>] or { children: [<config>] }
958
+ * @param {array<meta|{meta}>} childrenMeta e.g. [<meta>] or [{ meta: <meta> }]
959
+ */
960
+
961
+ const mergeMeta = (source, childrenMeta = []) => {
834
962
  // configMap
835
963
  const configMap = {};
836
- (meta.children || []).forEach((x) => {
964
+ (Array.isArray(source) ? source : source.children || []).forEach((x) => {
837
965
  configMap[x.name] = x;
838
966
  });
839
967
 
840
968
  // merge service from config to child meta
841
- childrenMeta.forEach((childMeta) => {
969
+ childrenMeta.forEach((child) => {
970
+ const childMeta = child.meta || child;
842
971
  const config = configMap[childMeta.name];
843
972
  if (!config) {
844
973
  return;
@@ -898,10 +1027,84 @@ const getUpdateMetaList = (oldMetas = [], newMetas = []) => {
898
1027
  return newMetas.filter(({ version, did }) => did && version !== oldMap[did]);
899
1028
  };
900
1029
 
1030
+ const getSourceFromInstallParams = (params) => {
1031
+ if (params.url) {
1032
+ return BlockletSource.url;
1033
+ }
1034
+
1035
+ if (params.file) {
1036
+ return BlockletSource.upload;
1037
+ }
1038
+
1039
+ if (params.did) {
1040
+ return BlockletSource.registry;
1041
+ }
1042
+
1043
+ if (params.title && params.description) {
1044
+ return BlockletSource.custom;
1045
+ }
1046
+
1047
+ throw new Error('Can only install blocklet from store/url/upload/custom');
1048
+ };
1049
+
1050
+ const checkDuplicateComponents = (dynamicComponents, staticComponents) => {
1051
+ const duplicates = intersectionBy(dynamicComponents, staticComponents, 'meta.did');
1052
+ if (duplicates.length) {
1053
+ throw new Error(
1054
+ `Cannot add duplicate component${duplicates.length > 1 ? 's' : ''}: ${duplicates
1055
+ .map((x) => x.meta.title || x.meta.name)
1056
+ .join(', ')}`
1057
+ );
1058
+ }
1059
+ };
1060
+
1061
+ const getDiffFiles = async (inputFiles, sourceDir) => {
1062
+ if (!fs.existsSync(sourceDir)) {
1063
+ throw new Error(`${sourceDir} does not exist`);
1064
+ }
1065
+
1066
+ const files = inputFiles.reduce((obj, item) => {
1067
+ obj[item.file] = item.hash;
1068
+ return obj;
1069
+ }, {});
1070
+
1071
+ const { files: sourceFiles } = await hashFiles(sourceDir, {
1072
+ filter: (x) => x.indexOf('node_modules') === -1,
1073
+ concurrentHash: 1,
1074
+ });
1075
+
1076
+ const addSet = [];
1077
+ const changeSet = [];
1078
+ const deleteSet = [];
1079
+
1080
+ const diffFiles = diff(sourceFiles, files);
1081
+ if (diffFiles) {
1082
+ diffFiles.forEach((item) => {
1083
+ if (item.kind === 'D') {
1084
+ deleteSet.push(item.path[0]);
1085
+ }
1086
+ if (item.kind === 'E') {
1087
+ changeSet.push(item.path[0]);
1088
+ }
1089
+ if (item.kind === 'N') {
1090
+ addSet.push(item.path[0]);
1091
+ }
1092
+ });
1093
+ }
1094
+
1095
+ return {
1096
+ addSet,
1097
+ changeSet,
1098
+ deleteSet,
1099
+ };
1100
+ };
1101
+
1102
+ const getBundleDir = (installDir, bundle) => path.join(installDir, bundle.name, bundle.version);
1103
+
901
1104
  module.exports = {
902
1105
  forEachBlocklet,
903
1106
  getBlockletMetaFromUrl,
904
- getChildrenMeta,
1107
+ parseChildren,
905
1108
  getBlockletDirs,
906
1109
  getRootSystemEnvironments,
907
1110
  getSystemEnvironments,
@@ -927,4 +1130,9 @@ module.exports = {
927
1130
  mergeMeta,
928
1131
  fixAndVerifyBlockletMeta,
929
1132
  getUpdateMetaList,
1133
+ getSourceFromInstallParams,
1134
+ findWebInterface,
1135
+ checkDuplicateComponents,
1136
+ getDiffFiles,
1137
+ getBundleDir,
930
1138
  };
@@ -35,7 +35,7 @@ const defaultNodeConfigs = {
35
35
  },
36
36
  ],
37
37
  },
38
- registerUrl: { getDefaultValue: () => NODE_REGISTER_URL }, // removed in 1.5.1
38
+ registerUrl: { getDefaultValue: () => NODE_REGISTER_URL },
39
39
  webWalletUrl: { getDefaultValue: () => WEB_WALLET_URL },
40
40
  };
41
41
 
@@ -0,0 +1,13 @@
1
+ const { BLOCKLET_SITE_GROUP_SUFFIX } = require('@abtnode/constant');
2
+
3
+ const getBlockletDomainGroupName = (did) => `${did}${BLOCKLET_SITE_GROUP_SUFFIX}`;
4
+
5
+ const getDidFromDomainGroupName = (name) => {
6
+ const did = name.replace(BLOCKLET_SITE_GROUP_SUFFIX, '');
7
+ return did;
8
+ };
9
+
10
+ module.exports = {
11
+ getBlockletDomainGroupName,
12
+ getDidFromDomainGroupName,
13
+ };
@@ -9,14 +9,23 @@ const nodeInfoSchema = Joi.object({
9
9
  description: Joi.string()
10
10
  .required()
11
11
  .messages({ zh: { 'string.empty': '描述不能为空' }, en: { 'string.empty': 'Description cannot be empty' } }),
12
+ registerUrl: Joi.string()
13
+ .uri({ scheme: [/https?/] })
14
+ .label('register url')
15
+ .allow('')
16
+ .optional()
17
+ .messages({
18
+ zh: { 'string.uriCustomScheme': '应用启动器必须是合法的 URL' },
19
+ en: { 'string.uriCustomScheme': 'Blocklet Launcher must be a valid URL' },
20
+ }),
12
21
  webWalletUrl: Joi.string()
13
22
  .uri({ scheme: [/https?/] })
14
23
  .label('web wallet url')
15
24
  .allow('')
16
25
  .optional()
17
26
  .messages({
18
- zh: { 'string.uriCustomScheme': 'Web DID Wallet 必须是合法的 URL' },
19
- en: { 'string.uriCustomScheme': 'Web DID Wallet must be a valid URL' },
27
+ zh: { 'string.uriCustomScheme': 'Web Wallet 必须是合法的 URL' },
28
+ en: { 'string.uriCustomScheme': 'Web Wallet must be a valid URL' },
20
29
  }),
21
30
  autoUpgrade: Joi.boolean(),
22
31
  enableWelcomePage: Joi.boolean(),
@@ -31,16 +40,6 @@ const nodeInfoSchema = Joi.object({
31
40
  'number.max': 'Disk usage alert threshold cannot be higher than 99%',
32
41
  },
33
42
  }),
34
- // removed in 1.5.1
35
- registerUrl: Joi.string()
36
- .uri({ scheme: [/https?/] })
37
- .label('register url')
38
- .allow('')
39
- .optional()
40
- .messages({
41
- zh: { 'string.uriCustomScheme': '注册地址必须是合法的 URL' },
42
- en: { 'string.uriCustomScheme': 'Registry URL must be a valid URL' },
43
- }),
44
43
  }).options({ stripUnknown: true });
45
44
 
46
45
  module.exports = {
@@ -2,7 +2,22 @@
2
2
  const JOI = require('joi');
3
3
  const { getMultipleLangParams } = require('./util');
4
4
 
5
- const nameSchema = JOI.string().trim().max(64);
5
+ const nameSchema = JOI.string()
6
+ .trim()
7
+ .max(64)
8
+ .custom((name) => {
9
+ const arr = name.split('_');
10
+ const formatTip = 'The format of permission name should be "{action}_{resource}", e.g. query_data';
11
+ if (arr.length > 2) {
12
+ throw new Error(`Too much "_". ${formatTip}`);
13
+ }
14
+
15
+ if (arr.length < 2) {
16
+ throw new Error(formatTip);
17
+ }
18
+
19
+ return name;
20
+ });
6
21
  const descriptionSchema = JOI.string().trim().max(600);
7
22
 
8
23
  const createPermissionSchema = JOI.object({