@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.
- package/lib/api/node.js +67 -69
- package/lib/api/team.js +386 -55
- package/lib/blocklet/downloader/blocklet-downloader.js +226 -0
- package/lib/blocklet/downloader/bundle-downloader.js +272 -0
- package/lib/blocklet/downloader/constants.js +3 -0
- package/lib/blocklet/downloader/resolve-download.js +199 -0
- package/lib/blocklet/extras.js +83 -26
- package/lib/blocklet/hooks.js +18 -65
- package/lib/blocklet/manager/base.js +10 -16
- package/lib/blocklet/manager/disk.js +1680 -1566
- package/lib/blocklet/manager/helper/install-application-from-backup.js +177 -0
- package/lib/blocklet/manager/helper/install-application-from-dev.js +94 -0
- package/lib/blocklet/manager/helper/install-application-from-general.js +188 -0
- package/lib/blocklet/manager/helper/install-component-from-dev.js +84 -0
- package/lib/blocklet/manager/helper/install-component-from-upload.js +181 -0
- package/lib/blocklet/manager/helper/install-component-from-url.js +173 -0
- package/lib/blocklet/manager/helper/migrate-application-to-struct-v2.js +450 -0
- package/lib/blocklet/manager/helper/rollback-cache.js +41 -0
- package/lib/blocklet/manager/helper/upgrade-components.js +152 -0
- package/lib/blocklet/migration.js +30 -52
- package/lib/blocklet/storage/backup/audit-log.js +27 -0
- package/lib/blocklet/storage/backup/base.js +62 -0
- package/lib/blocklet/storage/backup/blocklet-extras.js +92 -0
- package/lib/blocklet/storage/backup/blocklet.js +70 -0
- package/lib/blocklet/storage/backup/blocklets.js +74 -0
- package/lib/blocklet/storage/backup/data.js +19 -0
- package/lib/blocklet/storage/backup/logs.js +24 -0
- package/lib/blocklet/storage/backup/routing-rule.js +19 -0
- package/lib/blocklet/storage/backup/spaces.js +240 -0
- package/lib/blocklet/storage/restore/base.js +67 -0
- package/lib/blocklet/storage/restore/blocklet-extras.js +86 -0
- package/lib/blocklet/storage/restore/blocklet.js +56 -0
- package/lib/blocklet/storage/restore/blocklets.js +43 -0
- package/lib/blocklet/storage/restore/logs.js +21 -0
- package/lib/blocklet/storage/restore/spaces.js +156 -0
- package/lib/blocklet/storage/utils/hash.js +51 -0
- package/lib/blocklet/storage/utils/zip.js +43 -0
- package/lib/cert.js +206 -0
- package/lib/event.js +237 -64
- package/lib/index.js +191 -83
- package/lib/migrations/1.0.21-update-config.js +1 -1
- package/lib/migrations/1.0.22-max-memory.js +1 -1
- package/lib/migrations/1.0.25.js +1 -1
- package/lib/migrations/1.0.32-update-config.js +1 -1
- package/lib/migrations/1.0.33-blocklets.js +1 -1
- package/lib/migrations/1.5.20-registry.js +15 -0
- package/lib/migrations/1.6.17-blocklet-children.js +48 -0
- package/lib/migrations/1.6.21-rename-ip-echo-domain.js +35 -0
- package/lib/migrations/1.6.4-security.js +59 -0
- package/lib/migrations/1.6.5-security.js +60 -0
- package/lib/migrations/1.6.9-update-node-info-and-certificate.js +38 -0
- package/lib/migrations/1.7.1-blocklet-setup.js +18 -0
- package/lib/migrations/1.7.12-blocklet-meta.js +51 -0
- package/lib/migrations/1.7.15-blocklet-bundle-source.js +42 -0
- package/lib/migrations/1.7.20-blocklet-component.js +41 -0
- package/lib/migrations/1.8.33-blocklet-mem-limit.js +20 -0
- package/lib/migrations/README.md +1 -1
- package/lib/migrations/index.js +6 -2
- package/lib/monitor/blocklet-runtime-monitor.js +200 -0
- package/lib/monitor/get-history-list.js +37 -0
- package/lib/monitor/node-runtime-monitor.js +228 -0
- package/lib/router/helper.js +576 -500
- package/lib/router/index.js +85 -21
- package/lib/router/manager.js +146 -187
- package/lib/states/README.md +36 -1
- package/lib/states/access-key.js +39 -17
- package/lib/states/audit-log.js +462 -0
- package/lib/states/base.js +4 -213
- package/lib/states/blocklet-extras.js +195 -138
- package/lib/states/blocklet.js +371 -110
- package/lib/states/cache.js +8 -6
- package/lib/states/challenge.js +5 -5
- package/lib/states/index.js +19 -36
- package/lib/states/migration.js +4 -4
- package/lib/states/node.js +135 -46
- package/lib/states/notification.js +22 -35
- package/lib/states/session.js +17 -9
- package/lib/states/site.js +50 -25
- package/lib/states/user.js +74 -20
- package/lib/states/webhook.js +10 -6
- package/lib/team/manager.js +124 -7
- package/lib/util/blocklet.js +1223 -246
- package/lib/util/chain.js +1 -1
- package/lib/util/default-node-config.js +5 -23
- package/lib/util/disk-monitor.js +13 -10
- package/lib/util/domain-status.js +84 -15
- package/lib/util/get-accessible-external-node-ip.js +2 -2
- package/lib/util/get-domain-for-blocklet.js +13 -0
- package/lib/util/get-meta-from-url.js +33 -0
- package/lib/util/index.js +207 -272
- package/lib/util/ip.js +6 -0
- package/lib/util/maintain.js +233 -0
- package/lib/util/public-to-store.js +85 -0
- package/lib/util/ready.js +1 -1
- package/lib/util/requirement.js +28 -9
- package/lib/util/reset-node.js +22 -7
- package/lib/util/router.js +13 -0
- package/lib/util/rpc.js +16 -0
- package/lib/util/store.js +179 -0
- package/lib/util/sysinfo.js +44 -0
- package/lib/util/ua.js +54 -0
- package/lib/validators/blocklet-extra.js +24 -0
- package/lib/validators/node.js +25 -12
- package/lib/validators/permission.js +16 -1
- package/lib/validators/role.js +17 -3
- package/lib/validators/router.js +40 -20
- package/lib/validators/trusted-passport.js +1 -0
- package/lib/validators/util.js +22 -5
- package/lib/webhook/index.js +45 -35
- package/lib/webhook/sender/index.js +5 -0
- package/lib/webhook/sender/slack/index.js +1 -1
- package/lib/webhook/sender/wallet/index.js +48 -0
- package/package.json +54 -36
- package/lib/blocklet/registry.js +0 -205
- package/lib/states/https-cert.js +0 -67
- package/lib/util/get-ip-dns-domain-for-blocklet.js +0 -19
- package/lib/util/service.js +0 -66
- package/lib/util/upgrade.js +0 -178
- /package/lib/{queue.js → util/queue.js} +0 -0
package/lib/util/blocklet.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
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
|
|
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
|
|
94
|
-
* appDir:
|
|
95
|
-
* mainDir: appDir (dapp) or appDir/main (static). Used for for static
|
|
96
|
-
* appMain: app entry file or script (run appMain to start
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 } =
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 {
|
|
167
|
+
const { main, group } = component.meta;
|
|
130
168
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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 (
|
|
142
|
-
appDir =
|
|
184
|
+
if (component.source === BlockletSource.local) {
|
|
185
|
+
appDir = component.deployedFrom;
|
|
143
186
|
} else {
|
|
144
|
-
appDir =
|
|
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(
|
|
207
|
+
if (validate && !startFromDevEntry && !isBeforeInstalled(component.status)) {
|
|
154
208
|
try {
|
|
155
|
-
validateBlockletEntry(appDir,
|
|
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 =
|
|
222
|
+
appMain = startFromDevEntry;
|
|
169
223
|
appCwd = appDir;
|
|
170
224
|
} else if (group === 'dapp') {
|
|
171
|
-
appMain = getBlockletEngine(
|
|
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.
|
|
196
|
-
blocklet.environmentObj =
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
383
|
+
const safeNodeEnvironments = PRIVATE_NODE_ENVS.reduce((o, x) => {
|
|
305
384
|
o[x] = '';
|
|
306
385
|
return o;
|
|
307
386
|
}, {});
|
|
308
387
|
|
|
309
|
-
|
|
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) =>
|
|
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 (
|
|
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,
|
|
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:
|
|
406
|
-
|
|
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
|
-
|
|
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) ||
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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', {
|
|
496
|
-
await reloadProcess(b.env.
|
|
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.
|
|
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 {*}
|
|
694
|
+
* @param {*} processId
|
|
532
695
|
* @returns {BlockletStatus}
|
|
533
696
|
*/
|
|
534
|
-
const getProcessState = async (
|
|
535
|
-
const info = await getProcessInfo(
|
|
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 = (
|
|
710
|
+
const getProcessInfo = (processId) =>
|
|
548
711
|
new Promise((resolve, reject) => {
|
|
549
|
-
pm2.describe(
|
|
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 = (
|
|
726
|
+
const deleteProcess = (processId) =>
|
|
564
727
|
new Promise((resolve, reject) => {
|
|
565
|
-
pm2.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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 = (
|
|
737
|
+
const reloadProcess = (processId) =>
|
|
577
738
|
new Promise((resolve, reject) => {
|
|
578
|
-
pm2.reload(
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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,
|
|
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', {
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
798
|
-
const proc = await getProcessInfo(
|
|
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
|
-
|
|
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
|
-
(
|
|
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((
|
|
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
|
|
846
|
-
|
|
847
|
-
|
|
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
|
|
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
|
|
854
|
-
if (!
|
|
855
|
-
throw new Error(
|
|
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
|
-
|
|
858
|
-
|
|
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
|
-
|
|
862
|
-
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
};
|