@abtnode/core 1.17.8-beta-20260108-224855-28496abb → 1.17.8-beta-20260111-112953-aed5ff39

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 (66) hide show
  1. package/lib/api/team/access-key-manager.js +104 -0
  2. package/lib/api/team/invitation-manager.js +461 -0
  3. package/lib/api/team/notification-manager.js +189 -0
  4. package/lib/api/team/oauth-manager.js +60 -0
  5. package/lib/api/team/org-crud-manager.js +202 -0
  6. package/lib/api/team/org-manager.js +56 -0
  7. package/lib/api/team/org-member-manager.js +403 -0
  8. package/lib/api/team/org-query-manager.js +126 -0
  9. package/lib/api/team/org-resource-manager.js +186 -0
  10. package/lib/api/team/passport-manager.js +670 -0
  11. package/lib/api/team/rbac-manager.js +335 -0
  12. package/lib/api/team/session-manager.js +540 -0
  13. package/lib/api/team/store-manager.js +198 -0
  14. package/lib/api/team/tag-manager.js +230 -0
  15. package/lib/api/team/user-auth-manager.js +132 -0
  16. package/lib/api/team/user-manager.js +78 -0
  17. package/lib/api/team/user-query-manager.js +299 -0
  18. package/lib/api/team/user-social-manager.js +354 -0
  19. package/lib/api/team/user-update-manager.js +224 -0
  20. package/lib/api/team/verify-code-manager.js +161 -0
  21. package/lib/api/team.js +439 -3287
  22. package/lib/blocklet/manager/disk/auth-manager.js +68 -0
  23. package/lib/blocklet/manager/disk/backup-manager.js +288 -0
  24. package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
  25. package/lib/blocklet/manager/disk/component-manager.js +83 -0
  26. package/lib/blocklet/manager/disk/config-manager.js +191 -0
  27. package/lib/blocklet/manager/disk/controller-manager.js +64 -0
  28. package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
  29. package/lib/blocklet/manager/disk/download-manager.js +96 -0
  30. package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
  31. package/lib/blocklet/manager/disk/federated-manager.js +651 -0
  32. package/lib/blocklet/manager/disk/hook-manager.js +124 -0
  33. package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
  34. package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
  35. package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
  36. package/lib/blocklet/manager/disk/install-manager.js +36 -0
  37. package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
  38. package/lib/blocklet/manager/disk/job-manager.js +467 -0
  39. package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
  40. package/lib/blocklet/manager/disk/notification-manager.js +343 -0
  41. package/lib/blocklet/manager/disk/query-manager.js +562 -0
  42. package/lib/blocklet/manager/disk/settings-manager.js +507 -0
  43. package/lib/blocklet/manager/disk/start-manager.js +611 -0
  44. package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
  45. package/lib/blocklet/manager/disk/update-manager.js +153 -0
  46. package/lib/blocklet/manager/disk.js +669 -5796
  47. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
  48. package/lib/blocklet/manager/lock.js +18 -0
  49. package/lib/event/index.js +28 -24
  50. package/lib/router/helper.js +5 -1
  51. package/lib/util/blocklet/app-utils.js +192 -0
  52. package/lib/util/blocklet/blocklet-loader.js +258 -0
  53. package/lib/util/blocklet/config-manager.js +232 -0
  54. package/lib/util/blocklet/did-document.js +240 -0
  55. package/lib/util/blocklet/environment.js +555 -0
  56. package/lib/util/blocklet/health-check.js +449 -0
  57. package/lib/util/blocklet/install-utils.js +365 -0
  58. package/lib/util/blocklet/logo.js +57 -0
  59. package/lib/util/blocklet/meta-utils.js +269 -0
  60. package/lib/util/blocklet/port-manager.js +141 -0
  61. package/lib/util/blocklet/process-manager.js +504 -0
  62. package/lib/util/blocklet/runtime-info.js +105 -0
  63. package/lib/util/blocklet/validation.js +418 -0
  64. package/lib/util/blocklet.js +98 -3066
  65. package/lib/util/wallet-app-notification.js +40 -0
  66. package/package.json +22 -22
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Runtime Information Module
3
+ *
4
+ * Functions for getting blocklet runtime and disk information
5
+ * Extracted from blocklet.js for better modularity
6
+ */
7
+
8
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet:runtime-info');
9
+ const getFolderSize = require('@abtnode/util/lib/get-folder-size');
10
+ const { getDisplayName } = require('@blocklet/meta/lib/util');
11
+
12
+ const { getProcessInfo } = require('./process-manager');
13
+ const getDockerRuntimeInfo = require('../docker/get-docker-runtime-info');
14
+
15
+ /**
16
+ * Cache for disk info tasks to avoid duplicate calculations
17
+ */
18
+ const _diskInfoTasks = {};
19
+
20
+ /**
21
+ * Internal function to get disk info for a blocklet
22
+ * @param {object} blocklet - Blocklet object with env
23
+ * @returns {Promise<{ app: number, cache: number, log: number, data: number }>}
24
+ */
25
+ const _getDiskInfo = async (blocklet) => {
26
+ try {
27
+ const { env } = blocklet;
28
+ const [app, cache, log, data] = await Promise.all([
29
+ getFolderSize(env.appDir),
30
+ getFolderSize(env.cacheDir),
31
+ getFolderSize(env.logsDir),
32
+ getFolderSize(env.dataDir),
33
+ ]);
34
+ return { app, cache, log, data };
35
+ } catch (error) {
36
+ logger.error('Get disk info failed', { name: getDisplayName(blocklet), error });
37
+ return { app: 0, cache: 0, log: 0, data: 0 };
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Get disk info for a blocklet with caching
43
+ * @param {object} blocklet - Blocklet object
44
+ * @param {object} options - Options
45
+ * @param {boolean} options.useFakeDiskInfo - Return zeros instead of actual values
46
+ * @returns {Promise<{ app: number, cache: number, log: number, data: number }>}
47
+ */
48
+ const getDiskInfo = (blocklet, { useFakeDiskInfo } = {}) => {
49
+ if (useFakeDiskInfo) {
50
+ return { app: 0, cache: 0, log: 0, data: 0 };
51
+ }
52
+
53
+ const { appDid } = blocklet;
54
+
55
+ // Cache disk info results for 5 minutes
56
+ _diskInfoTasks[appDid] ??= _getDiskInfo(blocklet).finally(() => {
57
+ setTimeout(
58
+ () => {
59
+ delete _diskInfoTasks[appDid];
60
+ },
61
+ 5 * 60 * 1000
62
+ );
63
+ });
64
+
65
+ return new Promise((resolve) => {
66
+ _diskInfoTasks[appDid].then(resolve).catch(() => {
67
+ resolve({ app: 0, cache: 0, log: 0, data: 0 });
68
+ });
69
+ });
70
+ };
71
+
72
+ /**
73
+ * Get runtime info for a process
74
+ * @param {string} processId - PM2 process ID
75
+ * @returns {Promise<object>} Runtime info including pid, uptime, memory, cpu, status, port
76
+ */
77
+ const getRuntimeInfo = async (processId) => {
78
+ const proc = await getProcessInfo(processId);
79
+ const dockerName = proc.pm2_env?.env?.dockerName;
80
+ if (dockerName) {
81
+ const dockerInfo = await getDockerRuntimeInfo(dockerName);
82
+ return {
83
+ ...dockerInfo,
84
+ pid: proc.pid,
85
+ uptime: proc.pm2_env ? Date.now() - Number(proc.pm2_env.pm_uptime) : 0,
86
+ port: proc.pm2_env ? proc.pm2_env.BLOCKLET_PORT : null,
87
+ status: proc.pm2_env ? proc.pm2_env.status : null,
88
+ runningDocker: !!dockerName,
89
+ };
90
+ }
91
+ return {
92
+ pid: proc.pid,
93
+ uptime: proc.pm2_env ? Date.now() - Number(proc.pm2_env.pm_uptime) : 0,
94
+ memoryUsage: proc.monit.memory,
95
+ cpuUsage: proc.monit.cpu,
96
+ status: proc.pm2_env ? proc.pm2_env.status : null,
97
+ port: proc.pm2_env ? proc.pm2_env.BLOCKLET_PORT : null,
98
+ runningDocker: false,
99
+ };
100
+ };
101
+
102
+ module.exports = {
103
+ getDiskInfo,
104
+ getRuntimeInfo,
105
+ };
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Validation Module
3
+ *
4
+ * Functions for validating blocklet configurations, structures, and compatibility
5
+ * Extracted from blocklet.js for better modularity
6
+ */
7
+
8
+ const semver = require('semver');
9
+ const isEmpty = require('lodash/isEmpty');
10
+ const isUrl = require('is-url');
11
+
12
+ const { types } = require('@ocap/mcrypto');
13
+ const { fromSecretKey } = require('@ocap/wallet');
14
+ const { isHex } = require('@ocap/util');
15
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet:validation');
16
+ const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
17
+ const { APP_STRUCT_VERSION } = require('@abtnode/constant');
18
+ const { chainInfo: chainInfoSchema } = require('@arcblock/did-connect-js/lib/schema');
19
+
20
+ const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
21
+ const { titleSchema, descriptionSchema, logoSchema } = require('@blocklet/meta/lib/schema');
22
+ const {
23
+ forEachBlockletSync,
24
+ forEachComponentV2,
25
+ getDisplayName,
26
+ getChainInfo,
27
+ hasStartEngine,
28
+ } = require('@blocklet/meta/lib/util');
29
+ const { getBlockletInfo } = require('@blocklet/meta/lib/info');
30
+ const { hasMountPoint } = require('@blocklet/meta/lib/engine');
31
+
32
+ const formatName = require('@abtnode/util/lib/format-name');
33
+ const isRequirementsSatisfied = require('../requirement');
34
+ const { validate: validateEngine } = require('../../blocklet/manager/engine');
35
+
36
+ /**
37
+ * Validate blocklet requirements and engine
38
+ * @param {object} blocklet - Blocklet object
39
+ * @param {Function} getBlockletEngineNameByPlatform - Function to get engine name
40
+ */
41
+ const validateBlocklet = (blocklet, getBlockletEngineNameByPlatform) =>
42
+ forEachComponentV2(blocklet, (b) => {
43
+ isRequirementsSatisfied(b.meta.requirements);
44
+ validateEngine(getBlockletEngineNameByPlatform(b.meta));
45
+ });
46
+
47
+ /**
48
+ * Validate blocklet chain info configuration
49
+ * @param {object} blocklet - Blocklet object with configObj
50
+ * @returns {object} Validated chain info
51
+ */
52
+ const validateBlockletChainInfo = (blocklet) => {
53
+ const chainInfo = getChainInfo({
54
+ CHAIN_TYPE: blocklet.configObj[BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_TYPE],
55
+ CHAIN_ID: blocklet.configObj[BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_ID],
56
+ CHAIN_HOST: blocklet.configObj[BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_HOST],
57
+ });
58
+
59
+ const { error } = chainInfoSchema.validate(chainInfo);
60
+ if (error) {
61
+ throw error;
62
+ }
63
+
64
+ return chainInfo;
65
+ };
66
+
67
+ /**
68
+ * Check for duplicate components
69
+ * @param {Array} components - Array of components
70
+ */
71
+ const checkDuplicateComponents = (components = []) => {
72
+ const duplicates = components.filter(
73
+ (item, index) => components.findIndex((x) => x.meta.did === item.meta.did) !== index
74
+ );
75
+ if (duplicates.length) {
76
+ throw new Error(
77
+ `Cannot add duplicate component${duplicates.length > 1 ? 's' : ''}: ${duplicates
78
+ .map((x) => getDisplayName(x, true))
79
+ .join(', ')}`
80
+ );
81
+ }
82
+ };
83
+
84
+ /**
85
+ * Validate app config
86
+ * @param {object} config - Config object
87
+ * @param {object} states - State manager
88
+ */
89
+ const validateAppConfig = async (config, states) => {
90
+ const x = config;
91
+
92
+ // sk should be force secured while other app prop should not be secured
93
+ config.secure = x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK;
94
+
95
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
96
+ if (x.value) {
97
+ let wallet;
98
+ try {
99
+ wallet = fromSecretKey(x.value, { role: types.RoleType.ROLE_APPLICATION });
100
+ } catch {
101
+ try {
102
+ wallet = fromSecretKey(x.value, 'eth');
103
+ } catch {
104
+ throw new Error('Invalid custom blocklet secret key');
105
+ }
106
+ }
107
+
108
+ // Ensure sk is not used by existing blocklets, otherwise we may encounter appDid collision
109
+ const exist = await states.blocklet.hasBlocklet(wallet.address);
110
+ if (exist) {
111
+ throw new Error('Invalid custom blocklet secret key: already used by existing blocklet');
112
+ }
113
+ } else {
114
+ delete x.value;
115
+ }
116
+ }
117
+
118
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_URL) {
119
+ if (isEmpty(x.value)) {
120
+ throw new Error(`${x.key} can not be empty`);
121
+ }
122
+
123
+ if (!isUrl(x.value)) {
124
+ throw new Error(`${x.key}(${x.value}) is not a valid URL`);
125
+ }
126
+ }
127
+
128
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_NAME) {
129
+ x.value = await titleSchema.validateAsync(x.value);
130
+ }
131
+
132
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_DESCRIPTION) {
133
+ x.value = await descriptionSchema.validateAsync(x.value);
134
+ }
135
+
136
+ if (
137
+ [
138
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO,
139
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_RECT,
140
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_RECT_DARK,
141
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE,
142
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE_DARK,
143
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_FAVICON,
144
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPLASH_PORTRAIT,
145
+ BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPLASH_LANDSCAPE,
146
+ ].includes(x.key)
147
+ ) {
148
+ x.value = await logoSchema.validateAsync(x.value);
149
+ }
150
+
151
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_CHAIN_TYPE) {
152
+ if (['arcblock', 'ethereum'].includes(x.value) === false) {
153
+ throw new Error('Invalid blocklet wallet type, only "default" and "eth" are supported');
154
+ }
155
+ }
156
+
157
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_DELETABLE) {
158
+ if (['yes', 'no'].includes(x.value) === false) {
159
+ throw new Error('BLOCKLET_DELETABLE must be either "yes" or "no"');
160
+ }
161
+ }
162
+
163
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_PASSPORT_COLOR) {
164
+ if (x.value && x.value !== 'auto') {
165
+ if (x.value.length !== 7 || !isHex(x.value.slice(-6))) {
166
+ throw new Error('BLOCKLET_PASSPORT_COLOR must be a hex encoded color, eg. #ffeeaa');
167
+ }
168
+ }
169
+ }
170
+
171
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT) {
172
+ // @note: value 置空以表删除
173
+ if (x.value && !isUrl(x.value)) {
174
+ throw new Error(`${x.key}(${x.value}) is not a valid URL`);
175
+ }
176
+ }
177
+
178
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACES_URL) {
179
+ if (isEmpty(x.value)) {
180
+ throw new Error(`${x.key} can not be empty`);
181
+ }
182
+
183
+ if (!isUrl(x.value)) {
184
+ throw new Error(`${x.key}(${x.value}) is not a valid URL`);
185
+ }
186
+ }
187
+
188
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT) {
189
+ // @note: value 置空以表删除
190
+ if (isEmpty(x.value)) {
191
+ x.value = '';
192
+ }
193
+
194
+ if (!isEmpty(x.value) && !isUrl(x.value)) {
195
+ throw new Error(`${x.key}(${x.value}) is not a valid URL`);
196
+ }
197
+ }
198
+
199
+ if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_CLUSTER_SIZE) {
200
+ if (isEmpty(x.value)) {
201
+ x.value = '';
202
+ }
203
+
204
+ const v = Number(x.value);
205
+ if (Number.isNaN(v)) {
206
+ throw new Error(`${x.key} must be number`);
207
+ }
208
+ if (!Number.isInteger(v)) {
209
+ throw new Error(`${x.key} must be integer`);
210
+ }
211
+ }
212
+ };
213
+
214
+ /**
215
+ * Check for duplicate app secret key
216
+ * @param {object} options - Options
217
+ * @param {string} options.sk - Secret key
218
+ * @param {string} options.did - Blocklet DID
219
+ * @param {object} options.states - State manager
220
+ */
221
+ const checkDuplicateAppSk = async ({ sk, did, states }) => {
222
+ if (!sk && !did) {
223
+ throw new Error('sk and did is empty');
224
+ }
225
+
226
+ let appSk = sk;
227
+ if (!sk) {
228
+ const nodeInfo = await states.node.read();
229
+ const blocklet = await states.blocklet.getBlocklet(did);
230
+ const configs = await states.blockletExtras.getConfigs([did]);
231
+ const { wallet } = getBlockletInfo(
232
+ {
233
+ meta: blocklet.meta,
234
+ environments: (configs || []).filter((x) => x.value),
235
+ },
236
+ nodeInfo.sk
237
+ );
238
+ appSk = wallet.secretKey;
239
+ }
240
+
241
+ let wallet;
242
+ try {
243
+ wallet = fromSecretKey(appSk, { role: types.RoleType.ROLE_APPLICATION });
244
+ } catch {
245
+ try {
246
+ wallet = fromSecretKey(appSk, 'eth');
247
+ } catch {
248
+ throw new Error('Invalid custom blocklet secret key');
249
+ }
250
+ }
251
+
252
+ const exist = await states.blocklet.hasBlocklet(wallet.address);
253
+ if (exist) {
254
+ throw new Error(`blocklet secret key already used by ${exist.meta.title || exist.meta.name}`);
255
+ }
256
+ };
257
+
258
+ /**
259
+ * Check for duplicate mount point
260
+ * @param {object} app - App blocklet
261
+ * @param {string} mountPoint - Mount point to check
262
+ */
263
+ const checkDuplicateMountPoint = (app, mountPoint) => {
264
+ const err = new Error(`cannot add duplicate mount point, ${mountPoint || '/'} already exist`);
265
+
266
+ for (const component of app.children || []) {
267
+ if (
268
+ hasStartEngine(component.meta) &&
269
+ normalizePathPrefix(component.mountPoint) === normalizePathPrefix(mountPoint)
270
+ ) {
271
+ throw err;
272
+ }
273
+ }
274
+ };
275
+
276
+ /**
277
+ * Resolve mount point conflict by generating alternative mount point
278
+ * @param {object} comp - Component
279
+ * @param {object} blocklet - Parent blocklet
280
+ * @returns {object} Component with resolved mount point
281
+ */
282
+ const resolveMountPointConflict = (comp, blocklet) => {
283
+ try {
284
+ if (!comp?.mountPoint) return comp;
285
+
286
+ const children = (blocklet?.children || []).filter((x) => x?.meta && hasMountPoint(x.meta));
287
+
288
+ const existingComponent = children.find((x) => x.mountPoint === comp.mountPoint && x.meta?.did !== comp.meta?.did);
289
+ if (!existingComponent) return comp;
290
+
291
+ const baseName = formatName(comp?.meta?.name) || formatName(comp?.meta?.title);
292
+ comp.mountPoint = children.some((x) => x.mountPoint === `/${baseName}`)
293
+ ? comp.meta.did
294
+ : baseName.toLocaleLowerCase();
295
+
296
+ return comp;
297
+ } catch (error) {
298
+ logger.error('Failed to resolve mount point:', error);
299
+ comp.mountPoint = comp.meta.did;
300
+ return comp;
301
+ }
302
+ };
303
+
304
+ /**
305
+ * Validate store URL in serverless mode
306
+ * @param {object} nodeInfo - Node info
307
+ * @param {string} storeUrl - Store URL
308
+ */
309
+ const validateStore = (nodeInfo, storeUrl) => {
310
+ if (nodeInfo.mode !== 'serverless') {
311
+ return;
312
+ }
313
+
314
+ const storeUrlObj = new URL(storeUrl);
315
+
316
+ // Check trusted blocklet sources from environment variable first
317
+ const trustedSources = process.env.ABT_NODE_TRUSTED_SOURCES;
318
+ if (trustedSources) {
319
+ const trustedHosts = trustedSources
320
+ .split(',')
321
+ .map((url) => url.trim())
322
+ .filter(Boolean)
323
+ .map((url) => new URL(url).host);
324
+
325
+ if (trustedHosts.includes(storeUrlObj.host)) {
326
+ return;
327
+ }
328
+ }
329
+
330
+ const registerUrlObj = new URL(nodeInfo.registerUrl);
331
+
332
+ // 信任 Launcher 打包的应用
333
+ if (registerUrlObj.host === storeUrlObj.host) {
334
+ return;
335
+ }
336
+
337
+ const inStoreList = nodeInfo.blockletRegistryList.find((item) => {
338
+ const itemURLObj = new URL(item.url);
339
+
340
+ return itemURLObj.host === storeUrlObj.host;
341
+ });
342
+
343
+ if (!inStoreList) {
344
+ throw new Error('Must be installed from the compliant blocklet store list');
345
+ }
346
+ };
347
+
348
+ /**
349
+ * Validate blocklet in serverless mode
350
+ * @param {object} options - Options
351
+ * @param {object} options.blockletMeta - Blocklet meta
352
+ */
353
+ const validateInServerless = ({ blockletMeta }) => {
354
+ const { interfaces } = blockletMeta;
355
+ const externalPortInterfaces = (interfaces || []).filter((item) => !!item.port?.external);
356
+
357
+ if (externalPortInterfaces.length > 0) {
358
+ throw new Error('Blocklets with exposed ports cannot be installed');
359
+ }
360
+ };
361
+
362
+ /**
363
+ * Check struct version
364
+ * @param {object} blocklet - Blocklet object
365
+ */
366
+ const checkStructVersion = (blocklet) => {
367
+ if (blocklet.structVersion !== APP_STRUCT_VERSION) {
368
+ throw new Error('You should migrate the application first');
369
+ }
370
+ };
371
+
372
+ /**
373
+ * Check if version is compatible
374
+ * @param {string} actualVersion - Actual version
375
+ * @param {string} expectedRange - Expected version range
376
+ * @returns {boolean}
377
+ */
378
+ const isVersionCompatible = (actualVersion, expectedRange) =>
379
+ !expectedRange || expectedRange === 'latest' || semver.satisfies(actualVersion, expectedRange);
380
+
381
+ /**
382
+ * Check version compatibility of components
383
+ * @param {Array} components - Array of components
384
+ */
385
+ const checkVersionCompatibility = (components) => {
386
+ for (const component of components) {
387
+ // eslint-disable-next-line no-loop-func
388
+ forEachBlockletSync(component, (x) => {
389
+ const dependencies = x.dependencies || [];
390
+ dependencies.forEach((dep) => {
391
+ const { did, version: expectedRange } = dep;
392
+ const exist = components.find((y) => y.meta.did === did);
393
+ if (exist && !isVersionCompatible(exist.meta.version, expectedRange)) {
394
+ throw new Error(
395
+ `Check version compatible failed: ${component.meta.title || component.meta.did} expects ${
396
+ exist.meta.title || exist.meta.did
397
+ }'s version to be ${expectedRange}, but actual is ${exist.meta.version}`
398
+ );
399
+ }
400
+ });
401
+ });
402
+ }
403
+ };
404
+
405
+ module.exports = {
406
+ validateBlocklet,
407
+ validateBlockletChainInfo,
408
+ checkDuplicateComponents,
409
+ validateAppConfig,
410
+ checkDuplicateAppSk,
411
+ checkDuplicateMountPoint,
412
+ resolveMountPointConflict,
413
+ validateStore,
414
+ validateInServerless,
415
+ checkStructVersion,
416
+ isVersionCompatible,
417
+ checkVersionCompatibility,
418
+ };