@abtnode/core 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.
Files changed (119) hide show
  1. package/lib/api/node.js +67 -69
  2. package/lib/api/team.js +386 -55
  3. package/lib/blocklet/downloader/blocklet-downloader.js +226 -0
  4. package/lib/blocklet/downloader/bundle-downloader.js +272 -0
  5. package/lib/blocklet/downloader/constants.js +3 -0
  6. package/lib/blocklet/downloader/resolve-download.js +199 -0
  7. package/lib/blocklet/extras.js +83 -26
  8. package/lib/blocklet/hooks.js +18 -65
  9. package/lib/blocklet/manager/base.js +10 -16
  10. package/lib/blocklet/manager/disk.js +1680 -1566
  11. package/lib/blocklet/manager/helper/install-application-from-backup.js +177 -0
  12. package/lib/blocklet/manager/helper/install-application-from-dev.js +94 -0
  13. package/lib/blocklet/manager/helper/install-application-from-general.js +188 -0
  14. package/lib/blocklet/manager/helper/install-component-from-dev.js +84 -0
  15. package/lib/blocklet/manager/helper/install-component-from-upload.js +181 -0
  16. package/lib/blocklet/manager/helper/install-component-from-url.js +173 -0
  17. package/lib/blocklet/manager/helper/migrate-application-to-struct-v2.js +450 -0
  18. package/lib/blocklet/manager/helper/rollback-cache.js +41 -0
  19. package/lib/blocklet/manager/helper/upgrade-components.js +152 -0
  20. package/lib/blocklet/migration.js +30 -52
  21. package/lib/blocklet/storage/backup/audit-log.js +27 -0
  22. package/lib/blocklet/storage/backup/base.js +62 -0
  23. package/lib/blocklet/storage/backup/blocklet-extras.js +92 -0
  24. package/lib/blocklet/storage/backup/blocklet.js +70 -0
  25. package/lib/blocklet/storage/backup/blocklets.js +74 -0
  26. package/lib/blocklet/storage/backup/data.js +19 -0
  27. package/lib/blocklet/storage/backup/logs.js +24 -0
  28. package/lib/blocklet/storage/backup/routing-rule.js +19 -0
  29. package/lib/blocklet/storage/backup/spaces.js +240 -0
  30. package/lib/blocklet/storage/restore/base.js +67 -0
  31. package/lib/blocklet/storage/restore/blocklet-extras.js +86 -0
  32. package/lib/blocklet/storage/restore/blocklet.js +56 -0
  33. package/lib/blocklet/storage/restore/blocklets.js +43 -0
  34. package/lib/blocklet/storage/restore/logs.js +21 -0
  35. package/lib/blocklet/storage/restore/spaces.js +156 -0
  36. package/lib/blocklet/storage/utils/hash.js +51 -0
  37. package/lib/blocklet/storage/utils/zip.js +43 -0
  38. package/lib/cert.js +206 -0
  39. package/lib/event.js +237 -64
  40. package/lib/index.js +191 -83
  41. package/lib/migrations/1.0.21-update-config.js +1 -1
  42. package/lib/migrations/1.0.22-max-memory.js +1 -1
  43. package/lib/migrations/1.0.25.js +1 -1
  44. package/lib/migrations/1.0.32-update-config.js +1 -1
  45. package/lib/migrations/1.0.33-blocklets.js +1 -1
  46. package/lib/migrations/1.5.20-registry.js +15 -0
  47. package/lib/migrations/1.6.17-blocklet-children.js +48 -0
  48. package/lib/migrations/1.6.21-rename-ip-echo-domain.js +35 -0
  49. package/lib/migrations/1.6.4-security.js +59 -0
  50. package/lib/migrations/1.6.5-security.js +60 -0
  51. package/lib/migrations/1.6.9-update-node-info-and-certificate.js +38 -0
  52. package/lib/migrations/1.7.1-blocklet-setup.js +18 -0
  53. package/lib/migrations/1.7.12-blocklet-meta.js +51 -0
  54. package/lib/migrations/1.7.15-blocklet-bundle-source.js +42 -0
  55. package/lib/migrations/1.7.20-blocklet-component.js +41 -0
  56. package/lib/migrations/1.8.33-blocklet-mem-limit.js +20 -0
  57. package/lib/migrations/README.md +1 -1
  58. package/lib/migrations/index.js +6 -2
  59. package/lib/monitor/blocklet-runtime-monitor.js +200 -0
  60. package/lib/monitor/get-history-list.js +37 -0
  61. package/lib/monitor/node-runtime-monitor.js +228 -0
  62. package/lib/router/helper.js +576 -500
  63. package/lib/router/index.js +85 -21
  64. package/lib/router/manager.js +146 -187
  65. package/lib/states/README.md +36 -1
  66. package/lib/states/access-key.js +39 -17
  67. package/lib/states/audit-log.js +462 -0
  68. package/lib/states/base.js +4 -213
  69. package/lib/states/blocklet-extras.js +195 -138
  70. package/lib/states/blocklet.js +371 -110
  71. package/lib/states/cache.js +8 -6
  72. package/lib/states/challenge.js +5 -5
  73. package/lib/states/index.js +19 -36
  74. package/lib/states/migration.js +4 -4
  75. package/lib/states/node.js +135 -46
  76. package/lib/states/notification.js +22 -35
  77. package/lib/states/session.js +17 -9
  78. package/lib/states/site.js +50 -25
  79. package/lib/states/user.js +74 -20
  80. package/lib/states/webhook.js +10 -6
  81. package/lib/team/manager.js +124 -7
  82. package/lib/util/blocklet.js +1223 -246
  83. package/lib/util/chain.js +1 -1
  84. package/lib/util/default-node-config.js +5 -23
  85. package/lib/util/disk-monitor.js +13 -10
  86. package/lib/util/domain-status.js +84 -15
  87. package/lib/util/get-accessible-external-node-ip.js +2 -2
  88. package/lib/util/get-domain-for-blocklet.js +13 -0
  89. package/lib/util/get-meta-from-url.js +33 -0
  90. package/lib/util/index.js +207 -272
  91. package/lib/util/ip.js +6 -0
  92. package/lib/util/maintain.js +233 -0
  93. package/lib/util/public-to-store.js +85 -0
  94. package/lib/util/ready.js +1 -1
  95. package/lib/util/requirement.js +28 -9
  96. package/lib/util/reset-node.js +22 -7
  97. package/lib/util/router.js +13 -0
  98. package/lib/util/rpc.js +16 -0
  99. package/lib/util/store.js +179 -0
  100. package/lib/util/sysinfo.js +44 -0
  101. package/lib/util/ua.js +54 -0
  102. package/lib/validators/blocklet-extra.js +24 -0
  103. package/lib/validators/node.js +25 -12
  104. package/lib/validators/permission.js +16 -1
  105. package/lib/validators/role.js +17 -3
  106. package/lib/validators/router.js +40 -20
  107. package/lib/validators/trusted-passport.js +1 -0
  108. package/lib/validators/util.js +22 -5
  109. package/lib/webhook/index.js +45 -35
  110. package/lib/webhook/sender/index.js +5 -0
  111. package/lib/webhook/sender/slack/index.js +1 -1
  112. package/lib/webhook/sender/wallet/index.js +48 -0
  113. package/package.json +54 -36
  114. package/lib/blocklet/registry.js +0 -205
  115. package/lib/states/https-cert.js +0 -67
  116. package/lib/util/get-ip-dns-domain-for-blocklet.js +0 -19
  117. package/lib/util/service.js +0 -66
  118. package/lib/util/upgrade.js +0 -178
  119. /package/lib/{queue.js → util/queue.js} +0 -0
@@ -2,21 +2,46 @@
2
2
 
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
+ const joinURL = require('url-join');
6
+ const shelljs = require('shelljs');
5
7
  const os = require('os');
6
8
  const tar = require('tar');
7
9
  const get = require('lodash/get');
10
+ const isEmpty = require('lodash/isEmpty');
8
11
  const streamToPromise = require('stream-to-promise');
9
12
  const { Throttle } = require('stream-throttle');
10
13
  const ssri = require('ssri');
11
-
12
- const { toHex } = require('@ocap/util');
14
+ const diff = require('deep-diff');
15
+ const createArchive = require('archiver');
16
+ const isUrl = require('is-url');
17
+ const semver = require('semver');
18
+ const axios = require('@abtnode/util/lib/axios');
19
+ const { stableStringify } = require('@arcblock/vc');
20
+
21
+ const { fromSecretKey, WalletType } = require('@ocap/wallet');
22
+ const { toHex, toBase58, isHex, toDid } = require('@ocap/util');
23
+ const { types } = require('@ocap/mcrypto');
24
+ const { isValid: isValidDid } = require('@arcblock/did');
13
25
  const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
14
26
  const pm2 = require('@abtnode/util/lib/async-pm2');
15
27
  const sleep = require('@abtnode/util/lib/sleep');
28
+ const { formatEnv } = require('@abtnode/util/lib/security');
16
29
  const ensureEndpointHealthy = require('@abtnode/util/lib/ensure-endpoint-healthy');
17
30
  const CustomError = require('@abtnode/util/lib/custom-error');
18
31
  const getFolderSize = require('@abtnode/util/lib/get-folder-size');
19
- const { BLOCKLET_MAX_MEM_LIMIT_IN_MB } = require('@abtnode/constant');
32
+ const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
33
+ const hashFiles = require('@abtnode/util/lib/hash-files');
34
+ const {
35
+ BLOCKLET_MAX_MEM_LIMIT_IN_MB,
36
+ BLOCKLET_STORE,
37
+ BLOCKLET_INSTALL_TYPE,
38
+ BLOCKLET_STORE_DEV,
39
+ APP_STRUCT_VERSION,
40
+ } = require('@abtnode/constant');
41
+ const formatBackSlash = require('@abtnode/util/lib/format-back-slash');
42
+
43
+ const SCRIPT_ENGINES_WHITE_LIST = ['npm', 'npx', 'pnpm', 'yarn'];
44
+
20
45
  const {
21
46
  BlockletStatus,
22
47
  BlockletSource,
@@ -29,27 +54,53 @@ const {
29
54
  BLOCKLET_INTERFACE_TYPE_WEB,
30
55
  BLOCKLET_CONFIGURABLE_KEY,
31
56
  fromBlockletStatus,
32
- } = require('@blocklet/meta/lib/constants');
33
- const verifyMultiSig = require('@blocklet/meta/lib/verify-multi-sig');
57
+ BLOCKLET_PREFERENCE_FILE,
58
+ BLOCKLET_PREFERENCE_PREFIX,
59
+ } = require('@blocklet/constant');
34
60
  const validateBlockletEntry = require('@blocklet/meta/lib/entry');
35
61
  const getBlockletEngine = require('@blocklet/meta/lib/engine');
36
62
  const getBlockletInfo = require('@blocklet/meta/lib/info');
63
+ const getBlockletWallet = require('@blocklet/meta/lib/wallet');
37
64
  const { validateMeta, fixAndValidateService } = require('@blocklet/meta/lib/validate');
