@abtnode/core 1.8.67-beta-794a8082 → 1.8.68-beta-500af7e5

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.
@@ -2,6 +2,7 @@ const { EventEmitter } = require('events');
2
2
  const fs = require('fs-extra');
3
3
  const { fileURLToPath } = require('url');
4
4
  const path = require('path');
5
+ const pick = require('lodash/pick');
5
6
  const cloneDeep = require('lodash/cloneDeep');
6
7
  const { toBase58 } = require('@ocap/util');
7
8
 
@@ -161,7 +162,11 @@ class BundleDownloader extends EventEmitter {
161
162
  }
162
163
  ctrlStore[rootDid].set(did, cancelCtrl);
163
164
 
164
- const headers = context.headers ? cloneDeep(context.headers) : {};
165
+ const headers = pick(context.headers ? cloneDeep(context.headers) : {}, [
166
+ 'x-server-did',
167
+ 'x-server-public-key',
168
+ 'x-server-signature',
169
+ ]);
165
170
  const exist = (context.downloadTokenList || []).find((x) => x.did === did);
166
171
  if (exist) {
167
172
  headers['x-download-token'] = exist.token;
@@ -4,6 +4,7 @@ const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const flat = require('flat');
6
6
  const get = require('lodash/get');
7
+ const merge = require('lodash/merge');
7
8
  const pick = require('lodash/pick');
8
9
  const cloneDeep = require('lodash/cloneDeep');
9
10
  const semver = require('semver');
@@ -111,6 +112,8 @@ const {
111
112
  checkDuplicateMountPoint,
112
113
  validateStore,
113
114
  validateInServerless,
115
+ isRotatingAppSk,
116
+ isRotatingAppDid,
114
117
  } = require('../../util/blocklet');
115
118
  const StoreUtil = require('../../util/store');
116
119
  const states = require('../../states');
@@ -193,13 +196,14 @@ class BlockletManager extends BaseBlockletManager {
193
196
  /**
194
197
  * @param {*} dataDirs generate by ../../util:getDataDirs
195
198
  */
196
- constructor({ dataDirs, startQueue, installQueue, daemon = false, teamManager }) {
199
+ constructor({ dataDirs, startQueue, installQueue, backupQueue, daemon = false, teamManager }) {
197
200
  super();
198
201
 
199
202
  this.dataDirs = dataDirs;
200
203
  this.installDir = dataDirs.blocklets;
201
204
  this.startQueue = startQueue;
202
205
  this.installQueue = installQueue;
206
+ this.backupQueue = backupQueue;
203
207
  this.teamManager = teamManager;
204
208
 
205
209
  // cached installed blocklets for performance
@@ -266,6 +270,7 @@ class BlockletManager extends BaseBlockletManager {
266
270
 
267
271
  const info = await states.node.read();
268
272
 
273
+ // Note: if you added new header here, please change core/state/lib/blocklet/downloader/bundle-downloader.js to use that header
269
274
  context.headers = Object.assign(context?.headers || {}, {
270
275
  'x-server-did': info.did,
271
276
  'x-server-public-key': info.pk,
@@ -605,32 +610,67 @@ class BlockletManager extends BaseBlockletManager {
605
610
  }
606
611
 
607
612
  /**
608
- *
609
- *
610
- * @param {import('@abtnode/client').BackupToSpacesParams} input
613
+ * FIXME: @wangshijun create audit log for this
614
+ * @param {import('@abtnode/client').RequestBackupToSpacesInput} input
611
615
  * @memberof BlockletManager
612
616
  */
613
- async backupToSpaces(input) {
614
- const spacesBackup = new SpacesBackup(input);
617
+ // eslint-disable-next-line no-unused-vars
618
+ async backupToSpaces({ did }, context) {
619
+ // add to queue
620
+ // const ticket = this.backupQueue.push({
621
+ // entity: 'blocklet',
622
+ // action: 'backup-to-space',
623
+ // did,
624
+ // context,
625
+ // });
626
+
627
+ // ticket.on('failed', async (err) => {
628
+ // logger.error('backup failed', { entity: 'blocklet', did, error: err });
629
+ // this.emit('blocklet.backup.failed', { did, err });
630
+ // this._createNotification(did, {
631
+ // title: 'Blocklet Backup Failed',
632
+ // description: `Blocklet backup failed with error: ${err.message || 'queue exception'}`,
633
+ // entityType: 'blocklet',
634
+ // entityId: did,
635
+ // severity: 'error',
636
+ // });
637
+ // });
638
+
639
+ const userDid = context.user.did;
640
+ const { referrer } = context;
641
+
642
+ // FIXME: @yejianchao did should be renamed to appDid
643
+ const spacesBackup = new SpacesBackup({ did, event: this, userDid, referrer });
644
+ this.emit(BlockletEvents.backupProgress, { did, message: 'Start backup...', progress: 10 });
615
645
  await spacesBackup.backup();
646
+ this.emit(BlockletEvents.backupProgress, { did, completed: true, progress: 100 });
616
647
  }
617
648
 
618
649
  /**
619
- *
620
- *
650
+ * FIXME: @linchen support cancel
651
+ * FIXME: @wangshijun create audit log for this
621
652
  * @param {import('@abtnode/client').RequestRestoreFromSpacesInput} input
622
653
  * @memberof BlockletManager
623
654
  */
624
- async restoreFromSpaces(input) {
625
- const spacesRestore = new SpacesRestore(input);
626
- await spacesRestore.restore();
655
+ // eslint-disable-next-line no-unused-vars
656
+ async restoreFromSpaces(input, context) {
657
+ // FIXME: @yejianchao did should be renamed to appDid
658
+ this.emit(BlockletEvents.restoreProgress, { did: input.appDid, message: 'Start restore...' });
659
+
660
+ const userDid = context.user.did;
661
+ const { referrer } = context;
662
+
663
+ const spacesRestore = new SpacesRestore({ ...input, event: this, userDid, referrer });
664
+ const params = await spacesRestore.restore();
627
665
 
628
- // FIXME: 需要改成队列执行,本次失败,下次还需要重试,页面刷新后也能知道最新的状态
666
+ this.emit(BlockletEvents.restoreProgress, { did: input.appDid, message: 'Installing blocklet...' });
629
667
  await this._installFromBackup({
630
668
  url: `file://${spacesRestore.blockletRestoreDir}`,
631
- appSk: spacesRestore.blockletWallet.secretKey,
632
669
  moveDir: true,
670
+ ...merge(...params),
633
671
  });
672
+
673
+ this.emit(BlockletEvents.restoreProgress, { did: input.appDid, completed: true });
634
674
  }
635
675
 
636
676
  async restart({ did }, context) {
@@ -1000,6 +1040,7 @@ class BlockletManager extends BaseBlockletManager {
1000
1040
  return blocklets;
1001
1041
  }
1002
1042
 
1043
+ // CAUTION: this method currently only support config by blocklet.meta.did
1003
1044
  // eslint-disable-next-line no-unused-vars
1004
1045
  async config({ did, configs: newConfigs, skipHook, skipDidDocument }, context) {
1005
1046
  if (!Array.isArray(newConfigs)) {
@@ -1031,7 +1072,7 @@ class BlockletManager extends BaseBlockletManager {
1031
1072
  throw new Error(`Cannot set ${x.key} to child blocklet`);
1032
1073
  }
1033
1074
 
1034
- await validateAppConfig(x, rootDid, states);
1075
+ await validateAppConfig(x, states);
1035
1076
  } else if (!BLOCKLET_CONFIGURABLE_KEY[x.key] && !isPreferenceKey(x)) {
1036
1077
  if (!(blocklet.meta.environments || []).some((y) => y.name === x.key)) {
1037
1078
  // forbid unknown format key
@@ -1054,14 +1095,26 @@ class BlockletManager extends BaseBlockletManager {
1054
1095
  });
1055
1096
  }
1056
1097
 
1098
+ const willAppSkChange = isRotatingAppSk(newConfigs, blocklet.configs, blocklet.externalSk);
1099
+ const willAppDidChange = isRotatingAppDid(newConfigs, blocklet.configs, blocklet.externalSk);
1100
+
1057
1101
  // update db
1058
1102
  await states.blockletExtras.setConfigs(dids, newConfigs);
1059
1103
 
1060
- const isSkOrWalletTypeChanged = newConfigs.find((item) =>
1061
- [BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK, BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE].includes(item.key)
1062
- );
1104
+ if (willAppSkChange) {
1105
+ const info = await states.node.read();
1106
+ const { wallet } = getBlockletInfo(blocklet, info.sk);
1107
+ const migratedFrom = blocklet.migratedFrom || [];
1108
+ await states.blocklet.updateBlocklet(rootDid, {
1109
+ migratedFrom: [
1110
+ ...migratedFrom,
1111
+ { appSk: wallet.secretKey, appDid: wallet.address, at: new Date().toISOString() },
1112
+ ],
1113
+ });
1114
+ }
1063
1115
 
1064
- if (isSkOrWalletTypeChanged && !skipDidDocument) {
1116
+ // FIXME: @zhenqiang best way to handle the did document: allow all appDids to resolve
1117
+ if (willAppDidChange && !skipDidDocument) {
1065
1118
  await this._updateDidDocument(blocklet);
1066
1119
  }
1067
1120
 
@@ -2064,6 +2117,14 @@ class BlockletManager extends BaseBlockletManager {
2064
2117
  // put BLOCKLET_APP_ID at root level for indexing
2065
2118
  blocklet.appDid = appSystemEnvironments.BLOCKLET_APP_ID;
2066
2119
 
2120
+ if (!blocklet.migratedFrom) {
2121
+ blocklet.migratedFrom = [];
2122
+ }
2123
+ // This can only be set once, can be used for indexing, will not change ever
2124
+ if (!blocklet.appPid) {
2125
+ blocklet.appPid = appSystemEnvironments.BLOCKLET_APP_PID;
2126
+ }
2127
+
2067
2128
  // update state to db
2068
2129
  return states.blocklet.updateBlocklet(did, blocklet);
2069
2130
  }
@@ -2344,7 +2405,7 @@ class BlockletManager extends BaseBlockletManager {
2344
2405
  };
2345
2406
  const meta = validateMeta(rawMeta);
2346
2407
 
2347
- await states.blocklet.addBlocklet({ meta, source: BlockletSource.custom });
2408
+ await states.blocklet.addBlocklet({ meta, source: BlockletSource.custom, externalSk: !!appSk });
2348
2409
  await this._setConfigsFromMeta(did);
2349
2410
  await this._setAppSk(did, appSk);
2350
2411
 
@@ -2478,6 +2539,7 @@ class BlockletManager extends BaseBlockletManager {
2478
2539
  source: BlockletSource.upload,
2479
2540
  deployedFrom: `Upload by ${context.user.fullName}`,
2480
2541
  children,
2542
+ externalSk: !!appSk,
2481
2543
  });
2482
2544
 
2483
2545
  const action = 'install';
@@ -2812,6 +2874,7 @@ class BlockletManager extends BaseBlockletManager {
2812
2874
  source,
2813
2875
  deployedFrom,
2814
2876
  children,
2877
+ externalSk: !!appSk,
2815
2878
  });
2816
2879
 
2817
2880
  await validateBlocklet(blocklet);
@@ -1,8 +1,9 @@
1
1
  const { removeSync, outputJsonSync, readFileSync } = require('fs-extra');
2
2
  const { isEmpty, cloneDeep } = require('lodash');
3
3
  const { join } = require('path');
4
+ const { Hasher } = require('@ocap/mcrypto');
5
+ const { toBuffer } = require('@ocap/util');
4
6
  const security = require('@abtnode/util/lib/security');
5
- const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
6
7
  const states = require('../../../states');
7
8
  const { BaseBackup } = require('./base');
8
9
 
@@ -32,7 +33,7 @@ class BlockletExtrasBackup extends BaseBackup {
32
33
  throw new Error('blockletExtra cannot be empty');
33
34
  }
34
35
 
35
- return this.cleanDate(blockletExtra);
36
+ return this.cleanData(blockletExtra);
36
37
  }
37
38
 
38
39
  /**
@@ -42,7 +43,7 @@ class BlockletExtrasBackup extends BaseBackup {
42
43
  * @return {Promise<void>}
43
44
  * @memberof BlockletExtrasBackup
44
45
  */
45
- async cleanDate(blockletExtraInput) {
46
+ async cleanData(blockletExtraInput) {
46
47
  const blockletExtra = cloneDeep(blockletExtraInput);
47
48
 
48
49
  const queue = [blockletExtra];
@@ -57,7 +58,7 @@ class BlockletExtrasBackup extends BaseBackup {
57
58
  }
58
59
 
59
60
  // 加解密
60
- this.useBlockletEncryptConfigs(currentBlockletExtra.configs);
61
+ this.encryptBlockletConfigs(currentBlockletExtra.configs);
61
62
 
62
63
  if (currentBlockletExtra?.children) {
63
64
  queue.push(...currentBlockletExtra.children);
@@ -74,33 +75,28 @@ class BlockletExtrasBackup extends BaseBackup {
74
75
  * @return {void}
75
76
  * @memberof BlockletExtrasBackup
76
77
  */
77
- useBlockletEncryptConfigs(configs) {
78
+ encryptBlockletConfigs(configs) {
78
79
  if (isEmpty(configs)) {
79
80
  return;
80
81
  }
81
82
 
83
+ const { secretKey, address } = this.blockletWallet;
84
+
82
85
  // 准备加解密所需的参数
83
86
  // @see: https://github.com/ArcBlock/blocklet-server/blob/f561ba7290285f2e23dccb6d5323eb4d43c3cc3e/core/state/lib/index.js#L59
84
- const dek = readFileSync(join(this.serverDataDir, '.sock'));
87
+ const dk = readFileSync(join(this.serverDataDir, '.sock'));
88
+ const ek = toBuffer(Hasher.SHA3.hash256(Buffer.concat([secretKey, address].map(toBuffer))));
85
89
 
86
90
  for (const config of configs) {
87
- // 置空 blocklet 的密钥,但是保留其他属性,这很重要
88
- if (config.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
89
- config.value = '';
90
- } else if (config.secure) {
91
+ if (config.secure) {
91
92
  // secure 为 true 的配置才需要被加密保存上次到 did spaces
92
-
93
93
  const encryptByServer = config.value;
94
94
  // 先从 blocklet server 解密
95
95
  // @see https://github.com/ArcBlock/blocklet-server/blob/f40338168a66893f325464cea79ae54c43f623b1/core/state/lib/blocklet/extras.js#L139
96
- const decryptByServer = security.decrypt(encryptByServer, this.blocklet.meta.did, dek);
97
- // 再用 blocklet secret 加密,然后才可以上传到 spaces
98
- const encryptByBlocklet = security.encrypt(
99
- decryptByServer,
100
- this.blockletWallet.address,
101
- Buffer.from(this.blockletWallet.secretKey)
102
- );
103
- config.value = encryptByBlocklet;
96
+ const decrypted = security.decrypt(encryptByServer, this.blocklet.meta.did, dk);
97
+ // 再用 blocklet 信息加密,然后才可以上传到 spaces
98
+ const encrypted = security.encrypt(decrypted, address, ek);
99
+ config.value = encrypted;
104
100
  }
105
101
  }
106
102
  }
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * @typedef {{
3
- * did?: string
3
+ * did: string
4
+ * event: import('events').EventEmitter,
5
+ * userDid: string,
6
+ * referrer: string,
4
7
  * }} SpaceBackupInput
5
8
  */
6
9
 
7
10
  const { isValid } = require('@arcblock/did');
8
- const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
11
+ const { BLOCKLET_CONFIGURABLE_KEY, BlockletEvents } = require('@blocklet/constant');
9
12
  const { getBlockletInfo } = require('@blocklet/meta');
10
- const { SpaceClient, SyncFolderPushCommand } = require('@did-space/client');
13
+ const { SpaceClient, BackupBlockletCommand } = require('@did-space/client');
11
14
  const { ensureDirSync } = require('fs-extra');
12
15
  const { isEmpty } = require('lodash');
13
- const { join } = require('path');
16
+ const { join, basename } = require('path');
17
+
18
+ const logger = require('@abtnode/logger')('@abtnode/core:storage:backup');
19
+
14
20
  const states = require('../../../states');
15
21
  const { AuditLogBackup } = require('./audit-log');
16
22
  const { BlockletBackup } = require('./blocklet');
@@ -131,12 +137,22 @@ class SpacesBackup {
131
137
  }
132
138
 
133
139
  async export() {
140
+ this.input.event.emit(BlockletEvents.backupProgress, {
141
+ did: this.input.did,
142
+ message: 'Preparing data for backup...',
143
+ progress: 15,
144
+ });
134
145
  await Promise.all(
135
146
  this.storages.map((storage) => {
136
147
  storage.ensureParams(this);
137
148
  return storage.export();
138
149
  })
139
150
  );
151
+ this.input.event.emit(BlockletEvents.backupProgress, {
152
+ did: this.input.did,
153
+ message: 'Data ready, start backup...',
154
+ progress: 20,
155
+ });
140
156
  }
141
157
 
142
158
  async syncToSpaces() {
@@ -146,22 +162,34 @@ class SpacesBackup {
146
162
  wallet,
147
163
  });
148
164
 
149
- // FIXME: Spaces 里面预览出 blocklet 的样式,需要规划一个好的数据结构
150
- const { errorCount } = await spaceClient.send(
151
- new SyncFolderPushCommand({
165
+ const { errorCount, message } = await spaceClient.send(
166
+ new BackupBlockletCommand({
167
+ appDid: this.blocklet.appDid,
152
168
  source: join(this.blockletBackupDir, '/'),
153
- target: join('.did-objects', this.blocklet.appDid, '/'),
154
169
  debug: true,
155
170
  concurrency: 32,
156
171
  retryCount: 10,
157
172
  filter: (object) => {
173
+ // FIXME: @yejianchao 这里需要更完整的黑名单
158
174
  return object.name !== '.DS_Store';
159
175
  },
176
+ onProgress: (data) => {
177
+ logger.info('backup progress', { appDid: this.input.did, data });
178
+ const percent = (data.completed * 100) / data.total;
179
+ this.input.event.emit(BlockletEvents.backupProgress, {
180
+ did: this.input.did,
181
+ message: `Uploaded file ${basename(data.key)} (${data.completed}/${data.total})`,
182
+ // 0.8 是因为上传文件到 spaces 占进度的 80%,+ 20 是因为需要累加之前的进度
183
+ progress: +Math.ceil(percent * 0.8).toFixed(2) + 20,
184
+ });
185
+ },
186
+ userDid: this.input.userDid,
187
+ referrer: this.input.referrer,
160
188
  })
161
189
  );
162
190
 
163
191
  if (errorCount !== 0) {
164
- throw new Error(`Sync to spaces encountered ${errorCount} error`);
192
+ throw new Error(`Sync to spaces encountered ${errorCount} error: ${message}`);
165
193
  }
166
194
  }
167
195
 
@@ -13,14 +13,6 @@ class BaseRestore {
13
13
  */
14
14
  blockletRestoreDir;
15
15
 
16
- /**
17
- *
18
- * @description spaces 的 endpoint
19
- * @type {import('@ocap/wallet').WalletObject}
20
- * @memberof BaseRestore
21
- */
22
- blockletWallet;
23
-
24
16
  /**
25
17
  *
26
18
  * @description server 的数据目录
@@ -41,13 +33,22 @@ class BaseRestore {
41
33
  */
42
34
  ensureParams(spaces) {
43
35
  this.blockletRestoreDir = spaces.blockletRestoreDir;
44
- this.blockletWallet = spaces.blockletWallet;
45
36
  this.serverDataDir = spaces.serverDataDir;
46
37
  }
47
38
 
48
39
  async import() {
49
40
  throw new Error('not implemented');
50
41
  }
42
+
43
+ /**
44
+ * Generate params for BlockletManager to install
45
+ *
46
+ * @return {object}
47
+ * @memberof BaseRestore
48
+ */
49
+ getInstallParams() {
50
+ return {};
51
+ }
51
52
  }
52
53
 
53
54
  module.exports = {
@@ -2,6 +2,7 @@ const { removeSync, outputJsonSync, readJSONSync } = require('fs-extra');
2
2
  const { cloneDeep, isArray } = require('lodash');
3
3
  const { join } = require('path');
4
4
  const security = require('@abtnode/util/lib/security');
5
+ const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
5
6
  const { BaseRestore } = require('./base');
6
7
 
7
8
  class BlockletExtrasRestore extends BaseRestore {
@@ -24,7 +25,6 @@ class BlockletExtrasRestore extends BaseRestore {
24
25
  * @type {import('@abtnode/client').BlockletState}
25
26
  */
26
27
  const blockletExtra = readJSONSync(join(this.blockletRestoreDir, this.filename));
27
-
28
28
  return this.cleanData(blockletExtra);
29
29
  }
30
30
 
@@ -32,7 +32,6 @@ class BlockletExtrasRestore extends BaseRestore {
32
32
  *
33
33
  * @description 清理数据并加密
34
34
  * @param {import('@abtnode/client').BlockletState} blockletExtraInput
35
- * @return {Promise<void>}
36
35
  * @memberof BlockletExtrasRestore
37
36
  */
38
37
  async cleanData(blockletExtraInput) {
@@ -43,7 +42,7 @@ class BlockletExtrasRestore extends BaseRestore {
43
42
  const currentBlockletExtra = queue.pop();
44
43
 
45
44
  // 加解密
46
- this.useBlockletDecryptConfigs(currentBlockletExtra.configs);
45
+ this.decrypt(currentBlockletExtra.configs);
47
46
 
48
47
  if (currentBlockletExtra?.children) {
49
48
  queue.push(...currentBlockletExtra.children);
@@ -55,31 +54,37 @@ class BlockletExtrasRestore extends BaseRestore {
55
54
 
56
55
  /**
57
56
  *
58
- * @description 清理数据并加密
57
+ * @description 解密加密的数据
59
58
  * @param {import('@abtnode/client').ConfigEntry[]} configs
60
59
  * @return {void}
61
60
  * @memberof BlockletExtrasRestore
62
61
  */
63
- useBlockletDecryptConfigs(configs) {
62
+ decrypt(configs) {
64
63
  if (!configs || !isArray(configs)) {
65
64
  return;
66
65
  }
67
66
 
67
+ const { appDid, password } = this.input;
68
68
  for (const config of configs) {
69
- // secure 为 true 的配置才需要被加密保存上次到 did spaces
70
69
  if (config.secure) {
71
- const encryptByBlocklet = config.value;
72
-
73
- // 再用 blocklet secret 加密,然后才可以上传到 spaces
74
- const decryptByBlocklet = security.decrypt(
75
- encryptByBlocklet,
76
- this.blockletWallet.address,
77
- Buffer.from(this.blockletWallet.secretKey)
78
- );
79
-
80
- config.value = decryptByBlocklet;
70
+ const encrypted = config.value;
71
+ const decrypted = security.decrypt(encrypted, appDid, Buffer.from(password));
72
+ config.value = decrypted;
81
73
  }
82
74
  }
75
+
76
+ const config = configs.find((x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK);
77
+ if (!config) {
78
+ throw new Error(`Invalid blocklet backup file, no ${BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK} found`);
79
+ }
80
+
81
+ this.appSk = config.value;
82
+ }
83
+
84
+ getInstallParams() {
85
+ return {
86
+ appSk: this.appSk,
87
+ };
83
88
  }
84
89
  }
85
90
 
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * @typedef {{
3
+ * appDid: string;
3
4
  * endpoint: string;
4
- * appSk: string;
5
+ * password: Buffer;
6
+ * delegation: string;
7
+ * wallet: import('@ocap/wallet').WalletObject;
8
+ * event: import('events').EventEmitter,
9
+ * userDid: string,
10
+ * referrer: string,
5
11
  * }} SpaceRestoreInput
6
12
  */
7
13
 
8
- const { SpaceClient, SyncFolderPullCommand } = require('@did-space/client');
9
- const { types } = require('@ocap/mcrypto');
10
- const { fromSecretKey, WalletType } = require('@ocap/wallet');
11
- const { ensureDirSync, existsSync, rmdirSync } = require('fs-extra');
12
- const { join } = require('path');
13
14
  const validUrl = require('valid-url');
15
+ const { BlockletEvents } = require('@blocklet/constant');
16
+ const { SpaceClient, RestoreBlockletCommand } = require('@did-space/client');
17
+ const { ensureDirSync, existsSync, rmdirSync } = require('fs-extra');
18
+ const { join, basename } = require('path');
19
+
20
+ const logger = require('@abtnode/logger')('@abtnode/core:storage:restore');
21
+
14
22
  const { BlockletExtrasRestore } = require('./blocklet-extras');
15
23
  const { BlockletsRestore } = require('./blocklets');
16
24
 
@@ -37,14 +45,6 @@ class SpacesRestore {
37
45
  */
38
46
  serverDataDir;
39
47
 
40
- /**
41
- *
42
- * @description spaces 的 endpoint
43
- * @type {import('@ocap/wallet').WalletObject}
44
- * @memberof SpacesRestore
45
- */
46
- blockletWallet;
47
-
48
48
  storages;
49
49
 
50
50
  /**
@@ -72,68 +72,75 @@ class SpacesRestore {
72
72
 
73
73
  async initialize() {
74
74
  this.serverDataDir = process.env.ABT_NODE_DATA_DIR;
75
- this.blockletWallet = await this.getBlockletWallet();
76
75
 
77
- if (!this.input.endpoint.includes(this.blockletWallet.address)) {
78
- throw new Error(`endpoint and blocklet.appDid(${this.blockletWallet.address}) do not match`);
76
+ if (!this.input.endpoint.includes(this.input.appDid)) {
77
+ throw new Error(`endpoint and blocklet.appDid(${this.input.appDid}) do not match`);
79
78
  }
80
79
 
81
- this.blockletRestoreDir = join(process.env.ABT_NODE_DATA_DIR, 'tmp/restore', this.blockletWallet.address);
80
+ this.blockletRestoreDir = join(process.env.ABT_NODE_DATA_DIR, 'tmp/restore', this.input.appDid);
82
81
  if (existsSync(this.blockletRestoreDir)) {
83
82
  rmdirSync(this.blockletRestoreDir, { recursive: true });
84
83
  }
85
84
  ensureDirSync(this.blockletRestoreDir);
86
85
  }
87
86
 
88
- /**
89
- *
90
- * @returns {Promise<void>}
91
- * @memberof SpacesRestore
92
- */
93
87
  async restore() {
94
88
  await this.initialize();
95
89
  await this.syncFromSpaces();
96
90
  await this.import();
97
- }
98
-
99
- async getBlockletWallet() {
100
- // @FIXME: blocklet 钱包类型如何得知呢?
101
- const wallet = fromSecretKey(this.input.appSk, WalletType({ role: types.RoleType.ROLE_APPLICATION }));
102
91
 
103
- return wallet;
92
+ return this.storages.map((x) => x.getInstallParams());
104
93
  }
105
94
 
106
95
  async syncFromSpaces() {
107
- const { endpoint } = this.input;
108
- const wallet = await this.getBlockletWallet();
96
+ const { endpoint, wallet, delegation } = this.input;
109
97
 
110
98
  const spaceClient = new SpaceClient({
111
99
  endpoint,
100
+ delegation,
112
101
  wallet,
113
102
  });
114
103
 
115
- const { errorCount } = await spaceClient.send(
116
- new SyncFolderPullCommand({
117
- source: join('.did-objects', this.blockletWallet.address, '/'),
104
+ const { errorCount, message } = await spaceClient.send(
105
+ new RestoreBlockletCommand({
106
+ appDid: this.input.appDid,
118
107
  target: join(this.blockletRestoreDir, '/'),
119
108
  debug: true,
120
109
  concurrency: 32,
121
110
  retryCount: 10,
111
+ onProgress: (data) => {
112
+ logger.info('restore progress', { appDid: this.input.appDid, data });
113
+ this.input.event.emit(BlockletEvents.restoreProgress, {
114
+ did: this.input.appDid,
115
+ message: `Downloaded file ${basename(data.key)} (${data.completed}/${data.total})`,
116
+ });
117
+ },
118
+
119
+ userDid: this.input.userDid,
120
+ referrer: this.input.referrer,
122
121
  })
123
122
  );
124
123
 
125
124
  if (errorCount !== 0) {
126
- throw new Error(`Sync from spaces encountered ${errorCount} error`);
125
+ throw new Error(`Sync from spaces encountered ${errorCount} error: ${message}`);
127
126
  }
128
127
  }
129
128
 
130
129
  async import() {
130
+ this.input.event.emit(BlockletEvents.restoreProgress, {
131
+ did: this.input.appDid,
132
+ message: 'Preparing to import data...',
133
+ });
131
134
  await Promise.all(
132
135
  this.storages.map((storage) => {
133
136
  storage.ensureParams(this);
134
137
  return storage.import();
135
138
  })
136
139
  );
140
+ this.input.event.emit(BlockletEvents.restoreProgress, {
141
+ did: this.input.appDid,
142
+ message: 'Importing data successfully...',
143
+ });
137
144
  }
138
145
  }
139
146
 
package/lib/event.js CHANGED
@@ -169,6 +169,12 @@ module.exports = ({
169
169
  });
170
170
  };
171
171
 
172
+ /**
173
+ *
174
+ * @description 事件必须注册在这里才能被发布出去
175
+ * @param {string} eventName
176
+ * @param {any} payload
177
+ */
172
178
  const handleBlockletEvent = async (eventName, payload) => {
173
179
  const blocklet = payload.blocklet || payload;
174
180
 
@@ -273,6 +279,13 @@ module.exports = ({
273
279
  }
274
280
  };
275
281
 
282
+ /**
283
+ *
284
+ *
285
+ * @param {*} subject
286
+ * @param {string} event
287
+ * @param {(event: string, data: any) => Promise<void> | void} handler
288
+ */
276
289
  const listen = (subject, event, handler) => subject.on(event, (data) => handler(event, data));
277
290
 
278
291
  [
@@ -291,6 +304,9 @@ module.exports = ({
291
304
  BlockletEvents.startFailed,
292
305
  BlockletEvents.stopped,
293
306
  BlockletEvents.appDidChanged,
307
+
308
+ BlockletEvents.backupProgress,
309
+ BlockletEvents.restoreProgress,
294
310
  ].forEach((eventName) => {
295
311
  listen(blockletManager, eventName, handleBlockletEvent);
296
312
  });
package/lib/index.js CHANGED
@@ -26,7 +26,7 @@ const Maintain = require('./util/maintain');
26
26
  const resetNode = require('./util/reset-node');
27
27
  const DiskMonitor = require('./util/disk-monitor');
28
28
  const StoreUtil = require('./util/store');
29
- const createQueue = require('./queue');
29
+ const createQueue = require('./util/queue');
30
30
  const createEvents = require('./event');
31
31
  const pm2Events = require('./blocklet/manager/pm2-events');
32
32
  const { createStateReadyQueue, createStateReadyHandler } = require('./util/ready');
@@ -99,7 +99,25 @@ function ABTNode(options) {
99
99
  concurrency,
100
100
  maxRetries: 3,
101
101
  retryDelay: 5000, // retry after 5 seconds
102
- maxTimeout: 60 * 1000 * 15, // throw timeout error after 5 minutes
102
+ maxTimeout: 60 * 1000 * 15, // throw timeout error after 15 minutes
103
+ id: (job) => (job ? md5(`${job.entity}-${job.action}-${job.id}`) : ''),
104
+ },
105
+ });
106
+
107
+ const backupQueue = createQueue({
108
+ daemon: options.daemon,
109
+ name: 'backup_queue',
110
+ dataDir: dataDirs.core,
111
+ onJob: async (job) => {
112
+ if (typeof blockletManager.onJob === 'function') {
113
+ await blockletManager.onJob(job);
114
+ }
115
+ },
116
+ options: {
117
+ concurrency,
118
+ maxRetries: 3,
119
+ retryDelay: 10000, // retry after 10 seconds
120
+ maxTimeout: 60 * 1000 * 30, // throw timeout error after 30 minutes
103
121
  id: (job) => (job ? md5(`${job.entity}-${job.action}-${job.id}`) : ''),
104
122
  },
105
123
  });
@@ -129,6 +147,7 @@ function ABTNode(options) {
129
147
  dataDirs,
130
148
  startQueue,
131
149
  installQueue,
150
+ backupQueue,
132
151
  daemon: options.daemon,
133
152
  teamManager,
134
153
  });
@@ -28,6 +28,7 @@ const lock = new Lock('blocklet-port-assign-lock');
28
28
 
29
29
  const isHex = (str) => /^0x[0-9a-f]+$/i.test(str);
30
30
  const getMaxPort = (ports = {}) => Math.max(Object.values(ports).map(Number));
31
+ const getConditions = (did) => [{ 'meta.did': did }, { appDid: did }, { appPid: did }];
31
32
 
32
33
  const getExternalPortsFromMeta = (meta) =>
33
34
  (meta.interfaces || []).map((x) => x.port && x.port.external).filter(Boolean);
@@ -48,7 +49,16 @@ const formatBlocklet = (blocklet, phase, dek) => {
48
49
  return;
49
50
  }
50
51
 
51
- ['BLOCKLET_APP_SK'].forEach((key) => {
52
+ (b.migratedFrom || []).forEach((x) => {
53
+ if (phase === 'onUpdate' && isHex(x.appSk) === true) {
54
+ x.appSk = security.encrypt(x.appSk, b.meta.did, dek);
55
+ }
56
+ if (phase === 'onRead' && isHex(x.appSk) === false) {
57
+ x.appSk = security.decrypt(x.appSk, b.meta.did, dek);
58
+ }
59
+ });
60
+
61
+ ['BLOCKLET_APP_SK', 'BLOCKLET_APP_PSK'].forEach((key) => {
52
62
  const env = b.environments.find((x) => x.key === key);
53
63
  if (!env) {
54
64
  return;
@@ -98,7 +108,7 @@ class BlockletState extends BaseState {
98
108
  resolve(null);
99
109
  }
100
110
 
101
- this.findOne({ $or: [{ 'meta.did': did }, { appDid: did }] }, (err, doc) => {
111
+ this.findOne({ $or: getConditions(did) }, (err, doc) => {
102
112
  if (err) {
103
113
  return reject(err);
104
114
  }
@@ -114,7 +124,7 @@ class BlockletState extends BaseState {
114
124
  resolve(null);
115
125
  }
116
126
 
117
- this.findOne({ $or: [{ 'meta.did': did }, { appDid: did }] }, (err, doc) => {
127
+ this.findOne({ $or: getConditions(did) }, (err, doc) => {
118
128
  if (err) {
119
129
  return reject(err);
120
130
  }
@@ -130,7 +140,7 @@ class BlockletState extends BaseState {
130
140
  resolve(null);
131
141
  }
132
142
 
133
- this.findOne({ $or: [{ 'meta.did': did }, { appDid: did }] }, { status: 1 }, (err, doc) => {
143
+ this.findOne({ $or: getConditions(did) }, { status: 1 }, (err, doc) => {
134
144
  if (err) {
135
145
  return reject(err);
136
146
  }
@@ -146,7 +156,7 @@ class BlockletState extends BaseState {
146
156
  resolve(false);
147
157
  }
148
158
 
149
- this.count({ $or: [{ 'meta.did': did }, { appDid: did }] }, (err, count) => {
159
+ this.count({ $or: getConditions(did) }, (err, count) => {
150
160
  if (err) {
151
161
  return reject(err);
152
162
  }
@@ -199,6 +209,9 @@ class BlockletState extends BaseState {
199
209
  deployedFrom = '',
200
210
  mode = BLOCKLET_MODES.PRODUCTION,
201
211
  children: rawChildren = [],
212
+ appPid = null, // the permanent appDid, which will not change after initial set
213
+ migratedFrom = [], // the complete migrate history
214
+ externalSk = false, // whether sk is managed by some party beside server, such as did-wallet
202
215
  } = {}) {
203
216
  return this.getBlocklet(meta.did).then(
204
217
  (exist) =>
@@ -229,6 +242,7 @@ class BlockletState extends BaseState {
229
242
 
230
243
  const data = {
231
244
  appDid: null, // will updated later when updating blocklet environments
245
+ appPid,
232
246
  mode,
233
247
  meta: sanitized,
234
248
  status,
@@ -237,6 +251,8 @@ class BlockletState extends BaseState {
237
251
  ports,
238
252
  environments: [],
239
253
  children,
254
+ migratedFrom,
255
+ externalSk,
240
256
  };
241
257
 
242
258
  // add to db
@@ -305,10 +305,16 @@ const getAppSystemEnvironments = (blocklet, nodeInfo) => {
305
305
  })}`;
306
306
  }
307
307
 
308
+ const isMigrated = Array.isArray(blocklet.migratedFrom) && blocklet.migratedFrom.length > 0;
309
+ const appPid = blocklet.appPid || appId;
310
+ const appPsk = isMigrated ? blocklet.migratedFrom[0].appSk : appSk;
311
+
308
312
  return {
309
313
  BLOCKLET_DID: did,
310
314
  BLOCKLET_APP_SK: appSk,
311
315
  BLOCKLET_APP_ID: appId,
316
+ BLOCKLET_APP_PSK: appPsk, // permanent sk even the blocklet has been migrated
317
+ BLOCKLET_APP_PID: appPid, // permanent did even the blocklet has been migrated
312
318
  BLOCKLET_APP_NAME: appName,
313
319
  BLOCKLET_APP_DESCRIPTION: appDescription,
314
320
  BLOCKLET_APP_URL: appUrl,
@@ -1555,11 +1561,63 @@ const createDataArchive = (dataDir, fileName) => {
1555
1561
  });
1556
1562
  };
1557
1563
 
1564
+ const isBlockletAppSkUsed = ({ environments, migratedFrom = [] }, appSk) => {
1565
+ const isUsedInEnv = environments.find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK)?.value === appSk;
1566
+ const isUsedInHistory = migratedFrom.some((x) => x.appSk === appSk);
1567
+ return isUsedInEnv || isUsedInHistory;
1568
+ };
1569
+
1570
+ const isRotatingAppSk = (newConfigs, oldConfigs, externalSk) => {
1571
+ const newSk = newConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
1572
+ if (!newSk) {
1573
+ // If no newSk found, we are not rotating the appSk
1574
+ return false;
1575
+ }
1576
+
1577
+ const oldSk = oldConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
1578
+ if (!oldSk) {
1579
+ // If we have no oldSk, we are setting the initial appSk for external managed apps
1580
+ // If we have no oldSk, but we are not external managed apps, we are rotating the appSk
1581
+ return !externalSk;
1582
+ }
1583
+
1584
+ // Otherwise, we must be rotating the appSk
1585
+ // eslint-disable-next-line sonarjs/prefer-single-boolean-return
1586
+ if (oldSk.value !== newSk.value) {
1587
+ return true;
1588
+ }
1589
+
1590
+ return false;
1591
+ };
1592
+
1593
+ const isRotatingAppDid = (newConfigs, oldConfigs, externalSk) => {
1594
+ if (isRotatingAppSk(newConfigs, oldConfigs, externalSk)) {
1595
+ return true;
1596
+ }
1597
+
1598
+ const newType = newConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE === x.key);
1599
+ const oldType = oldConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_WALLET_TYPE === x.key);
1600
+ if (!newType) {
1601
+ return false;
1602
+ }
1603
+
1604
+ if (!oldType) {
1605
+ return true;
1606
+ }
1607
+
1608
+ // eslint-disable-next-line sonarjs/prefer-single-boolean-return
1609
+ if (oldType !== newType) {
1610
+ return true;
1611
+ }
1612
+
1613
+ return false;
1614
+ };
1615
+
1558
1616
  /**
1559
1617
  * this function has side effect on config.value
1560
1618
  * @param {{ key: string, value?: string }} config
1561
1619
  */
1562
- const validateAppConfig = async (config, blockletDid, states) => {
1620
+ const validateAppConfig = async (config, states) => {
1563
1621
  const x = config;
1564
1622
 
1565
1623
  // sk should be force secured while other app prop should not be secured
@@ -1577,15 +1635,10 @@ const validateAppConfig = async (config, blockletDid, states) => {
1577
1635
  }
1578
1636
  }
1579
1637
 
1580
- // Ensure sk is not used by other blocklets, otherwise we may encounter appDid collision
1638
+ // Ensure sk is not used by existing blocklets, otherwise we may encounter appDid collision
1581
1639
  const blocklets = await states.blocklet.getBlocklets({});
1582
- const others = blocklets.filter((b) => b.meta.did !== blockletDid);
1583
- if (
1584
- others.some(
1585
- (b) => b.environments.find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK).value === x.value
1586
- )
1587
- ) {
1588
- throw new Error('Invalid custom blocklet secret key: already used by another blocklet');
1640
+ if (blocklets.some((b) => isBlockletAppSkUsed(b, x.value))) {
1641
+ throw new Error('Invalid custom blocklet secret key: already used by existing blocklet');
1589
1642
  }
1590
1643
  } else {
1591
1644
  delete x.value;
@@ -1769,6 +1822,9 @@ module.exports = {
1769
1822
  getConfigFromPreferences,
1770
1823
  createDataArchive,
1771
1824
  validateAppConfig,
1825
+ isBlockletAppSkUsed,
1826
+ isRotatingAppSk,
1827
+ isRotatingAppDid,
1772
1828
  checkDuplicateAppSk,
1773
1829
  checkDuplicateMountPoint,
1774
1830
  validateStore,
@@ -5,7 +5,7 @@ const checkURLAccessible = require('@abtnode/util/lib/url-evaluation/check-acces
5
5
  const { EVENTS } = require('@abtnode/constant');
6
6
  const WebHookSender = require('./sender');
7
7
  const WalletSender = require('./sender/wallet');
8
- const createQueue = require('../queue');
8
+ const createQueue = require('../util/queue');
9
9
  const IP = require('../util/ip');
10
10
  const states = require('../states');
11
11
  const { getBaseUrls } = require('../util');
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.8.67-beta-794a8082",
6
+ "version": "1.8.68-beta-500af7e5",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,33 +19,33 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@abtnode/auth": "1.8.67-beta-794a8082",
23
- "@abtnode/certificate-manager": "1.8.67-beta-794a8082",
24
- "@abtnode/constant": "1.8.67-beta-794a8082",
25
- "@abtnode/cron": "1.8.67-beta-794a8082",
26
- "@abtnode/db": "1.8.67-beta-794a8082",
27
- "@abtnode/logger": "1.8.67-beta-794a8082",
28
- "@abtnode/queue": "1.8.67-beta-794a8082",
29
- "@abtnode/rbac": "1.8.67-beta-794a8082",
30
- "@abtnode/router-provider": "1.8.67-beta-794a8082",
31
- "@abtnode/static-server": "1.8.67-beta-794a8082",
32
- "@abtnode/timemachine": "1.8.67-beta-794a8082",
33
- "@abtnode/util": "1.8.67-beta-794a8082",
34
- "@arcblock/did": "1.18.54",
22
+ "@abtnode/auth": "1.8.68-beta-500af7e5",
23
+ "@abtnode/certificate-manager": "1.8.68-beta-500af7e5",
24
+ "@abtnode/constant": "1.8.68-beta-500af7e5",
25
+ "@abtnode/cron": "1.8.68-beta-500af7e5",
26
+ "@abtnode/db": "1.8.68-beta-500af7e5",
27
+ "@abtnode/logger": "1.8.68-beta-500af7e5",
28
+ "@abtnode/queue": "1.8.68-beta-500af7e5",
29
+ "@abtnode/rbac": "1.8.68-beta-500af7e5",
30
+ "@abtnode/router-provider": "1.8.68-beta-500af7e5",
31
+ "@abtnode/static-server": "1.8.68-beta-500af7e5",
32
+ "@abtnode/timemachine": "1.8.68-beta-500af7e5",
33
+ "@abtnode/util": "1.8.68-beta-500af7e5",
34
+ "@arcblock/did": "1.18.57",
35
35
  "@arcblock/did-motif": "^1.1.10",
36
- "@arcblock/did-util": "1.18.54",
37
- "@arcblock/event-hub": "1.18.54",
38
- "@arcblock/jwt": "^1.18.54",
36
+ "@arcblock/did-util": "1.18.57",
37
+ "@arcblock/event-hub": "1.18.57",
38
+ "@arcblock/jwt": "^1.18.57",
39
39
  "@arcblock/pm2-events": "^0.0.5",
40
- "@arcblock/vc": "1.18.54",
41
- "@blocklet/constant": "1.8.67-beta-794a8082",
42
- "@blocklet/meta": "1.8.67-beta-794a8082",
43
- "@blocklet/sdk": "1.8.67-beta-794a8082",
44
- "@did-space/client": "^0.1.76",
40
+ "@arcblock/vc": "1.18.57",
41
+ "@blocklet/constant": "1.8.68-beta-500af7e5",
42
+ "@blocklet/meta": "1.8.68-beta-500af7e5",
43
+ "@blocklet/sdk": "1.8.68-beta-500af7e5",
44
+ "@did-space/client": "0.1.87-beta-1",
45
45
  "@fidm/x509": "^1.2.1",
46
- "@ocap/mcrypto": "1.18.54",
47
- "@ocap/util": "1.18.54",
48
- "@ocap/wallet": "1.18.54",
46
+ "@ocap/mcrypto": "1.18.57",
47
+ "@ocap/util": "1.18.57",
48
+ "@ocap/wallet": "1.18.57",
49
49
  "@slack/webhook": "^5.0.4",
50
50
  "archiver": "^5.3.1",
51
51
  "axios": "^0.27.2",
@@ -89,5 +89,5 @@
89
89
  "express": "^4.18.2",
90
90
  "jest": "^27.5.1"
91
91
  },
92
- "gitHead": "f4ad32bea4d80b12971fb6bef941bdbe2af6a834"
92
+ "gitHead": "9070621373f317a10ff0d289323bf725e30d3521"
93
93
  }
File without changes