@abtnode/core 1.16.0-beta-8ee536d7 → 1.16.0-beta-62b42401

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ const Client = require('@ocap/client');
6
6
 
7
7
  const logger = require('@abtnode/logger')('@abtnode/core:migrate-application-to-struct-v2');
8
8
 
9
- const { forEachBlockletSync, getSharedConfigObj, getChainInfo } = require('@blocklet/meta/lib/util');
9
+ const { forEachBlockletSync, forEachChildSync, getSharedConfigObj, getChainInfo } = require('@blocklet/meta/lib/util');
10
10
  const { SLOT_FOR_IP_DNS_SITE } = require('@abtnode/constant');
11
11
 
12
12
  const {
@@ -91,6 +91,31 @@ const fillBlockletData = (data, app, id) => {
91
91
 
92
92
  const appSystemFiles = ['logo.svg', 'rbac.db', 'session.db', 'user.db', '.assets', BLOCKLET_UPLOADS_DIR];
93
93
 
94
+ const getChainHost = (blocklet) => {
95
+ if (!blocklet) {
96
+ return 'none';
97
+ }
98
+
99
+ const chainInfo = getChainInfo(blocklet.configObj);
100
+ if (chainInfo.host !== 'none') {
101
+ return chainInfo.host;
102
+ }
103
+
104
+ let childChainHost;
105
+ forEachChildSync(blocklet, (b) => {
106
+ if (childChainHost) {
107
+ return;
108
+ }
109
+
110
+ const childChainInfo = getChainInfo(b.configObj);
111
+ if (childChainInfo.host !== 'none') {
112
+ childChainHost = childChainInfo.host;
113
+ }
114
+ });
115
+
116
+ return childChainHost || 'none';
117
+ };
118
+
94
119
  const migrateAppOnChain = async (blocklet, oldSk, newSk) => {
95
120
  logger.info('Preparing for on-chain migration', { did: blocklet.meta.did });
96
121
  if (!oldSk) {
@@ -104,13 +129,13 @@ const migrateAppOnChain = async (blocklet, oldSk, newSk) => {
104
129
  }
105
130
 
106
131
  // ensure chain host
107
- const chainInfo = getChainInfo(blocklet.configObj);
108
- if (chainInfo.host === 'none') {
132
+ const chainHost = getChainHost(blocklet);
133
+ if (!chainHost || chainHost === 'none') {
109
134
  logger.info('on-chain migration aborted because CHAIN_HOST is empty', { did: blocklet.meta.did });
110
135
  return;
111
136
  }
112
137
 
113
- logger.info('on-chain migration for chain ', { did: blocklet.meta.did, host: chainInfo.host });
138
+ logger.info('on-chain migration for chain ', { did: blocklet.meta.did, host: chainHost });
114
139
 
115
140
  // ensure account changed
116
141
  const type = blocklet.configObj.BLOCKLET_WALLET_TYPE;
@@ -122,7 +147,7 @@ const migrateAppOnChain = async (blocklet, oldSk, newSk) => {
122
147
  }
123
148
 
124
149
  // ensure old account exist on chain
125
- const client = new Client(chainInfo.host);
150
+ const client = new Client(chainHost);
126
151
  const oldResult = await client.getAccountState({ address: oldWallet.address });
127
152
  if (!oldResult.state) {
128
153
  logger.info('on-chain migration aborted because oldSk not declared on chain', { did: blocklet.meta.did });
@@ -250,7 +275,7 @@ const migrateApplicationToStructV2 = async ({ did, appSk: newAppSk, context = {}
250
275
  // add root component to blockletData
251
276
  const { source, deployedFrom } = component;
252
277
  let bundleSource;
253
- if (source === BlockletSource.store) {
278
+ if (source === BlockletSource.registry && deployedFrom && component.meta.bundleName) {
254
279
  bundleSource = {
255
280
  store: component.deployedFrom,
256
281
  name: component.meta.bundleName,
@@ -447,4 +472,4 @@ const migrateApplicationToStructV2 = async ({ did, appSk: newAppSk, context = {}
447
472
  });
448
473
  };
449
474
 
450
- module.exports = { migrateApplicationToStructV2, sortMoveListBySrc };
475
+ module.exports = { migrateApplicationToStructV2, sortMoveListBySrc, getChainHost };
@@ -13,6 +13,7 @@ const {
13
13
  checkStructVersion,
14
14
  checkVersionCompatibility,
15
15
  validateBlocklet,
16
+ getFixedBundleSource,
16
17
  } = require('../../../util/blocklet');
17
18
 
18
19
  const check = async ({ did, states }) => {
@@ -24,7 +25,10 @@ const check = async ({ did, states }) => {
24
25
  const newChildren = [];
25
26
 
26
27
  for (const child of newBlocklet.children || []) {
27
- if (child.bundleSource) {
28
+ // There may be dirty data without bundleSource but with source and deployedFrom
29
+ const bundleSource = getFixedBundleSource(child);
30
+
31
+ if (bundleSource) {
28
32
  const {
29
33
  staticComponents: [newChild],
30
34
  dynamicComponents,
@@ -32,7 +36,7 @@ const check = async ({ did, states }) => {
32
36
  meta: {
33
37
  staticComponents: [
34
38
  {
35
- source: child.bundleSource,
39
+ source: bundleSource,
36
40
  name: child.meta.name,
37
41
  title: child.meta.title,
38
42
  mountPoint: child.mountPoint,
@@ -1,6 +1,23 @@
1
+ /**
2
+ * @typedef {{
3
+ * appDid: string
4
+ * event: import('events').EventEmitter,
5
+ * }} BaseBackupInput
6
+ *
7
+ * @typedef {{
8
+ * encrypt: (v: string) => string,
9
+ * decrypt: (v: string) => string,
10
+ * }} BaseSecurityContext
11
+ */
12
+
13
+ const { Hasher } = require('@ocap/mcrypto');
14
+ const { toBuffer } = require('@ocap/util');
15
+ const getBlockletInfo = require('@blocklet/meta/lib/info');
16
+ const security = require('@abtnode/util/lib/security');
17
+
1
18
  class BaseBackup {
2
19
  /**
3
- * @type {import('./spaces').SpaceBackupInput}
20
+ * @type {BaseBackupInput}
4
21
  * @memberof BaseBackup
5
22
  */
6
23
  input;
@@ -21,8 +38,8 @@ class BaseBackup {
21
38
 
22
39
  /**
23
40
  *
24
- * @description spaces 的 endpoint
25
- * @type {import('./spaces').SecurityContext}
41
+ * @description 安全相关的上下文
42
+ * @type {BaseSecurityContext}
26
43
  * @memberof BaseBackup
27
44
  */
28
45
  securityContext;
@@ -41,20 +58,57 @@ class BaseBackup {
41
58
 
42
59
  /**
43
60
  *
44
- *
45
- * @param {import('./spaces').SpacesBackup} spacesBackup
61
+ * @param {BaseBackup} backup
46
62
  * @memberof BaseBackup
47
63
  */
48
- ensureParams(spacesBackup) {
49
- this.blocklet = spacesBackup.blocklet;
50
- this.serverDir = spacesBackup.serverDir;
51
- this.backupDir = spacesBackup.backupDir;
52
- this.securityContext = spacesBackup.securityContext;
64
+ ensureParams(backup) {
65
+ this.blocklet = backup.blocklet;
66
+ this.serverDir = backup.serverDir;
67
+ this.backupDir = backup.backupDir;
68
+ this.securityContext = backup.securityContext;
53
69
  }
54
70
 
55
71
  async export() {
56
72
  throw new Error('not implemented');
57
73
  }
74
+
75
+ async _getSecurityContext(states) {
76
+ const blocklet = await states.blocklet.getBlocklet(this.input.appDid);
77
+ const nodeInfo = await states.node.read();
78
+
79
+ const { wallet } = getBlockletInfo(blocklet, nodeInfo.sk);
80
+
81
+ const { secretKey, address } = wallet; // we encrypt using latest wallet, not the permanent wallet
82
+ const password = toBuffer(Hasher.SHA3.hash256(Buffer.concat([secretKey, address].map(toBuffer))));
83
+ const encrypt = (v) => security.encrypt(v, address, password);
84
+ const decrypt = (v) => security.decrypt(v, address, password);
85
+
86
+ return {
87
+ signer: wallet,
88
+ delegation: '',
89
+ encrypt,
90
+ decrypt,
91
+ };
92
+ }
93
+
94
+ /**
95
+ *
96
+ * @param {BaseBackup} dataBackup
97
+ * @param {Array<BaseBackup>} storages
98
+ * @memberof BaseBackup
99
+ */
100
+ async _exportData(dataBackup, storages) {
101
+ // @note: dataBackup 需要先于 blockletBackup 执行,并且 blockletBackup 与其他 backup的执行可以是无序的
102
+ dataBackup.ensureParams(this);
103
+ await dataBackup.export();
104
+
105
+ await Promise.all(
106
+ storages.map((storage) => {
107
+ storage.ensureParams(this);
108
+ return storage.export();
109
+ })
110
+ );
111
+ }
58
112
  }
59
113
 
60
114
  module.exports = {
@@ -1,6 +1,12 @@
1
- const { removeSync, outputJsonSync } = require('fs-extra');
1
+ const { removeSync, outputJsonSync, createWriteStream, createReadStream } = require('fs-extra');
2
2
  const { cloneDeep } = require('lodash');
3
- const { join } = require('path');
3
+ const { join, basename } = require('path');
4
+ const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
5
+ const isEmpty = require('lodash/isEmpty');
6
+ const streamToPromise = require('stream-to-promise');
7
+ const axios = require('@abtnode/util/lib/axios');
8
+ const isUrl = require('is-url');
9
+ const { getLogoUrl } = require('@abtnode/util/lib/logo');
4
10
  const { BaseBackup } = require('./base');
5
11
 
6
12
  class BlockletBackup extends BaseBackup {
@@ -8,6 +14,10 @@ class BlockletBackup extends BaseBackup {
8
14
 
9
15
  async export() {
10
16
  const blocklet = await this.cleanData();
17
+
18
+ const targetLogoPath = await this.writeLogoFile();
19
+ blocklet.meta.appLogo = basename(targetLogoPath);
20
+
11
21
  removeSync(join(this.backupDir, this.filename));
12
22
  outputJsonSync(join(this.backupDir, this.filename), blocklet);
13
23
  }
@@ -56,7 +66,7 @@ class BlockletBackup extends BaseBackup {
56
66
  * @memberof BlockletExtrasBackup
57
67
  */
58
68
  encrypt(info) {
59
- if (Array.isArray(info.migratedFrom)) {
69
+ if (Array.isArray(info?.migratedFrom)) {
60
70
  info.migratedFrom = info.migratedFrom.map((x) => {
61
71
  x.appSk = this.securityContext.encrypt(x.appSk);
62
72
  return x;
@@ -65,6 +75,57 @@ class BlockletBackup extends BaseBackup {
65
75
 
66
76
  return info;
67
77
  }
78
+
79
+ /**
80
+ *
81
+ *
82
+ * @param {string} target
83
+ * @returns {Promise<string>}
84
+ * @memberof DataBackup
85
+ */
86
+ async writeLogoFile() {
87
+ const customLogoSquareUrl = this.blocklet.environments.find(
88
+ (e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_LOGO_SQUARE
89
+ )?.value;
90
+ const appDir = this.blocklet.environments.find((e) => e.key === 'BLOCKLET_APP_DIR')?.value;
91
+ const logo = this.blocklet?.meta?.logo;
92
+ const defaultLogoPath = join(this.serverDir, 'data', this.blocklet.meta.name, 'logo.svg');
93
+
94
+ const logoUrl = await getLogoUrl({
95
+ customLogoSquareUrl,
96
+ appDir,
97
+ logo,
98
+ defaultLogoPath,
99
+ });
100
+
101
+ const logoStream = await this.getLogoStream(logoUrl);
102
+ const targetLogoPath = join(this.backupDir, 'data', basename(logoUrl));
103
+ await streamToPromise(logoStream.pipe(createWriteStream(targetLogoPath)));
104
+
105
+ return targetLogoPath;
106
+ }
107
+
108
+ /**
109
+ *
110
+ *
111
+ * @param {string} logoUrl
112
+ * @returns {Promise<NodeJS.ReadStream>}
113
+ * @memberof DataBackup
114
+ */
115
+ async getLogoStream(logoUrl) {
116
+ if (isEmpty(logoUrl)) {
117
+ throw new Error(`logoUrl(${logoUrl}) cannot be empty`);
118
+ }
119
+
120
+ if (isUrl(logoUrl)) {
121
+ const res = await axios.get(logoUrl, {
122
+ responseType: 'stream',
123
+ });
124
+ return res.data;
125
+ }
126
+
127
+ return createReadStream(logoUrl);
128
+ }
68
129
  }
69
130
 
70
131
  module.exports = { BlockletBackup };
@@ -0,0 +1,132 @@
1
+ const fs = require('fs-extra');
2
+ const { isValid } = require('@arcblock/did');
3
+ const { ensureDirSync } = require('fs-extra');
4
+ const { isEmpty } = require('lodash');
5
+ const { join } = require('path');
6
+ const { getAppName } = require('@blocklet/meta/lib/util');
7
+
8
+ const states = require('../../../states');
9
+ const { getBackupDirs } = require('../utils/disk');
10
+ const { BaseBackup } = require('./base');
11
+ const { AuditLogBackup } = require('./audit-log');
12
+ const { BlockletBackup } = require('./blocklet');
13
+ const { BlockletExtrasBackup } = require('./blocklet-extras');
14
+ const { BlockletsBackup } = require('./blocklets');
15
+ const { DataBackup } = require('./data');
16
+ const { RoutingRuleBackup } = require('./routing-rule');
17
+
18
+ class DiskBackup extends BaseBackup {
19
+ /**
20
+ *
21
+ * @type {import('./base').BaseBackupInput}
22
+ * @memberof DiskBackup
23
+ */
24
+ input;
25
+
26
+ /**
27
+ * @description blocklet state 对象
28
+ * @type {import('@abtnode/client').BlockletState}
29
+ * @memberof DiskBackup
30
+ */
31
+ blocklet;
32
+
33
+ /**
34
+ * @type {string}
35
+ * @memberof DiskBackup
36
+ */
37
+ backupDir;
38
+
39
+ /**
40
+ *
41
+ * @description server 的数据目录
42
+ * @type {string}
43
+ * @memberof DiskBackup
44
+ */
45
+ serverDir;
46
+
47
+ /**
48
+ *
49
+ * @type {import('./base').BaseSecurityContext}
50
+ * @memberof DiskBackup
51
+ */
52
+ securityContext;
53
+
54
+ storages;
55
+
56
+ dataBackup;
57
+
58
+ /**
59
+ *
60
+ * @param {import('./base').BaseBackupInput} input
61
+ * @memberof DiskBackup
62
+ */
63
+ constructor(input) {
64
+ super(input);
65
+ this.verify(input);
66
+ this.input = input;
67
+ this.storages = [
68
+ new AuditLogBackup(this.input),
69
+ new BlockletBackup(this.input),
70
+ new BlockletsBackup(this.input),
71
+ new BlockletExtrasBackup(this.input),
72
+ new RoutingRuleBackup(this.input),
73
+ ];
74
+ this.dataBackup = new DataBackup(this.input);
75
+ }
76
+
77
+ /**
78
+ * @param {import('./base').BaseBackupInput} input
79
+ * @returns {void}
80
+ * @memberof DiskBackup
81
+ */
82
+ verify(input) {
83
+ if (isEmpty(input?.appDid) || !isValid(input?.appDid)) {
84
+ throw new Error(`input.appDid(${input?.appDid}) is not a valid did`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ *
90
+ * @returns {Promise<void>}
91
+ * @memberof DiskBackup
92
+ */
93
+ async backup() {
94
+ await this.initialize();
95
+ await this.addMeta();
96
+ await this.export();
97
+ }
98
+
99
+ async initialize() {
100
+ this.blocklet = await states.blocklet.getBlocklet(this.input.appDid);
101
+ if (isEmpty(this.blocklet)) {
102
+ throw new Error('blocklet cannot be empty');
103
+ }
104
+
105
+ this.serverDir = process.env.ABT_NODE_DATA_DIR;
106
+ const { baseBackupDir, backupDir } = getBackupDirs(this.serverDir, this.blocklet.appDid);
107
+ this.baseBackupDir = baseBackupDir;
108
+ this.backupDir = backupDir;
109
+ ensureDirSync(this.backupDir);
110
+
111
+ this.securityContext = await this._getSecurityContext(states);
112
+ }
113
+
114
+ async export() {
115
+ return this._exportData(this.dataBackup, this.storages);
116
+ }
117
+
118
+ async addMeta() {
119
+ const meta = {
120
+ appDid: this.blocklet.appDid,
121
+ appPid: this.blocklet.appPid,
122
+ name: getAppName(this.blocklet),
123
+ createdAt: Date.now(),
124
+ };
125
+
126
+ await fs.writeJSON(join(this.baseBackupDir, 'meta.json'), meta);
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ DiskBackup,
132
+ };
@@ -20,15 +20,12 @@ const { SpaceClient, BackupBlockletCommand } = require('@did-space/client');
20
20
  const { ensureDirSync } = require('fs-extra');
21
21
  const { isEmpty } = require('lodash');
22
22
  const { join, basename } = require('path');
23
- const { Hasher } = require('@ocap/mcrypto');
24
- const { toBuffer } = require('@ocap/util');
25
23
  const { getAppName, getAppDescription } = require('@blocklet/meta/lib/util');
26
- const getBlockletInfo = require('@blocklet/meta/lib/info');
27
- const security = require('@abtnode/util/lib/security');
28
24
 
29
25
  const logger = require('@abtnode/logger')('@abtnode/core:storage:backup');
30
26
 
31
27
  const states = require('../../../states');
28
+ const { BaseBackup } = require('./base');
32
29
  const { AuditLogBackup } = require('./audit-log');
33
30
  const { BlockletBackup } = require('./blocklet');
34
31
  const { BlockletExtrasBackup } = require('./blocklet-extras');
@@ -36,7 +33,7 @@ const { BlockletsBackup } = require('./blocklets');
36
33
  const { DataBackup } = require('./data');
37
34
  const { RoutingRuleBackup } = require('./routing-rule');
38
35
 
39
- class SpacesBackup {
36
+ class SpacesBackup extends BaseBackup {
40
37
  /**
41
38
  *
42
39
  * @type {SpaceBackupInput}
@@ -84,12 +81,15 @@ class SpacesBackup {
84
81
 
85
82
  storages;
86
83
 
84
+ dataBackup;
85
+
87
86
  /**
88
87
  *
89
88
  * @param {SpaceBackupInput} input
90
89
  * @memberof SpacesBackup
91
90
  */
92
91
  constructor(input) {
92
+ super(input);
93
93
  this.verify(input);
94
94
  this.input = input;
95
95
  this.storages = [
@@ -98,8 +98,8 @@ class SpacesBackup {
98
98
  new BlockletsBackup(this.input),
99
99
  new BlockletExtrasBackup(this.input),
100
100
  new RoutingRuleBackup(this.input),
101
- new DataBackup(this.input),
102
101
  ];
102
+ this.dataBackup = new DataBackup(this.input);
103
103
  }
104
104
 
105
105
  /**
@@ -141,7 +141,7 @@ class SpacesBackup {
141
141
  throw new Error('spaceEndpoint cannot be empty');
142
142
  }
143
143
 
144
- this.securityContext = await this.getSecurityContext();
144
+ this.securityContext = await this._getSecurityContext(states);
145
145
  }
146
146
 
147
147
  async export() {
@@ -151,12 +151,9 @@ class SpacesBackup {
151
151
  progress: 15,
152
152
  completed: false,
153
153
  });
154
- await Promise.all(
155
- this.storages.map((storage) => {
156
- storage.ensureParams(this);
157
- return storage.export();
158
- })
159
- );
154
+
155
+ await this._exportData(this.dataBackup, this.storages);
156
+
160
157
  this.input.event.emit(BlockletEvents.backupProgress, {
161
158
  appDid: this.input.appDid,
162
159
  message: 'Data ready, start backup...',
@@ -201,7 +198,7 @@ class SpacesBackup {
201
198
  const percent = (data.completed * 100) / data.total;
202
199
  this.input.event.emit(BlockletEvents.backupProgress, {
203
200
  appDid: this.input.appDid,
204
- message: `Uploaded file ${basename(data.key)} (${data.completed}/${data.total})`,
201
+ message: `Uploading file ${basename(data.key)} (${data.completed}/${data.total})`,
205
202
  // 0.8 是因为上传文件到 spaces 占进度的 80%,+ 20 是因为需要累加之前的进度
206
203
  progress: +Math.ceil(percent * 0.8).toFixed(2) + 20,
207
204
  completed: false,
@@ -214,25 +211,6 @@ class SpacesBackup {
214
211
  throw new Error(`Sync to spaces encountered error: ${message}`);
215
212
  }
216
213
  }
217
-
218
- async getSecurityContext() {
219
- const blocklet = await states.blocklet.getBlocklet(this.input.appDid);
220
- const nodeInfo = await states.node.read();
221
-
222
- const { wallet } = getBlockletInfo(blocklet, nodeInfo.sk);
223
-
224
- const { secretKey, address } = wallet; // we encrypt using latest wallet, not the permanent wallet
225
- const password = toBuffer(Hasher.SHA3.hash256(Buffer.concat([secretKey, address].map(toBuffer))));
226
- const encrypt = (v) => security.encrypt(v, address, password);
227
- const decrypt = (v) => security.decrypt(v, address, password);
228
-
229
- return {
230
- signer: wallet,
231
- delegation: '',
232
- encrypt,
233
- decrypt,
234
- };
235
- }
236
214
  }
237
215
 
238
216
  module.exports = {
@@ -1,13 +1,20 @@
1
+ /**
2
+ * @typedef {{
3
+ * appDid: string; // --> appDid
4
+ * password: Buffer; // derived from (appSk, appDid)
5
+ * event: import('events').EventEmitter,
6
+ * }} BaseRestoreInput
7
+ */
8
+
1
9
  class BaseRestore {
2
10
  /**
3
11
  *
4
- * @type {import('./spaces').SpaceRestoreInput}
12
+ * @type {BaseRestoreInput}
5
13
  * @memberof BaseRestore
6
14
  */
7
15
  input;
8
16
 
9
17
  /**
10
- * @description 当前 blocklet 的数据目录
11
18
  * @type {string}
12
19
  * @memberof BaseRestore
13
20
  */
@@ -28,12 +35,12 @@ class BaseRestore {
28
35
  /**
29
36
  *
30
37
  *
31
- * @param {import('./spaces').SpacesRestore} spaces
38
+ * @param {BaseRestore} restore
32
39
  * @memberof BaseRestore
33
40
  */
34
- ensureParams(spaces) {
35
- this.restoreDir = spaces.restoreDir;
36
- this.serverDir = spaces.serverDir;
41
+ ensureParams(restore) {
42
+ this.restoreDir = restore.restoreDir;
43
+ this.serverDir = restore.serverDir;
37
44
  }
38
45
 
39
46
  // eslint-disable-next-line
@@ -9,8 +9,7 @@ class BlockletExtrasRestore extends BaseRestore {
9
9
  filename = 'blocklet-extras.json';
10
10
 
11
11
  async import(params) {
12
- const extras = this.getExtras();
13
- this.cleanExtras(extras, params);
12
+ const extras = this.cleanExtras(this.getExtras(), params);
14
13
  removeSync(join(this.restoreDir, this.filename));
15
14
  outputJsonSync(join(this.restoreDir, this.filename), extras);
16
15
  }
@@ -32,9 +31,10 @@ class BlockletExtrasRestore extends BaseRestore {
32
31
  *
33
32
  * @description 清理数据并加密
34
33
  * @param {import('@abtnode/client').BlockletState} raw
34
+ * @returns {import('@abtnode/client').BlockletState}
35
35
  * @memberof BlockletExtrasRestore
36
36
  */
37
- async cleanExtras(raw, params) {
37
+ cleanExtras(raw, params) {
38
38
  const blockletExtra = cloneDeep(raw);
39
39
 
40
40
  const queue = [blockletExtra];
@@ -48,6 +48,8 @@ class BlockletExtrasRestore extends BaseRestore {
48
48
  queue.push(...current.children);
49
49
  }
50
50
  }
51
+
52
+ return blockletExtra;
51
53
  }
52
54
 
53
55
  /**
@@ -33,7 +33,7 @@ class BlockletRestore extends BaseRestore {
33
33
  */
34
34
  decrypt(blocklet, params) {
35
35
  const { password } = this.input;
36
- if (Array.isArray(blocklet.migratedFrom)) {
36
+ if (Array.isArray(blocklet?.migratedFrom)) {
37
37
  blocklet.migratedFrom = blocklet.migratedFrom.map((x) => {
38
38
  x.appSk = security.decrypt(x.appSk, params.salt, password);
39
39
  return x;