38
- const { forEachBlocklet } = require('@blocklet/meta/lib/util');
65
+ const {
66
+ forEachBlocklet,
67
+ getDisplayName,
68
+ findWebInterface,
69
+ forEachBlockletSync,
70
+ forEachChildSync,
71
+ isComponentBlocklet,
72
+ getSharedConfigObj,
73
+ getComponentName,
74
+ isEnvShareable,
75
+ } = require('@blocklet/meta/lib/util');
76
+ const toBlockletDid = require('@blocklet/meta/lib/did');
77
+ const { titleSchema, descriptionSchema, logoSchema } = require('@blocklet/meta/lib/schema');
78
+ const {
79
+ getSourceUrlsFromConfig,
80
+ getBlockletMetaFromUrls,
81
+ getBlockletMetaFromUrl,
82
+ } = require('@blocklet/meta/lib/util-meta');
83
+ const getComponentProcessId = require('@blocklet/meta/lib/get-component-process-id');
39
84
 
40
85
  const { validate: validateEngine, get: getEngine } = require('../blocklet/manager/engine');
41
86
 
42
87
  const isRequirementsSatisfied = require('./requirement');
43
- const { getServices } = require('./service');
88
+ const { getDidDomainForBlocklet } = require('./get-domain-for-blocklet');
44
89
  const {
45
90
  isBeforeInstalled,
46
91
  expandBundle,
47
- getBlockletMetaByUrl,
48
- validateUrl,
49
92
  findInterfacePortByName,
50
- validateBlockletMeta,
93
+ prettyURL,
94
+ getNFTState,
95
+ templateReplace,
51
96
  } = require('./index');
52
97
 
98
+ const getComponentConfig = (meta) => {
99
+ const components = meta.components || meta.children || [];
100
+ const staticComponents = (meta.staticComponents || []).map((x) => ({ ...x, static: true }));
101
+ return [...components, ...staticComponents];
102
+ };
103
+
53
104
  /**
54
105
  * get blocklet engine info, default is node
55
106
  * @param {object} blockletMeta blocklet meta
@@ -58,8 +109,7 @@ const {
58
109
  const getBlockletEngineNameByPlatform = (blockletMeta) => getBlockletEngine(blockletMeta).interpreter;
59
110
 
60
111
  const noop = () => {};
61
-
62
- const asyncFs = fs.promises;
112
+ const noopAsync = async () => {};
63
113
 
64
114
  const statusMap = {
65
115
  online: BlockletStatus.running,
@@ -69,106 +119,110 @@ const statusMap = {
69
119
  stopped: BlockletStatus.stopped,
70
120
  };
71
121
 
72
- const nodePrivateEnvs = [
73
- // 'NEDB_MULTI_PORT', // FIXME: 排查 abtnode 对外提供的 SDK(比如 @abtnode/queue), SDK 中不要自动使用 NEDB_MULTI_PORT 环境变量
122
+ const PRIVATE_NODE_ENVS = [
123
+ 'NEDB_MULTI_PORT',
74
124
  'ABT_NODE_UPDATER_PORT',
75
125
  'ABT_NODE_SESSION_TTL',
76
- 'ABT_NODE_MODE',
77
126
  'ABT_NODE_ROUTER_PROVIDER',
78
127
  'ABT_NODE_DATA_DIR',
79
128
  'ABT_NODE_TOKEN_SECRET',
80
129
  'ABT_NODE_SK',
81
130
  'ABT_NODE_SESSION_SECRET',
82
- 'ABT_NODE_NAME',
83
- 'ABT_NODE_DESCRIPTION',
84
131
  'ABT_NODE_BASE_URL',
85
132
  'ABT_NODE_LOG_LEVEL',
86
133
  'ABT_NODE_LOG_DIR',
134
+ // in /core/cli/bin/blocklet.js
135
+ 'CLI_MODE',
136
+ 'ABT_NODE_HOME',
137
+ 'PM2_HOME',
138
+ 'ABT_NODE_CONFIG_FILE',
87
139
  ];
88
140
 
89
141
  /**
90
142
  * @returns { dataDir, logsDir, cacheDir, appMain, appDir, mainDir, appCwd }
91
- * dataDir: dataDirs.data/name (root blocklet) or dataDirs.data/name/childName (child blocklet)
143
+ * dataDir: dataDirs.data/name (root component) or dataDirs.data/name/childName (child component)
92
144
  * logsDir: dataDirs.log/name
93
- * cacheDir: dataDirs.cache/name (root blocklet) or dataDirs.cache/name/childName (child blocklet)
94
- * appDir: blocklet bundle dir
95
- * mainDir: appDir (dapp) or appDir/main (static). Used for for static blocklet
96
- * appMain: app entry file or script (run appMain to start blocklet process)
145
+ * cacheDir: dataDirs.cache/name (root component) or dataDirs.cache/name/childName (child component)
146
+ * appDir: component bundle dir
147
+ * mainDir: appDir (dapp) or appDir/main (static). Used for for static component
148
+ * appMain: app entry file or script (run appMain to start component process)
97
149
  * appCwd: cwd of appMain
98
150
  */
99
- const getBlockletDirs = (blocklet, { rootBlocklet, dataDirs, ensure = false } = {}) => {
100
- if (!rootBlocklet) {
101
- // eslint-disable-next-line no-param-reassign
102
- rootBlocklet = blocklet;
103
- }
151
+ const getComponentDirs = (
152
+ component,
153
+ { dataDirs, ensure = false, e2eMode = false, validate = false, ancestors = [] } = {}
154
+ ) => {
155
+ // FIXME 这个函数做了太多的事
104
156
  // get data dirs
105
157
 
106
- const { name } = blocklet.meta;
107
-
108
- let logsDir = path.join(dataDirs.logs, name);
109
- let dataDir = path.join(dataDirs.data, name);
110
- let cacheDir = path.join(dataDirs.cache, name);
111
- if (rootBlocklet !== blocklet) {
112
- dataDir = path.join(dataDirs.data, rootBlocklet.meta.name, name);
113
- cacheDir = path.join(dataDirs.cache, rootBlocklet.meta.name, name);
114
- logsDir = path.join(dataDirs.logs, rootBlocklet.meta.name);
115
- }
158
+ const { name: appName } = ancestors.concat(component)[0].meta;
159
+ const componentName = getComponentName(component, ancestors);
116
160
 
117
- if (ensure) {
118
- try {
119
- fs.mkdirSync(dataDir, { recursive: true });
120
- fs.mkdirSync(logsDir, { recursive: true });
121
- fs.mkdirSync(cacheDir, { recursive: true });
122
- } catch (err) {
123
- logger.error('make blocklet dir failed', { error: err });
124
- }
125
- }
161
+ const logsDir = path.join(dataDirs.logs, appName);
162
+ const dataDir = path.join(dataDirs.data, componentName);
163
+ const cacheDir = path.join(dataDirs.cache, componentName);
126
164
 
127
165
  // get app dirs
128
166
 
129
- const { version, main, group } = blocklet.meta;
167
+ const { main, group } = component.meta;
130
168
 
131
- const startFromDevEntry =
132
- blocklet.mode === BLOCKLET_MODES.DEVELOPMENT && blocklet.meta.scripts && blocklet.meta.scripts.dev;
169
+ let startFromDevEntry = '';
170
+ if (component.mode === BLOCKLET_MODES.DEVELOPMENT && component.meta.scripts) {
171
+ startFromDevEntry = component.meta.scripts.dev;
172
+ if (e2eMode && component.meta.scripts.e2eDev) {
173
+ startFromDevEntry = component.meta.scripts.e2eDev;
174
+ }
175
+ }
133
176
 
134
- if (!main && !startFromDevEntry && group !== BlockletGroup.gateway) {
135
- throw new Error('Incorrect blocklet manifest: missing `main` field');
177
+ if (validate && !main && !startFromDevEntry && group !== BlockletGroup.gateway) {
178
+ throw new Error('Incorrect blocklet.yml: missing `main` field');
136
179
  }
137
180
 
138
181
  let appDir = null;
139
182
  let mainDir = null;
140
183
 
141
- if (blocklet.source === BlockletSource.local) {
142
- appDir = blocklet.deployedFrom;
184
+ if (component.source === BlockletSource.local) {
185
+ appDir = component.deployedFrom;
143
186
  } else {
144
- appDir = path.join(dataDirs.blocklets, name, version);
187
+ appDir = getBundleDir(dataDirs.blocklets, component.meta);
145
188
  }
146
189
 
147
190
  if (!appDir) {
148
191
  throw new Error('Can not determine blocklet directory, maybe invalid deployment from local blocklets');
149
192
  }
150
193
 
194
+ if (ensure) {
195
+ try {
196
+ fs.mkdirSync(dataDir, { recursive: true });
197
+ fs.mkdirSync(logsDir, { recursive: true });
198
+ fs.mkdirSync(cacheDir, { recursive: true });
199
+ fs.mkdirSync(appDir, { recursive: true }); // prevent getDiskInfo failed from custom blocklet
200
+ } catch (err) {
201
+ logger.error('make blocklet dir failed', { error: err });
202
+ }
203
+ }
204
+
151
205
  mainDir = appDir;
152
206
 
153
- if (!startFromDevEntry && !isBeforeInstalled(rootBlocklet.status)) {
207
+ if (validate && !startFromDevEntry && !isBeforeInstalled(component.status)) {
154
208
  try {
155
- validateBlockletEntry(appDir, blocklet.meta);
209
+ validateBlockletEntry(appDir, component.meta);
156
210
  } catch (err) {
157
211
  throw new CustomError('BLOCKLET_CORRUPTED', err.message);
158
212
  }
159
213
  }
160
214
 
161
- if (!BLOCKLET_GROUPS.includes(group)) {
215
+ if (validate && !BLOCKLET_GROUPS.includes(group)) {
162
216
  throw new CustomError('BLOCKLET_CORRUPTED', `Unsupported blocklet type ${group}`);
163
217
  }
164
218
 
165
219
  let appMain = null;
166
220
  let appCwd = null;
167
221
  if (startFromDevEntry) {
168
- appMain = blocklet.meta.scripts.dev;
222
+ appMain = startFromDevEntry;
169
223
  appCwd = appDir;
170
224
  } else if (group === 'dapp') {
171
- appMain = getBlockletEngine(blocklet.meta).script || BLOCKLET_ENTRY_FILE;
225
+ appMain = getBlockletEngine(component.meta).script || BLOCKLET_ENTRY_FILE;
172
226
  appCwd = appDir;
173
227
  } else if (group === 'static') {
174
228
  mainDir = path.join(appDir, main);
@@ -182,44 +236,40 @@ const getBlockletDirs = (blocklet, { rootBlocklet, dataDirs, ensure = false } =
182
236
  };
183
237
 
184
238
  /**
185
- * set 'configs', configObj', 'environmentObj' to blocklet
239
+ * set 'configs', configObj', 'environmentObj' to blocklet TODO
186
240
  * @param {*} blocklet
187
241
  * @param {*} configs
188
242
  */
189
243
  const fillBlockletConfigs = (blocklet, configs) => {
190
244
  blocklet.configs = configs || [];
191
245
  blocklet.configObj = blocklet.configs.reduce((acc, x) => {
192
- acc[x.key] = x.value;
246
+ acc[x.key] = templateReplace(x.value, blocklet);
193
247
  return acc;
194
248
  }, {});
195
- blocklet.userEnvironments = blocklet.configObj; // deprecated
196
- blocklet.environmentObj = (blocklet.environments || []).reduce((acc, x) => {
197
- acc[x.key] = x.value;
249
+ blocklet.environments = blocklet.environments || [];
250
+ blocklet.environmentObj = blocklet.environments.reduce((acc, x) => {
251
+ acc[x.key] = templateReplace(x.value, blocklet);
198
252
  return acc;
199
253
  }, {});
200
254
  };
201
255
 
202
256
  const ensureBlockletExpanded = async (meta, appDir) => {
203
- const { main } = meta;
204
-
205
- if (main === BLOCKLET_BUNDLE_FILE) {
206
- const bundlePath = path.join(appDir, BLOCKLET_BUNDLE_FILE);
207
- if (fs.existsSync(bundlePath)) {
208
- try {
209
- const nodeModulesPath = path.join(appDir, 'node_modules');
210
- if (fs.existsSync(nodeModulesPath)) {
211
- fs.removeSync(nodeModulesPath);
212
- }
213
- await expandBundle(bundlePath, appDir);
214
- fs.removeSync(bundlePath);
215
- } catch (err) {
216
- throw new Error(`Failed to expand blocklet bundle: ${err.message}`);
257
+ const bundlePath = path.join(appDir, BLOCKLET_BUNDLE_FILE);
258
+ if (fs.existsSync(bundlePath)) {
259
+ try {
260
+ const nodeModulesPath = path.join(appDir, 'node_modules');
261
+ if (fs.existsSync(nodeModulesPath)) {
262
+ fs.removeSync(nodeModulesPath);
217
263
  }
264
+ await expandBundle(bundlePath, appDir);
265
+ fs.removeSync(bundlePath);
266
+ } catch (err) {
267
+ throw new Error(`Failed to expand blocklet bundle: ${err.message}`);
218
268
  }
219
269
  }
220
270
  };
221
271
 
222
- const getRootSystemEnvironments = (blocklet, nodeInfo) => {
272
+ const getAppSystemEnvironments = (blocklet, nodeInfo) => {
223
273
  const { did, name, title, description } = blocklet.meta;
224
274
  const keys = Object.keys(BLOCKLET_CONFIGURABLE_KEY);
225
275
  const result = getBlockletInfo(
@@ -236,16 +286,41 @@ const getRootSystemEnvironments = (blocklet, nodeInfo) => {
236
286
  const appName = title || name || result.name;
237
287
  const appDescription = description || result.description;
238
288
 
289
+ const isMigrated = Array.isArray(blocklet.migratedFrom) && blocklet.migratedFrom.length > 0;
290
+ const appPid = blocklet.appPid || appId;
291
+ const appPsk = isMigrated ? blocklet.migratedFrom[0].appSk : appSk;
292
+
293
+ /* 获取 did domain 方式:
294
+ * 1. 先从 site 里读
295
+ * 2. 如果没有,再拼接
296
+ */
297
+
298
+ let appUrl = '';
299
+
300
+ const domainAliases = get(blocklet, 'site.domainAliases') || [];
301
+ const didDomainAlias = domainAliases.find(
302
+ (item) => item.value.endsWith(nodeInfo.didDomain) || item.value.endsWith('did.staging.arcblock.io') // did.staging.arcblock.io 是旧 did domain, 但主要存在于比较旧的节点中, 需要做兼容
303
+ );
304
+
305
+ if (didDomainAlias) {
306
+ appUrl = prettyURL(didDomainAlias.value, true);
307
+ } else {
308
+ appUrl = `https://${getDidDomainForBlocklet({ appPid, didDomain: nodeInfo.didDomain })}`;
309
+ }
310
+
239
311
  return {
240
- BLOCKLET_DID: did,
312
+ BLOCKLET_DID: did, // BLOCKLET_DID is always same as BLOCKLET_APP_PID in structV2 application
241
313
  BLOCKLET_APP_SK: appSk,
242
314
  BLOCKLET_APP_ID: appId,
315
+ BLOCKLET_APP_PSK: appPsk, // permanent sk even the blocklet has been migrated
316
+ BLOCKLET_APP_PID: appPid, // permanent did even the blocklet has been migrated
243
317
  BLOCKLET_APP_NAME: appName,
244
318
  BLOCKLET_APP_DESCRIPTION: appDescription,
319
+ BLOCKLET_APP_URL: appUrl,
245
320
  };
246
321
  };
247
322
 
248
- const getOverwrittenEnvironments = (blocklet, nodeInfo) => {
323
+ const getAppOverwrittenEnvironments = (blocklet, nodeInfo) => {
249
324
  const result = {};
250
325
  if (!blocklet || !blocklet.configObj) {
251
326
  return result;
@@ -278,7 +353,7 @@ const getOverwrittenEnvironments = (blocklet, nodeInfo) => {
278
353
  return result;
279
354
  };
280
355
 
281
- const getSystemEnvironments = (blocklet) => {
356
+ const getComponentSystemEnvironments = (blocklet) => {
282
357
  const { port, ports } = blocklet;
283
358
  const portEnvironments = {};
284
359
  if (port) {
@@ -287,33 +362,97 @@ const getSystemEnvironments = (blocklet) => {
287
362
  if (ports) {
288
363
  Object.assign(portEnvironments, ports);
289
364
  }
365
+
290
366
  return {
367
+ BLOCKLET_REAL_DID: blocklet.env.id,
368
+ BLOCKLET_REAL_NAME: blocklet.env.name,
291
369
  BLOCKLET_DATA_DIR: blocklet.env.dataDir,
292
370
  BLOCKLET_LOG_DIR: blocklet.env.logsDir,
293
371
  BLOCKLET_CACHE_DIR: blocklet.env.cacheDir,
294
- BLOCKLET_REAL_DID: blocklet.meta.did,
295
372
  BLOCKLET_APP_DIR: blocklet.env.appDir,
296
373
  BLOCKLET_MAIN_DIR: blocklet.env.mainDir,
297
374
  ...portEnvironments,
298
375
  };
299
376
  };
300
377
 
301
- const getRuntimeEnvironments = (blocklet, nodeEnvironments) => {
378
+ const getRuntimeEnvironments = (blocklet, nodeEnvironments, ancestors) => {
379
+ const root = (ancestors || [])[0] || blocklet;
380
+
302
381
  // pm2 will force inject env variables of daemon process to blocklet process
303
382
  // we can only rewrite these private env variables to empty
304
- const safeNodeEnvironments = nodePrivateEnvs.reduce((o, x) => {
383
+ const safeNodeEnvironments = PRIVATE_NODE_ENVS.reduce((o, x) => {
305
384
  o[x] = '';
306
385
  return o;
307
386
  }, {});
308
387
 
309
- return {
388
+ // get devEnvironments, when blocklet is in dev mode
389
+ const devEnvironments =
390
+ blocklet.mode === BLOCKLET_MODES.DEVELOPMENT
391
+ ? {
392
+ BLOCKLET_DEV_MOUNT_POINT: blocklet?.mountPoint || '',
393
+ }
394
+ : {};
395
+
396
+ // BLOCKLET_DEV_PORT should NOT in components of production mode
397
+ if (process.env.BLOCKLET_DEV_PORT) {
398
+ devEnvironments.BLOCKLET_DEV_PORT =
399
+ blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? process.env.BLOCKLET_DEV_PORT : '';
400
+ }
401
+
402
+ const ports = {};
403
+ forEachBlockletSync(root, (x) => {
404
+ const webInterface = findWebInterface(x);
405
+ if (webInterface && x.environmentObj[webInterface.port]) {
406
+ ports[x.environmentObj.BLOCKLET_REAL_NAME] = x.environmentObj[webInterface.port];
407
+ }
408
+ });
409
+
410
+ const mountPoints = [];
411
+ for (const x of root.children || []) {
412
+ const mountPoint = {
413
+ title: x.meta.title,
414
+ did: x.meta.bundleDid,
415
+ name: x.meta.bundleName,
416
+ mountPoint: x.mountPoint || '',
417
+ };
418
+
419
+ const webInterface = findWebInterface(x);
420
+ if (webInterface && x.environmentObj[webInterface.port]) {
421
+ mountPoint.port = x.environmentObj[webInterface.port];
422
+ }
423
+
424
+ mountPoints.push(mountPoint);
425
+ }
426
+
427
+ // use index 1 as the path to derive deterministic encryption key for blocklet
428
+ const tmp = get(nodeEnvironments, 'ABT_NODE_SK')
429
+ ? getBlockletWallet(blocklet.meta.did, nodeEnvironments.ABT_NODE_SK, undefined, 1)
430
+ : null;
431
+
432
+ const env = {
433
+ ...blocklet.configObj,
434
+ ...getSharedConfigObj(blocklet, ancestors),
310
435
  ...blocklet.environmentObj,
436
+ ...devEnvironments,
437
+ BLOCKLET_WEB_PORTS: JSON.stringify(ports),
438
+ BLOCKLET_MOUNT_POINTS: JSON.stringify(mountPoints),
439
+ BLOCKLET_MODE: blocklet.mode || BLOCKLET_MODES.PRODUCTION,
440
+ BLOCKLET_APP_EK: tmp?.secretKey,
441
+ BLOCKLET_APP_VERSION: root.meta.version,
311
442
  ...nodeEnvironments,
312
443
  ...safeNodeEnvironments,
313
444
  };
445
+
446
+ // ensure all envs are literals and do not contain line breaks
447
+ Object.keys(env).forEach((key) => {
448
+ env[key] = formatEnv(env[key]);
449
+ });
450
+
451
+ return env;
314
452
  };
315
453
 
316
- const isUsefulError = (err) => err && err.message !== 'process or namespace not found';
454
+ const isUsefulError = (err) =>
455
+ err && err.message !== 'process or namespace not found' && !/^Process \d+ not found$/.test(err.message);
317
456
 
318
457
  const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
319
458
  let minConsecutiveTime = 5000;
@@ -346,68 +485,51 @@ const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
346
485
  };
347
486
  };
348
487
 
349
- const getBlockletMetaFromUrl = async (url) => {
350
- const meta = await getBlockletMetaByUrl(url);
351
- delete meta.htmlAst;
352
-
353
- validateBlockletMeta(meta, { ensureDist: true });
354
-
355
- try {
356
- const { href } = new URL(meta.dist.tarball, url);
357
- const tarball = decodeURIComponent(href);
358
-
359
- try {
360
- await validateUrl(tarball, ['application/octet-stream', 'application/x-gzip']);
361
- } catch (error) {
362
- if (!error.message.startsWith('Cannot get content-type')) {
363
- throw error;
364
- }
365
- }
366
- logger.info('resolve tarball url base on meta url', { meta: url, tarball });
367
-
368
- meta.dist.tarball = tarball;
369
- } catch (err) {
370
- throw new Error(`Invalid blocklet meta: dist.tarball is not a valid url ${err.message}`);
371
- }
372
-
373
- return meta;
374
- };
375
-
376
488
  /**
377
489
  * Start all precesses of a blocklet
378
490
  * @param {*} blocklet should contain env props
379
491
  */
380
- const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironments, nodeInfo } = {}) => {
492
+ const startBlockletProcess = async (
493
+ blocklet,
494
+ { preStart = noop, postStart = noopAsync, nodeEnvironments, nodeInfo, e2eMode, skippedProcessIds = [] } = {}
495
+ ) => {
381
496
  if (!blocklet) {
382
497
  throw new Error('blocklet should not be empty');
383
498
  }
384
499
 
385
500
  await forEachBlocklet(
386
501
  blocklet,
387
- async (b) => {
502
+ async (b, { ancestors }) => {
388
503
  if (b.meta.group === BlockletGroup.gateway) {
389
504
  return;
390
505
  }
391
506
 
392
- const { appMain, appId, appCwd, logsDir } = b.env;
507
+ const { appMain, processId, appCwd, logsDir } = b.env;
508
+
509
+ if (skippedProcessIds.includes(processId)) {
510
+ logger.info(`skip start process ${processId}`);
511
+ return;
512
+ }
393
513
 
394
514
  // get env
395
- const env = getRuntimeEnvironments(b, nodeEnvironments);
515
+ const env = getRuntimeEnvironments(b, nodeEnvironments, ancestors);
396
516
 
397
517
  // run hook
398
- await preStart(b);
518
+ await preStart(b, { env });
399
519
 
400
520
  // start process
401
521
  const maxMemoryRestart = get(nodeInfo, 'runtimeConfig.blockletMaxMemoryLimit', BLOCKLET_MAX_MEM_LIMIT_IN_MB);
402
522
 
403
523
  const options = {
404
524
  namespace: 'blocklets',
405
- name: appId,
406
- max_memory_restart: `${maxMemoryRestart}M`,
525
+ name: processId,
526
+ cwd: appCwd,
407
527
  time: true,
408
528
  output: path.join(logsDir, 'output.log'),
409
529
  error: path.join(logsDir, 'error.log'),
410
- cwd: appCwd,
530
+ wait_ready: process.env.NODE_ENV !== 'test',
531
+ listen_timeout: 5000,
532
+ max_memory_restart: `${maxMemoryRestart}M`,
411
533
  max_restarts: b.mode === BLOCKLET_MODES.DEVELOPMENT ? 0 : 3,
412
534
  env: {
413
535
  ...env,
@@ -417,17 +539,35 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
417
539
 
418
540
  const clusterMode = get(b.meta, 'capabilities.clusterMode', false);
419
541
  if (clusterMode && blocklet.mode !== BLOCKLET_MODES.DEVELOPMENT) {
420
- const clusterSize = Number(blocklet.configObj.BLOCKLET_CLUSTER_SIZE) || Number.POSITIVE_INFINITY;
542
+ const clusterSize = Number(blocklet.configObj.BLOCKLET_CLUSTER_SIZE) || +process.env.ABT_NODE_MAX_CLUSTER_SIZE;
421
543
  options.execMode = 'cluster';
422
544
  options.mergeLogs = true;
423
545
  options.instances = Math.min(os.cpus().length, clusterSize);
424
546
  }
425
547
 
426
548
  if (b.mode === BLOCKLET_MODES.DEVELOPMENT) {
427
- options.env.NODE_ENV = 'development';
549
+ options.env.NODE_ENV = e2eMode ? 'e2e' : 'development';
428
550
  options.env.BROWSER = 'none';
429
551
  options.env.PORT = options.env[BLOCKLET_DEFAULT_PORT_NAME];
430
552
  options.script = appMain;
553
+
554
+ if (process.platform === 'win32') {
555
+ const [cmd, ...args] = options.script.split(' ').filter(Boolean);
556
+
557
+ if (!SCRIPT_ENGINES_WHITE_LIST.includes(cmd)) {
558
+ throw new Error(`${cmd} script is not supported, ${SCRIPT_ENGINES_WHITE_LIST.join(', ')} are supported`);
559
+ }
560
+
561
+ const { stdout: nodejsBinPath } = shelljs.which('node');
562
+
563
+ const cmdPath = path.join(path.dirname(nodejsBinPath), 'node_modules', cmd);
564
+
565
+ const pkg = JSON.parse(fs.readFileSync(path.join(cmdPath, 'package.json')));
566
+ const cmdBinPath = pkg.bin[cmd];
567
+
568
+ options.script = path.resolve(cmdPath, cmdBinPath);
569
+ options.args = [...args].join(' ');
570
+ }
431
571
  } else {
432
572
  const blockletEngineInfo = getBlockletEngine(b.meta);
433
573
  options.args = blockletEngineInfo.args || [];
@@ -435,16 +575,20 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
435
575
  const engine = getEngine(blockletEngineInfo.interpreter);
436
576
  options.interpreter = engine.interpreter === 'node' ? '' : engine.interpreter;
437
577
  options.interpreterArgs = engine.args || '';
438
-
439
578
  options.script = blockletEngineInfo.script || appMain;
440
-
441
- logger.debug('start.blocklet.engine.info', { blockletEngineInfo });
442
- logger.debug('start.blocklet.max_memory_restart', { maxMemoryRestart });
443
579
  }
444
580
 
445
581
  await pm2.startAsync(options);
446
- logger.info('blocklet started', {
447
- appId,
582
+
583
+ const status = await getProcessState(processId);
584
+ if (status === BlockletStatus.error) {
585
+ throw new Error(`${processId} is not running within 5 seconds`);
586
+ }
587
+ logger.info('blocklet started', { processId, status });
588
+
589
+ // run hook
590
+ postStart(b, { env }).catch((err) => {
591
+ logger.error('blocklet post start failed', { processId, error: err });
448
592
  });
449
593
  },
450
594
  { parallel: true }
@@ -455,12 +599,17 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
455
599
  * Stop all precesses of a blocklet
456
600
  * @param {*} blocklet should contain env props
457
601
  */
458
- const stopBlockletProcess = async (blocklet, { preStop = noop } = {}) => {
602
+ const stopBlockletProcess = async (blocklet, { preStop = noop, skippedProcessIds = [] } = {}) => {
459
603
  await forEachBlocklet(
460
604
  blocklet,
461
- async (b) => {
462
- await preStop(b);
463
- await deleteProcess(b.env.appId);
605
+ async (b, { ancestors }) => {
606
+ if (skippedProcessIds.includes(b.env.processId)) {
607
+ logger.info(`skip stop process ${b.env.processId}`);
608
+ return;
609
+ }
610
+
611
+ await preStop(b, { ancestors });
612
+ await deleteProcess(b.env.processId);
464
613
  },
465
614
  { parallel: true }
466
615
  );
@@ -470,12 +619,22 @@ const stopBlockletProcess = async (blocklet, { preStop = noop } = {}) => {
470
619
  * Delete all precesses of a blocklet
471
620
  * @param {*} blocklet should contain env props
472
621
  */
473
- const deleteBlockletProcess = async (blocklet, { preDelete = noop } = {}) => {
622
+ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProcessIds = [] } = {}) => {
474
623
  await forEachBlocklet(
475
624
  blocklet,
476
- async (b) => {
477
- await preDelete(b);
478
- await deleteProcess(b.env.appId);
625
+ async (b, { ancestors }) => {
626
+ // NOTICE: 如果不判断 group, 在 github action 中测试 disk.spec.js 时会报错, 但是在 mac 中跑测试不会报错
627
+ if (b.meta?.group === BlockletGroup.gateway) {
628
+ return;
629
+ }
630
+
631
+ if (skippedProcessIds.includes(b.env.processId)) {
632
+ logger.info(`skip delete process ${b.env.processId}`);
633
+ return;
634
+ }
635
+
636
+ await preDelete(b, { ancestors });
637
+ await deleteProcess(b.env.processId);
479
638
  },
480
639
  { parallel: true }
481
640
  );
@@ -492,8 +651,8 @@ const reloadBlockletProcess = async (blocklet) =>
492
651
  if (b.meta.group === BlockletGroup.gateway) {
493
652
  return;
494
653
  }
495
- logger.info('reload process', { appId: b.env.appId });
496
- await reloadProcess(b.env.appId);
654
+ logger.info('reload process', { processId: b.env.processId });
655
+ await reloadProcess(b.env.processId);
497
656
  },
498
657
  { parallel: false }
499
658
  );
@@ -502,12 +661,16 @@ const getBlockletStatusFromProcess = async (blocklet) => {
502
661
  const tasks = [];
503
662
  await forEachBlocklet(blocklet, (b) => {
504
663
  if (b.meta.group !== BlockletGroup.gateway) {
505
- tasks.push(getProcessState(b.env.appId));
664
+ tasks.push(getProcessState(b.env.processId));
506
665
  }
507
666
  });
508
667
 
509
668
  const list = await Promise.all(tasks);
510
669
 
670
+ if (!list.length) {
671
+ return blocklet.status;
672
+ }
673
+
511
674
  return getRootBlockletStatus(list);
512
675
  };
513
676
 
@@ -528,11 +691,11 @@ const getRootBlockletStatus = (statusList = []) => {
528
691
  };
529
692
 
530
693
  /**
531
- * @param {*} appId
694
+ * @param {*} processId
532
695
  * @returns {BlockletStatus}
533
696
  */
534
- const getProcessState = async (appId) => {
535
- const info = await getProcessInfo(appId);
697
+ const getProcessState = async (processId) => {
698
+ const info = await getProcessInfo(processId);
536
699
  if (!statusMap[info.pm2_env.status]) {
537
700
  logger.error('Cannot find the blocklet status for pm2 status mapping', {
538
701
  pm2Status: info.pm2_env.status,
@@ -544,9 +707,9 @@ const getProcessState = async (appId) => {
544
707
  return statusMap[info.pm2_env.status];
545
708
  };
546
709
 
547
- const getProcessInfo = (appId) =>
710
+ const getProcessInfo = (processId) =>
548
711
  new Promise((resolve, reject) => {
549
- pm2.describe(appId, async (err, [info]) => {
712
+ pm2.describe(processId, async (err, [info]) => {
550
713
  if (err) {
551
714
  logger.error('Failed to get blocklet status from pm2', { error: err });
552
715
  return reject(err);
@@ -560,22 +723,20 @@ const getProcessInfo = (appId) =>
560
723
  });
561
724
  });
562
725
 
563
- const deleteProcess = (appId) =>
726
+ const deleteProcess = (processId) =>
564
727
  new Promise((resolve, reject) => {
565
- pm2.connect(() => {
566
- pm2.delete(appId, async (err) => {
567
- if (isUsefulError(err)) {
568
- logger.error('blocklet process delete failed', { error: err });
569
- return reject(err);
570
- }
571
- return resolve();
572
- });
728
+ pm2.delete(processId, async (err) => {
729
+ if (isUsefulError(err)) {
730
+ logger.error('blocklet process delete failed', { error: err });
731
+ return reject(err);
732
+ }
733
+ return resolve();
573
734
  });
574
735
  });
575
736
 
576
- const reloadProcess = (appId) =>
737
+ const reloadProcess = (processId) =>
577
738
  new Promise((resolve, reject) => {
578
- pm2.reload(appId, async (err) => {
739
+ pm2.reload(processId, async (err) => {
579
740
  if (err) {
580
741
  if (isUsefulError(err)) {
581
742
  logger.error('blocklet reload failed', { error: err });
@@ -586,30 +747,130 @@ const reloadProcess = (appId) =>
586
747
  });
587
748
  });
588
749
 
589
- const getChildrenMeta = async (meta) => {
590
- const children = [];
591
- if (meta.children && meta.children.length) {
592
- for (const child of meta.children) {
593
- const m = await getBlockletMetaFromUrl(child.resolved);
594
- if (m.name !== child.name) {
595
- logger.error('Resolved child blocklet name does not match in the configuration', {
596
- expected: child.name,
597
- resolved: m.name,
598
- });
599
- throw new Error(
600
- `Child blocklet name does not match in the configuration. expected: ${child.name}, resolved: ${m.name}`
601
- );
750
+ /**
751
+ * this function has side effect to component (will set component.children: Array<staticComponent> )
752
+ * this function has side effect to dynamicComponents (will push dynamicComponent in dynamicComponents)
753
+ *
754
+ * @param {Component} component
755
+ * @param {{
756
+ * ancestors: Array<{meta}>
757
+ * dynamicComponents: Array<{Component}>
758
+ * }} context
759
+ * @returns {{
760
+ * dynamicComponents: Array<Component>,
761
+ * staticComponents: Array<Component>,
762
+ * }}
763
+ */
764
+ const parseComponents = async (component, context = {}) => {
765
+ const { ancestors = [], dynamicComponents = [] } = context;
766
+ if (ancestors.length > 40) {
767
+ throw new Error('The depth of component should not exceed 40');
768
+ }
769
+
770
+ const configs = getComponentConfig(component.meta) || [];
771
+
772
+ if (!configs || !configs.length) {
773
+ return {
774
+ staticComponents: [],
775
+ dynamicComponents,
776
+ };
777
+ }
778
+
779
+ const staticComponents = [];
780
+
781
+ // FIXME @linchen 改成并行获取
782
+ for (const config of configs) {
783
+ const isStatic = !!config.static;
784
+
785
+ if (isStatic) {
786
+ if (!config.name) {
787
+ throw new Error(`Name does not found in child ${config.name}`);
788
+ }
789
+
790
+ if (!config.mountPoint) {
791
+ throw new Error(`MountPoint does not found in child ${config.name}`);
602
792
  }
603
- validateBlockletMeta(m, { ensureDist: true });
604
- children.push(m);
605
793
  }
794
+
795
+ const urls = getSourceUrlsFromConfig(config);
796
+
797
+ let rawMeta;
798
+ try {
799
+ rawMeta = await getBlockletMetaFromUrls(urls, { logger });
800
+ } catch (error) {
801
+ throw new Error(
802
+ `Failed get component meta. Component: ${rawMeta.title || rawMeta.did}, reason: ${error.message}`
803
+ );
804
+ }
805
+
806
+ if (rawMeta.group === BlockletGroup.gateway) {
807
+ throw new Error(`Cannot add gateway component ${rawMeta.title || rawMeta.did}`);
808
+ }
809
+
810
+ validateBlockletMeta(rawMeta, { ensureDist: true });
811
+
812
+ if (!isComponentBlocklet(rawMeta)) {
813
+ throw new Error(`The blocklet cannot be a component: ${rawMeta.title}`);
814
+ }
815
+
816
+ const webInterface = findWebInterface(rawMeta);
817
+ if (!webInterface) {
818
+ throw new Error(`Web interface does not found in component ${rawMeta.title || rawMeta.name}`);
819
+ }
820
+
821
+ const meta = ensureMeta(rawMeta, { name: isStatic ? config.name : null });
822
+
823
+ // check circular dependencies
824
+ if (ancestors.map((x) => x.meta?.bundleDid).indexOf(meta.bundleDid) > -1) {
825
+ throw new Error('Blocklet components have circular dependencies');
826
+ }
827
+
828
+ if (config.title) {
829
+ meta.title = config.title;
830
+ meta.title = await titleSchema.validateAsync(config.title);
831
+ }
832
+
833
+ if (config.description) {
834
+ meta.description = await descriptionSchema.validateAsync(config.description);
835
+ }
836
+
837
+ const mountPoint = isStatic ? config.mountPoint : config.mountPoint || `/${meta.did}`;
838
+
839
+ const child = {
840
+ mountPoint,
841
+ meta,
842
+ bundleSource: config.source,
843
+ dependencies: [],
844
+ };
845
+
846
+ if (isStatic) {
847
+ staticComponents.push(child);
848
+ } else {
849
+ dynamicComponents.push(child);
850
+ component.dependencies = component.dependencies || [];
851
+ component.dependencies.push({
852
+ did: child.meta.bundleDid,
853
+ required: !!config.required,
854
+ version: config.source.version || 'latest',
855
+ });
856
+ }
857
+
858
+ await parseComponents(child, {
859
+ ancestors: [...ancestors, { meta }],
860
+ dynamicComponents,
861
+ });
606
862
  }
607
863
 
608
- return children;
864
+ component.children = staticComponents;
865
+
866
+ return {
867
+ staticComponents,
868
+ dynamicComponents,
869
+ };
609
870
  };
610
871
 
611
872
  const validateBlocklet = (blocklet) =>
612
- forEachBlocklet(blocklet, (b) => {
873
+ forEachBlocklet(blocklet, async (b) => {
613
874
  isRequirementsSatisfied(b.meta.requirements);
614
875
  validateEngine(getBlockletEngineNameByPlatform(b.meta));
615
876
  });
@@ -629,7 +890,7 @@ const checkBlockletProcessHealthy = async (blocklet, { minConsecutiveTime, timeo
629
890
  const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout }) => {
630
891
  const { meta, ports, env } = blocklet;
631
892
  const { name } = meta;
632
- const { appId } = env;
893
+ const { processId } = env;
633
894
 
634
895
  const webInterface = (meta.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
635
896
  if (!webInterface) {
@@ -641,10 +902,10 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout }) =
641
902
  // ensure pm2 status is 'online'
642
903
  const getStatus = async () => {
643
904
  try {
644
- const info = await getProcessInfo(appId);
905
+ const info = await getProcessInfo(processId);
645
906
  return info.pm2_env.status;
646
907
  } catch (err) {
647
- logger.error('blocklet checkStart error', { error: err, appId, name });
908
+ logger.error('blocklet checkStart error', { error: err, processId, name });
648
909
  return '';
649
910
  }
650
911
  };
@@ -673,7 +934,7 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout }) =
673
934
  throw error;
674
935
  }
675
936
  } catch (error) {
676
- logger.error('start blocklet failed', { appId, name });
937
+ logger.error('start blocklet failed', { processId, name });
677
938
  throw error;
678
939
  }
679
940
  };
@@ -703,7 +964,60 @@ const verifyIntegrity = async ({ file, integrity: expected }) => {
703
964
  return true;
704
965
  };
705
966
 
706
- const pruneBlockletBundle = async (blocklets, installDir) => {
967
+ /**
968
+ * @param {string} installDir
969
+ * @returns {Promise<Array<{ key: string, dir: string }>>} key is <[scope/]name/version>, dir is appDir
970
+ */
971
+ const getAppDirs = async (installDir) => {
972
+ const appDirs = [];
973
+
974
+ const getNextLevel = (level, name) => {
975
+ if (level === 'root') {
976
+ if (name.startsWith('@')) {
977
+ return 'scope';
978
+ }
979
+ return 'name';
980
+ }
981
+ if (level === 'scope') {
982
+ return 'name';
983
+ }
984
+ if (level === 'name') {
985
+ return 'version';
986
+ }
987
+ throw new Error(`Invalid level ${level}`);
988
+ };
989
+
990
+ const fillAppDirs = async (dir, level = 'root') => {
991
+ if (level === 'version') {
992
+ appDirs.push({
993
+ key: formatBackSlash(path.relative(installDir, dir)),
994
+ dir,
995
+ });
996
+
997
+ return;
998
+ }
999
+
1000
+ const nextDirs = [];
1001
+ for (const x of await fs.promises.readdir(dir)) {
1002
+ if (!fs.lstatSync(path.join(dir, x)).isDirectory()) {
1003
+ logger.error('pruneBlockletBundle: invalid file in bundle storage', { dir, file: x });
1004
+ // eslint-disable-next-line no-continue
1005
+ continue;
1006
+ }
1007
+ nextDirs.push(x);
1008
+ }
1009
+
1010
+ for (const x of nextDirs) {
1011
+ await fillAppDirs(path.join(dir, x), getNextLevel(level, x));
1012
+ }
1013
+ };
1014
+
1015
+ await fillAppDirs(installDir, 'root');
1016
+
1017
+ return appDirs;
1018
+ };
1019
+
1020
+ const pruneBlockletBundle = async ({ blocklets, installDir, blockletSettings }) => {
707
1021
  for (const blocklet of blocklets) {
708
1022
  if (
709
1023
  [
@@ -714,60 +1028,66 @@ const pruneBlockletBundle = async (blocklets, installDir) => {
714
1028
  ].includes(blocklet.status)
715
1029
  ) {
716
1030
  logger.info('There are blocklet activities in progress, abort pruning', {
717
- name: blocklet.meta.name,
1031
+ bundleName: blocklet.meta.bundleName,
718
1032
  status: fromBlockletStatus(blocklet.status),
719
1033
  });
720
1034
  return;
721
1035
  }
722
1036
  }
723
1037
 
724
- // blockletMap: { <name/version>: true }
725
- const blockletMap = blocklets.reduce((map, b) => {
726
- map[`${b.meta.name}/${b.meta.version}`] = true;
727
- for (const child of b.children || []) {
728
- map[`${child.meta.name}/${child.meta.version}`] = true;
1038
+ // blockletMap: { <[scope/]name/version>: true }
1039
+ const blockletMap = {};
1040
+ for (const blocklet of blocklets) {
1041
+ forEachBlockletSync(blocklet, (component) => {
1042
+ blockletMap[`${component.meta.bundleName}/${component.meta.version}`] = true;
1043
+ });
1044
+ }
1045
+ for (const setting of blockletSettings) {
1046
+ for (const child of setting.children || []) {
1047
+ if (child.status !== BlockletStatus.deleted) {
1048
+ forEachBlockletSync(child, (component) => {
1049
+ blockletMap[`${component.meta.bundleName}/${component.meta.version}`] = true;
1050
+ });
1051
+ }
729
1052
  }
730
- return map;
731
- }, {});
732
-
733
- // appDirs: [{ key: <name/version>, dir: appDir }]
734
- const appDirs = [];
1053
+ }
735
1054
 
736
1055
  // fill appDirs
1056
+ let appDirs = [];
737
1057
  try {
738
- const fillAppDirs = async (dir) => {
739
- if (fs.existsSync(path.join(dir, 'blocklet.yml'))) {
740
- appDirs.push({
741
- key: path.relative(installDir, dir),
742
- dir,
743
- });
744
- return;
745
- }
746
- const nextDirs = [];
747
- for (const x of await asyncFs.readdir(dir)) {
748
- const nextDir = path.join(dir, x);
749
- // if blocklet.yml does not exist in dir but non-folder file exists in dir, stop finding
750
- if (!fs.lstatSync(nextDir).isDirectory()) {
751
- logger.error('blocklet.yml does not exist in blocklet bundle dir', { dir });
752
- return;
753
- }
754
- nextDirs.push(nextDir);
755
- }
756
-
757
- for (const x of nextDirs) {
758
- await fillAppDirs(x);
759
- }
760
- };
761
- await fillAppDirs(installDir);
1058
+ appDirs = await getAppDirs(installDir);
762
1059
  } catch (error) {
763
1060
  logger.error('fill app dirs failed', { error });
764
1061
  }
765
1062
 
1063
+ const ensureBundleDirRemoved = async (dir) => {
1064
+ const relativeDir = path.relative(installDir, dir);
1065
+ const arr = relativeDir.split(path.sep).filter(Boolean);
1066
+ const { length } = arr;
1067
+ const bundleName = arr[length - 2];
1068
+ const scopeName = length > 2 ? arr[length - 3] : '';
1069
+ const bundleDir = path.join(installDir, scopeName, bundleName);
1070
+ const isDirEmpty = (await fs.promises.readdir(bundleDir)).length === 0;
1071
+ if (isDirEmpty) {
1072
+ logger.info('Remove bundle folder', { bundleDir });
1073
+ await fs.remove(bundleDir);
1074
+ }
1075
+ if (scopeName) {
1076
+ const scopeDir = path.join(installDir, scopeName);
1077
+ const isScopeEmpty = (await fs.promises.readdir(scopeDir)).length === 0;
1078
+ if (isScopeEmpty) {
1079
+ logger.info('Remove scope folder', { scopeDir });
1080
+ await fs.remove(scopeDir);
1081
+ }
1082
+ }
1083
+ };
1084
+
766
1085
  // remove trash
767
1086
  for (const app of appDirs) {
768
1087
  if (!blockletMap[app.key]) {
769
- logger.info('Remove blocklet bundle', { dir: app.dir });
1088
+ logger.info('Remove app folder', { dir: app.dir });
770
1089
  await fs.remove(app.dir);
1090
+ await ensureBundleDirRemoved(app.dir);
771
1091
  }
772
1092
  }
773
1093
 
@@ -794,26 +1114,36 @@ const getDiskInfo = async (blocklet, { useFakeDiskInfo } = {}) => {
794
1114
  }
795
1115
  };
796
1116
 
797
- const getRuntimeInfo = async (appId) => {
798
- const proc = await getProcessInfo(appId);
1117
+ const getRuntimeInfo = async (processId) => {
1118
+ const proc = await getProcessInfo(processId);
799
1119
  return {
800
1120
  pid: proc.pid,
801
- uptime: proc.pm2_env ? Number(proc.pm2_env.pm_uptime) : 0,
1121
+ uptime: proc.pm2_env ? +new Date() - Number(proc.pm2_env.pm_uptime) : 0,
802
1122
  memoryUsage: proc.monit.memory,
803
1123
  cpuUsage: proc.monit.cpu,
804
1124
  status: proc.pm2_env ? proc.pm2_env.status : null,
805
1125
  };
806
1126
  };
807
1127
 
808
- const mergeMeta = (meta, childrenMeta = []) => {
1128
+ /**
1129
+ * merge services
1130
+ * from meta.children[].mountPoints[].services, meta.children[].services
1131
+ * to childrenMeta[].interfaces[].services
1132
+ *
1133
+ * @param {array<child>|object{children:array}} source e.g. [<config>] or { children: [<config>] }
1134
+ * @param {array<meta|{meta}>} childrenMeta e.g. [<meta>] or [{ meta: <meta> }]
1135
+ */
1136
+
1137
+ const mergeMeta = (source, childrenMeta = []) => {
809
1138
  // configMap
810
1139
  const configMap = {};
811
- (meta.children || []).forEach((x) => {
1140
+ (Array.isArray(source) ? source : getComponentConfig(source) || []).forEach((x) => {
812
1141
  configMap[x.name] = x;
813
1142
  });
814
1143
 
815
1144
  // merge service from config to child meta
816
- childrenMeta.forEach((childMeta) => {
1145
+ childrenMeta.forEach((child) => {
1146
+ const childMeta = child.meta || child;
817
1147
  const config = configMap[childMeta.name];
818
1148
  if (!config) {
819
1149
  return;
@@ -839,48 +1169,671 @@ const mergeMeta = (meta, childrenMeta = []) => {
839
1169
  childInterface.services = services;
840
1170
  }
841
1171
  });
1172
+
1173
+ if (config.services) {
1174
+ const childInterface = findWebInterface(childMeta);
1175
+ if (childInterface) {
1176
+ // merge
1177
+ const services = childInterface.services || [];
1178
+ config.services.forEach((x) => {
1179
+ const index = services.findIndex((y) => y.name === x.name);
1180
+ if (index >= 0) {
1181
+ services.splice(index, 1, x);
1182
+ } else {
1183
+ services.push(x);
1184
+ }
1185
+ });
1186
+ childInterface.services = services;
1187
+ }
1188
+ }
842
1189
  });
843
1190
  };
844
1191
 
845
- const fixAndVerifyBlockletMeta = (meta, did) => {
846
- if (meta.did !== did) {
847
- throw new Error('Invalid blocklet meta: did does not match');
1192
+ const getUpdateMetaList = (oldBlocklet = {}, newBlocklet = {}) => {
1193
+ const oldMap = {};
1194
+ forEachChildSync(oldBlocklet, (b, { id }) => {
1195
+ if (b.bundleSource) {
1196
+ oldMap[id] = b.meta.version;
1197
+ }
1198
+ });
1199
+
1200
+ const res = [];
1201
+
1202
+ forEachChildSync(newBlocklet, (b, { id }) => {
1203
+ if (b.bundleSource && b.meta.version !== oldMap[id]) {
1204
+ res.push({ id, meta: b.meta });
1205
+ }
1206
+ });
1207
+
1208
+ return res;
1209
+ };
1210
+
1211
+ /**
1212
+ * @returns BLOCKLET_INSTALL_TYPE
1213
+ */
1214
+ const getTypeFromInstallParams = (params) => {
1215
+ if (params.type) {
1216
+ if (!Object.values(BLOCKLET_INSTALL_TYPE).includes(params.type)) {
1217
+ throw new Error(`Can only install blocklet from ${Object.values(BLOCKLET_INSTALL_TYPE).join('/')}`);
1218
+ }
1219
+ return params.type;
1220
+ }
1221
+
1222
+ if (params.url) {
1223
+ return BLOCKLET_INSTALL_TYPE.URL;
1224
+ }
1225
+
1226
+ if (params.file) {
1227
+ throw new Error('install from upload is not supported');
1228
+ }
1229
+
1230
+ if (params.did) {
1231
+ return BLOCKLET_INSTALL_TYPE.STORE;
1232
+ }
1233
+
1234
+ if (params.title && params.description) {
1235
+ return BLOCKLET_INSTALL_TYPE.CREATE;
1236
+ }
1237
+
1238
+ throw new Error(`Can only install blocklet from ${Object.values(BLOCKLET_INSTALL_TYPE).join('/')}`);
1239
+ };
1240
+
1241
+ const checkDuplicateComponents = (components = []) => {
1242
+ const duplicates = components.filter(
1243
+ (item, index) => components.findIndex((x) => x.meta.did === item.meta.did) !== index
1244
+ );
1245
+ if (duplicates.length) {
1246
+ throw new Error(
1247
+ `Cannot add duplicate component${duplicates.length > 1 ? 's' : ''}: ${duplicates
1248
+ .map((x) => getDisplayName(x, true))
1249
+ .join(', ')}`
1250
+ );
1251
+ }
1252
+ };
1253
+
1254
+ const getDiffFiles = async (inputFiles, sourceDir) => {
1255
+ if (!fs.existsSync(sourceDir)) {
1256
+ throw new Error(`${sourceDir} does not exist`);
1257
+ }
1258
+
1259
+ const files = inputFiles.reduce((obj, item) => {
1260
+ obj[item.file] = item.hash;
1261
+ return obj;
1262
+ }, {});
1263
+
1264
+ const { files: sourceFiles } = await hashFiles(sourceDir, {
1265
+ filter: (x) => x.indexOf('node_modules') === -1,
1266
+ concurrentHash: 1,
1267
+ });
1268
+
1269
+ const addSet = [];
1270
+ const changeSet = [];
1271
+ const deleteSet = [];
1272
+
1273
+ const diffFiles = diff(sourceFiles, files);
1274
+ if (diffFiles) {
1275
+ diffFiles.forEach((item) => {
1276
+ if (item.kind === 'D') {
1277
+ deleteSet.push(item.path[0]);
1278
+ }
1279
+ if (item.kind === 'E') {
1280
+ changeSet.push(item.path[0]);
1281
+ }
1282
+ if (item.kind === 'N') {
1283
+ addSet.push(item.path[0]);
1284
+ }
1285
+ });
1286
+ }
1287
+
1288
+ return {
1289
+ addSet,
1290
+ changeSet,
1291
+ deleteSet,
1292
+ };
1293
+ };
1294
+
1295
+ const getBundleDir = (installDir, meta) => path.join(installDir, meta.bundleName || meta.name, meta.version);
1296
+
1297
+ const needBlockletDownload = (blocklet, oldBlocklet) => {
1298
+ if ([BlockletSource.upload, BlockletSource.local, BlockletSource.custom].includes(blocklet.source)) {
1299
+ return false;
1300
+ }
1301
+
1302
+ if (!get(oldBlocklet, 'meta.dist.integrity')) {
1303
+ return true;
1304
+ }
1305
+
1306
+ return get(oldBlocklet, 'meta.dist.integrity') !== get(blocklet, 'meta.dist.integrity');
1307
+ };
1308
+
1309
+ const isDidMatchName = (did, name) => {
1310
+ if (isValidDid(did) && name === did) {
1311
+ return true;
1312
+ }
1313
+
1314
+ return toBlockletDid(name) === did;
1315
+ };
1316
+
1317
+ /**
1318
+ * set bundleDid and bundleMeta in meta
1319
+ * update name and did to server index
1320
+ * in app structure 2.0, application's bundleDid, bundleName, meta, did will be same
1321
+ */
1322
+ const ensureMeta = (meta, { name, did } = {}) => {
1323
+ if (name && did && !isDidMatchName(did, name)) {
1324
+ throw new Error(`name does not match with did: ${name}, ${did}`);
848
1325
  }
849
1326
 
850
- const sanitized = validateMeta(meta, { ensureDist: false, schemaOptions: { noDefaults: true, stripUnknown: true } });
1327
+ const newMeta = {
1328
+ ...meta,
1329
+ };
851
1330
 
1331
+ if (!newMeta.did || !newMeta.name || !isDidMatchName(newMeta.did, newMeta.name)) {
1332
+ throw new Error(`name does not match with did in meta: ${newMeta.name}, ${newMeta.did}`);
1333
+ }
1334
+
1335
+ if (!meta.bundleDid) {
1336
+ newMeta.bundleDid = meta.did;
1337
+ newMeta.bundleName = meta.name;
1338
+ }
1339
+
1340
+ if (name) {
1341
+ newMeta.name = name;
1342
+ newMeta.did = did || toBlockletDid(name);
1343
+ }
1344
+
1345
+ return newMeta;
1346
+ };
1347
+
1348
+ const getBlocklet = async ({
1349
+ did,
1350
+ dataDirs,
1351
+ states,
1352
+ e2eMode = false,
1353
+ throwOnNotExist = true,
1354
+ ensureIntegrity = false,
1355
+ } = {}) => {
1356
+ if (!did) {
1357
+ throw new Error('Blocklet did does not exist');
1358
+ }
1359
+ if (!isValidDid(did)) {
1360
+ throw new Error(`Blocklet did is invalid: ${did}`);
1361
+ }
1362
+
1363
+ if (!dataDirs) {
1364
+ throw new Error('dataDirs does not exist');
1365
+ }
1366
+
1367
+ if (!states) {
1368
+ throw new Error('states does not exist');
1369
+ }
1370
+
1371
+ const blocklet = await states.blocklet.getBlocklet(did);
1372
+ if (!blocklet) {
1373
+ if (throwOnNotExist || ensureIntegrity) {
1374
+ throw new Error(`can not find blocklet in database by did ${did}`);
1375
+ }
1376
+ return null;
1377
+ }
1378
+
1379
+ // app settings
1380
+ const settings = await states.blockletExtras.getSettings(blocklet.meta.did);
1381
+ blocklet.trustedPassports = get(settings, 'trustedPassports') || [];
1382
+ blocklet.enablePassportIssuance = get(settings, 'enablePassportIssuance', true);
1383
+ blocklet.settings = settings || {};
1384
+
1385
+ const extrasMeta = await states.blockletExtras.getMeta(blocklet.meta.did);
1386
+ if (extrasMeta) {
1387
+ blocklet.controller = extrasMeta.controller;
1388
+ }
1389
+
1390
+ blocklet.settings.storeList = blocklet.settings.storeList || [];
1391
+
1392
+ [BLOCKLET_STORE_DEV, BLOCKLET_STORE].forEach((store) => {
1393
+ if (!blocklet.settings.storeList.find((x) => x.url === store.url)) {
1394
+ blocklet.settings.storeList.unshift({
1395
+ ...store,
1396
+ protected: true,
1397
+ });
1398
+ }
1399
+ });
1400
+
1401
+ // app site
1402
+ blocklet.site = await states.site.findOneByBlocklet(blocklet.meta.did);
1403
+
1404
+ await forEachBlocklet(blocklet, async (component, { id, level, ancestors }) => {
1405
+ // component env
1406
+ component.env = {
1407
+ id,
1408
+ name: getComponentName(component, ancestors),
1409
+ processId: getComponentProcessId(component, ancestors),
1410
+ ...getComponentDirs(component, {
1411
+ dataDirs,
1412
+ ensure: ensureIntegrity,
1413
+ validate: ensureIntegrity,
1414
+ ancestors,
1415
+ e2eMode: level === 0 ? e2eMode : false,
1416
+ }),
1417
+ };
1418
+
1419
+ // component config
1420
+ const configs = await states.blockletExtras.getConfigs([...ancestors.map((x) => x.meta.did), component.meta.did]);
1421
+ fillBlockletConfigs(component, configs);
1422
+ });
1423
+
1424
+ return blocklet;
1425
+ };
1426
+
1427
+ /**
1428
+ * this function has side effect on environments
1429
+ */
1430
+ const ensureEnvDefault = (environments, ancestors) => {
1431
+ // remove default if ancestors has a value
1432
+ const envMap = environments.reduce((o, env) => {
1433
+ o[env.name] = env;
1434
+ return o;
1435
+ }, {});
1436
+
1437
+ for (let i = ancestors.length - 1; i >= 0; i--) {
1438
+ const ancestor = ancestors[i];
1439
+ const aEnvironments = get(ancestor.meta, 'environments', []);
1440
+ const aEnv = aEnvironments.find((x) => envMap[x.name]);
1441
+
1442
+ if (!isEnvShareable(aEnv)) {
1443
+ break;
1444
+ }
1445
+
1446
+ const env = envMap[aEnv.name];
1447
+ if (isEnvShareable(env) && aEnv.default) {
1448
+ env.default = '';
1449
+ break;
1450
+ }
1451
+ }
1452
+
1453
+ return environments;
1454
+ };
1455
+
1456
+ const fromProperty2Config = (properties = {}, result) => {
1457
+ Object.keys(properties).forEach((key) => {
1458
+ const prop = properties[key];
1459
+ if (prop.properties && ['ArrayTable', 'ArrayCards'].includes(prop['x-component']) === false) {
1460
+ fromProperty2Config(prop.properties, result);
1461
+ } else if (prop['x-decorator'] === 'FormItem') {
1462
+ const secure = prop['x-component'] === 'Password';
1463
+ result.push({
1464
+ default: prop.default || '',
1465
+ description: prop.title || key,
1466
+ name: `${BLOCKLET_PREFERENCE_PREFIX}${key}`,
1467
+ required: prop.required || false,
1468
+ secure,
1469
+ shared: !secure,
1470
+ });
1471
+ }
1472
+ });
1473
+ };
1474
+ const getConfigFromPreferences = (blocklet) => {
1475
+ const result = [];
1476
+ const schemaFile = path.join(blocklet.env.appDir, BLOCKLET_PREFERENCE_FILE);
1477
+ if (fs.existsSync(schemaFile)) {
1478
+ try {
1479
+ const schema = JSON.parse(fs.readFileSync(schemaFile, 'utf8'));
1480
+ fromProperty2Config(schema.schema?.properties, result);
1481
+ } catch {
1482
+ // do nothing
1483
+ }
1484
+ }
1485
+
1486
+ return result;
1487
+ };
1488
+
1489
+ const consumeServerlessNFT = async ({ nftId, nodeInfo, blocklet }) => {
852
1490
  try {
853
- const result = verifyMultiSig(sanitized);
854
- if (!result) {
855
- throw new Error('invalid signature from developer or registry');
1491
+ const state = await getNFTState(blocklet.controller.chainHost, nftId);
1492
+ if (!state) {
1493
+ throw new Error(`get nft state failed, chainHost: ${blocklet.controller.chainHost}, nftId: ${nftId}`);
856
1494
  }
857
- } catch (err) {
858
- throw new Error(`Invalid blocklet meta: ${err.message}`);
1495
+
1496
+ const type = WalletType({
1497
+ role: types.RoleType.ROLE_APPLICATION,
1498
+ pk: types.KeyType.ED25519,
1499
+ hash: types.HashType.SHA3,
1500
+ });
1501
+ const wallet = fromSecretKey(nodeInfo.sk, type);
1502
+ const appURL = blocklet.environments.find((item) => item.key === 'BLOCKLET_APP_URL').value;
1503
+
1504
+ const body = { nftId, appURL };
1505
+
1506
+ const { launcherUrl } = state.data.value;
1507
+ const { data } = await axios.post(joinURL(launcherUrl, '/api/serverless/consume'), body, {
1508
+ headers: {
1509
+ 'x-sig': toBase58(wallet.sign(stableStringify(body))),
1510
+ },
1511
+ });
1512
+
1513
+ logger.error('consume serverless nft success', { nftId, hash: data.hash });
1514
+ } catch (error) {
1515
+ logger.error('consume serverless nft failed', { nftId, error });
1516
+
1517
+ throw new Error(`consume nft ${nftId} failed`);
859
1518
  }
1519
+ };
860
1520
 
861
- // this step comes last because it has side effects: the meta is changed
862
- return fixAndValidateService(meta, getServices());
1521
+ const createDataArchive = (dataDir, fileName) => {
1522
+ const zipPath = path.join(os.tmpdir(), fileName);
1523
+ if (fs.existsSync(zipPath)) {
1524
+ fs.removeSync(zipPath);
1525
+ }
1526
+
1527
+ const archive = createArchive('zip', { zlib: { level: 9 } });
1528
+ const stream = fs.createWriteStream(zipPath);
1529
+
1530
+ return new Promise((resolve, reject) => {
1531
+ archive
1532
+ .directory(dataDir, false)
1533
+ .on('error', (err) => reject(err))
1534
+ .pipe(stream);
1535
+
1536
+ stream.on('close', () => resolve(zipPath));
1537
+ archive.finalize();
1538
+ });
1539
+ };
1540
+
1541
+ const isBlockletAppSkUsed = ({ environments, migratedFrom = [] }, appSk) => {
1542
+ const isUsedInEnv = environments.find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK)?.value === appSk;
1543
+ const isUsedInHistory = migratedFrom.some((x) => x.appSk === appSk);
1544
+ return isUsedInEnv || isUsedInHistory;
1545
+ };
1546
+
1547
+ const isRotatingAppSk = (newConfigs, oldConfigs, externalSk) => {
1548
+ const newSk = newConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
1549
+ if (!newSk) {
1550
+ // If no newSk found, we are not rotating the appSk
1551
+ return false;
1552
+ }
1553
+
1554
+ const oldSk = oldConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
1555
+ if (!oldSk) {
1556
+ // If we have no oldSk, we are setting the initial appSk for external managed apps
1557
+ // If we have no oldSk, but we are not external managed apps, we are rotating the appSk
1558
+ return !externalSk;
1559
+ }
1560
+
1561
+ // Otherwise, we must be rotating the appSk
1562
+ // eslint-disable-next-line sonarjs/prefer-single-boolean-return
1563
+ if (oldSk.value !== newSk.value) {
1564
+ return true;
1565
+ }
1566
+
1567
+ return false;
863
1568
  };
864
1569
 
865
- const getUpdateMetaList = (oldMetas = [], newMetas = []) => {
866
- const oldMap = oldMetas.reduce((obj, x) => {
867
- if (x.did) {
868
- obj[x.did] = x.version;
1570
+ const isRotatingAppDid = (newConfigs, oldConfigs, externalSk) => {
1571
+ if (isRotatingAppSk(newConfigs, oldConfigs, externalSk)) {
1572
+ return true;
1573
+ }
1574
+
1575
+ const newType = newConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE === x.key);
1576
+ const oldType = oldConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE === x.key);
1577
+ if (!newType) {
1578
+ return false;
1579
+ }
1580
+
1581
+ if (!oldType) {
1582
+ return true;
1583
+ }
1584
+
1585
+ // eslint-disable-next-line sonarjs/prefer-single-boolean-return
1586
+ if (oldType !== newType) {
1587
+ return true;
1588
+ }
1589
+
1590
+ return false;
1591
+ };
1592
+
1593
+ /**
1594
+ * this function has side effect on config.value
1595
+ * @param {{ key: string, value?: string }} config
1596
+ */
1597
+ const validateAppConfig = async (config, states) => {
1598
+ const x = config;
1599
+
1600
+ // sk should be force secured while other app prop should not be secured
1601
+ config.secure = x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK;
1602
+
1603
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
1604
+ if (x.value) {
1605
+ try {
1606
+ fromSecretKey(x.value);
1607
+ } catch {
1608
+ try {
1609
+ fromSecretKey(x.value, 'eth');
1610
+ } catch {
1611
+ throw new Error('Invalid custom blocklet secret key');
1612
+ }
1613
+ }
1614
+
1615
+ // Ensure sk is not used by existing blocklets, otherwise we may encounter appDid collision
1616
+ const blocklets = await states.blocklet.getBlocklets({});
1617
+ if (blocklets.some((b) => isBlockletAppSkUsed(b, x.value))) {
1618
+ throw new Error('Invalid custom blocklet secret key: already used by existing blocklet');
1619
+ }
1620
+ } else {
1621
+ delete x.value;
869
1622
  }
870
- return obj;
871
- }, {});
1623
+ }
1624
+
1625
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_NAME) {
1626
+ x.value = await titleSchema.validateAsync(x.value);
1627
+ }
1628
+
1629
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_DESCRIPTION) {
1630
+ x.value = await descriptionSchema.validateAsync(x.value);
1631
+ }
1632
+
1633
+ if (
1634
+ [
1635
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO,
1636
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_RECT,
1637
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE,
1638
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_FAVICON,
1639
+ ].includes(x.key)
1640
+ ) {
1641
+ x.value = await logoSchema.validateAsync(x.value);
1642
+ }
1643
+
1644
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE) {
1645
+ if (['default', 'eth'].includes(x.value) === false) {
1646
+ throw new Error('Invalid blocklet wallet type, only "default" and "eth" are supported');
1647
+ }
1648
+ }
1649
+
1650
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_DELETABLE) {
1651
+ if (['yes', 'no'].includes(x.value) === false) {
1652
+ throw new Error('BLOCKLET_DELETABLE must be either "yes" or "no"');
1653
+ }
1654
+ }
1655
+
1656
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_PASSPORT_COLOR) {
1657
+ if (x.value && x.value !== 'auto') {
1658
+ if (x.value.length !== 7 || !isHex(x.value.slice(-6))) {
1659
+ throw new Error('BLOCKLET_PASSPORT_COLOR must be a hex encoded color, eg. #ffeeaa');
1660
+ }
1661
+ }
1662
+ }
1663
+
1664
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT) {
1665
+ if (isEmpty(x.value)) {
1666
+ throw new Error(`${x.key} can not be empty`);
1667
+ }
1668
+
1669
+ if (!isUrl(x.value)) {
1670
+ throw new Error(`${x.key}(${x.value}) is not a valid http address`);
1671
+ }
1672
+ }
1673
+
1674
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACES_URL) {
1675
+ if (isEmpty(x.value)) {
1676
+ throw new Error(`${x.key} can not be empty`);
1677
+ }
1678
+
1679
+ if (!isUrl(x.value)) {
1680
+ throw new Error(`${x.key}(${x.value}) is not a valid http address`);
1681
+ }
1682
+ }
1683
+ };
1684
+
1685
+ const checkDuplicateAppSk = async ({ sk, did, states }) => {
1686
+ if (!sk && !did) {
1687
+ throw new Error('sk and did is empty');
1688
+ }
1689
+
1690
+ let appSk = sk;
1691
+ if (!sk) {
1692
+ const nodeInfo = await states.node.read();
1693
+ const blocklet = await states.blocklet.getBlocklet(did);
1694
+ const configs = await states.blockletExtras.getConfigs([did]);
1695
+ const { wallet } = getBlockletInfo(
1696
+ {
1697
+ meta: blocklet.meta,
1698
+ environments: (configs || []).filter((x) => x.value),
1699
+ },
1700
+ nodeInfo.sk
1701
+ );
1702
+ appSk = wallet.secretKey;
1703
+ }
1704
+
1705
+ const blocklets = await states.blocklet.getBlocklets({});
1706
+ const others = did ? blocklets.filter((b) => b.meta.did !== did) : blocklets;
1707
+
1708
+ const exist = others.find((b) => {
1709
+ const item = (b.environments || []).find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK);
1710
+ return item?.value === toHex(appSk);
1711
+ });
1712
+
1713
+ if (exist) {
1714
+ throw new Error(`blocklet secret key already used by ${exist.meta.title || exist.meta.name}`);
1715
+ }
1716
+ };
872
1717
 
873
- return newMetas.filter(({ version, did }) => did && version !== oldMap[did]);
1718
+ const checkDuplicateMountPoint = (blocklet, mountPoint) => {
1719
+ const err = new Error(`cannot add duplicate mount point, ${mountPoint || '/'} already exist`);
1720
+ if (
1721
+ blocklet.meta?.group !== BlockletGroup.gateway &&
1722
+ normalizePathPrefix(blocklet.mountPoint) === normalizePathPrefix(mountPoint)
1723
+ ) {
1724
+ throw err;
1725
+ }
1726
+
1727
+ for (const component of blocklet.children || []) {
1728
+ if (normalizePathPrefix(component.mountPoint) === normalizePathPrefix(mountPoint)) {
1729
+ throw err;
1730
+ }
1731
+ }
1732
+ };
1733
+
1734
+ const validateStore = (nodeInfo, storeUrl) => {
1735
+ if (nodeInfo.mode !== 'serverless') {
1736
+ return;
1737
+ }
1738
+
1739
+ const inStoreList = nodeInfo.blockletRegistryList.find((item) => {
1740
+ const itemURLObj = new URL(item.url);
1741
+ const storeUrlObj = new URL(storeUrl);
1742
+
1743
+ return itemURLObj.host === storeUrlObj.host;
1744
+ });
1745
+
1746
+ if (!inStoreList) {
1747
+ throw new Error('Must be installed from the compliant blocklet store list');
1748
+ }
1749
+ };
1750
+
1751
+ const validateInServerless = ({ blockletMeta }) => {
1752
+ const { interfaces } = blockletMeta;
1753
+ const externalPortInterfaces = (interfaces || []).filter((item) => !!item.port?.external);
1754
+
1755
+ if (externalPortInterfaces.length > 0) {
1756
+ throw new Error('Blocklets with exposed ports cannot be installed');
1757
+ }
1758
+ };
1759
+
1760
+ const checkStructVersion = (blocklet) => {
1761
+ if (blocklet.structVersion !== APP_STRUCT_VERSION) {
1762
+ throw new Error('You should migrate the application first');
1763
+ }
1764
+ };
1765
+
1766
+ const isVersionCompatible = (actualVersion, expectedRange) =>
1767
+ !expectedRange || expectedRange === 'latest' || semver.satisfies(actualVersion, expectedRange);
1768
+
1769
+ const checkVersionCompatibility = (components) => {
1770
+ for (const component of components) {
1771
+ // eslint-disable-next-line no-loop-func
1772
+ forEachBlockletSync(component, (x) => {
1773
+ const dependencies = x.dependencies || [];
1774
+ dependencies.forEach((dep) => {
1775
+ const { did, version: expectedRange } = dep;
1776
+ const exist = components.find((y) => y.meta.did === did);
1777
+ if (exist && !isVersionCompatible(exist.meta.version, expectedRange)) {
1778
+ throw new Error(
1779
+ `Check version compatible failed: ${component.meta.title || component.meta.did} expects ${
1780
+ exist.meta.title || exist.meta.did
1781
+ }'s version to be ${expectedRange}, but actual is ${exist.meta.version}`
1782
+ );
1783
+ }
1784
+ });
1785
+ });
1786
+ }
1787
+ };
1788
+
1789
+ const filterDuplicateComponents = (components = [], currents = []) => {
1790
+ const arr = [];
1791
+
1792
+ components.forEach((component) => {
1793
+ if (currents.some((x) => x.meta.did === component.meta.did)) {
1794
+ return;
1795
+ }
1796
+
1797
+ const index = arr.findIndex((x) => x.meta.did === component.meta.did);
1798
+ if (index > -1) {
1799
+ const exist = arr[index];
1800
+
1801
+ // 选择最小版本:
1802
+ // 如果 com1 和 com2 都依赖某个 component, com1 声明依赖版本 1.0.0, 实际获取版本 1.0.0; com2 声明依赖版本 latest(不限), 实际获取版本 2.0.0 -> 则应该取 1.0.0
1803
+ if (semver.lt(component.meta.version, exist.meta.version)) {
1804
+ arr.splice(index, 1, component);
1805
+ }
1806
+ } else {
1807
+ arr.push(component);
1808
+ }
1809
+ });
1810
+
1811
+ return arr;
1812
+ };
1813
+
1814
+ const validateBlockletMeta = (meta, opts = {}) => {
1815
+ fixAndValidateService(meta);
1816
+ return validateMeta(meta, { ensureName: true, skipValidateDidName: true, schemaOptions: { ...opts } });
1817
+ };
1818
+
1819
+ const getBlockletKnownAs = (blocklet) => {
1820
+ const alsoKnownAs = [blocklet.appDid];
1821
+ if (Array.isArray(blocklet.migratedFrom)) {
1822
+ blocklet.migratedFrom.filter((x) => x.appDid !== blocklet.appPid).forEach((x) => alsoKnownAs.push(x.appDid));
1823
+ }
1824
+
1825
+ return alsoKnownAs.filter(Boolean).map(toDid);
874
1826
  };
875
1827
 
876
1828
  module.exports = {
1829
+ consumeServerlessNFT,
877
1830
  forEachBlocklet,
878
- getBlockletMetaFromUrl,
879
- getChildrenMeta,
880
- getBlockletDirs,
881
- getRootSystemEnvironments,
882
- getSystemEnvironments,
883
- getOverwrittenEnvironments,
1831
+ getBlockletMetaFromUrl: (url) => getBlockletMetaFromUrl(url, { logger }),
1832
+ parseComponents,
1833
+ getComponentDirs,
1834
+ getAppSystemEnvironments,
1835
+ getAppOverwrittenEnvironments,
1836
+ getComponentSystemEnvironments,
884
1837
  getRuntimeEnvironments,
885
1838
  validateBlocklet,
886
1839
  fillBlockletConfigs,
@@ -896,10 +1849,34 @@ module.exports = {
896
1849
  expandTarball,
897
1850
  verifyIntegrity,
898
1851
  statusMap,
1852
+ getAppDirs,
899
1853
  pruneBlockletBundle,
900
1854
  getDiskInfo,
901
1855
  getRuntimeInfo,
902
1856
  mergeMeta,
903
- fixAndVerifyBlockletMeta,
904
1857
  getUpdateMetaList,
1858
+ getTypeFromInstallParams,
1859
+ findWebInterface,
1860
+ checkDuplicateComponents,
1861
+ filterDuplicateComponents,
1862
+ getDiffFiles,
1863
+ getBundleDir,
1864
+ needBlockletDownload,
1865
+ ensureMeta,
1866
+ getBlocklet,
1867
+ ensureEnvDefault,
1868
+ getConfigFromPreferences,
1869
+ createDataArchive,
1870
+ validateAppConfig,
1871
+ isBlockletAppSkUsed,
1872
+ isRotatingAppSk,
1873
+ isRotatingAppDid,
1874
+ checkDuplicateAppSk,
1875
+ checkDuplicateMountPoint,
1876
+ validateStore,
1877
+ validateInServerless,
1878
+ checkStructVersion,
1879
+ checkVersionCompatibility,
1880
+ validateBlockletMeta,
1881
+ getBlockletKnownAs,
905
1882
  };