@abtnode/core 1.8.63 → 1.8.64-beta-0b5ede51

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.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @typedef {{
3
+ * did?: string
4
+ * }} SpaceBackupInput
5
+ */
6
+
7
+ const { isValid } = require('@arcblock/did');
8
+ const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
9
+ const { getBlockletInfo } = require('@blocklet/meta');
10
+ const { SpaceClient, SyncFolderPushCommand } = require('@did-space/client');
11
+ const { ensureDirSync, existsSync, rmdirSync, removeSync } = require('fs-extra');
12
+ const { isEmpty } = require('lodash');
13
+ const { join } = require('path');
14
+ const states = require('../../../states');
15
+ const { AuditLogBackup } = require('./audit-log');
16
+ const { BlockletBackup } = require('./blocklet');
17
+ const { BlockletExtrasBackup } = require('./blocklet-extras');
18
+ const { BlockletsBackup } = require('./blocklets');
19
+ const { DataBackup } = require('./data');
20
+ const { LogsBackup } = require('./logs');
21
+ const { RoutingRuleBackup } = require('./routing-rule');
22
+
23
+ class SpacesBackup {
24
+ /**
25
+ *
26
+ * @type {SpaceBackupInput}
27
+ * @memberof SpacesBackup
28
+ */
29
+ input;
30
+
31
+ /**
32
+ * @description blocklet state 对象
33
+ * @type {import('@abtnode/client').BlockletState}
34
+ * @memberof SpacesBackup
35
+ */
36
+ blocklet;
37
+
38
+ /**
39
+ * @description 当前 blocklet 的数据目录
40
+ * @type {string}
41
+ * @memberof SpacesBackup
42
+ */
43
+ blockletBackupDir;
44
+
45
+ /**
46
+ *
47
+ * @description server 的数据目录
48
+ * @type {string}
49
+ * @memberof SpacesBackup
50
+ */
51
+ serverDataDir;
52
+
53
+ /**
54
+ *
55
+ * @description spaces 的 endpoint
56
+ * @type {string}
57
+ * @memberof SpacesBackup
58
+ */
59
+ spacesEndpoint;
60
+
61
+ /**
62
+ *
63
+ * @description spaces 的 endpoint
64
+ * @type {import('@ocap/wallet').WalletObject}
65
+ * @memberof SpacesBackup
66
+ */
67
+ blockletWallet;
68
+
69
+ storages;
70
+
71
+ /**
72
+ *
73
+ * @param {SpaceBackupInput} input
74
+ * @memberof SpacesBackup
75
+ */
76
+ constructor(input) {
77
+ this.verify(input);
78
+ this.input = input;
79
+ this.storages = [
80
+ new AuditLogBackup(this.input),
81
+ new LogsBackup(this.input),
82
+ new BlockletBackup(this.input),
83
+ new BlockletsBackup(this.input),
84
+ new BlockletExtrasBackup(this.input),
85
+ new RoutingRuleBackup(this.input),
86
+ new DataBackup(this.input),
87
+ ];
88
+ }
89
+
90
+ /**
91
+ * @param {SpaceBackupInput} input
92
+ * @returns {void}
93
+ * @memberof SpacesBackup
94
+ */
95
+ verify(input) {
96
+ if (isEmpty(input?.did) || !isValid(input?.did)) {
97
+ throw new Error(`input.did(${input?.did}) is not a valid did`);
98
+ }
99
+ }
100
+
101
+ /**
102
+ *
103
+ * @returns {Promise<void>}
104
+ * @memberof SpacesBackup
105
+ */
106
+ async backup() {
107
+ await this.initialize();
108
+ await this.export();
109
+ await this.syncToSpaces();
110
+ await this.destroy();
111
+ }
112
+
113
+ async initialize() {
114
+ this.blocklet = await states.blocklet.findOne({
115
+ appDid: this.input.did,
116
+ });
117
+ if (isEmpty(this.blocklet)) {
118
+ throw new Error('blocklet cannot be empty');
119
+ }
120
+
121
+ this.serverDataDir = process.env.ABT_NODE_DATA_DIR;
122
+
123
+ this.blockletBackupDir = join(process.env.ABT_NODE_DATA_DIR, 'tmp/backup', this.blocklet.meta.did);
124
+ if (existsSync(this.blockletBackupDir)) {
125
+ rmdirSync(this.blockletBackupDir, { recursive: true });
126
+ }
127
+ ensureDirSync(this.blockletBackupDir);
128
+
129
+ this.spacesEndpoint = this.blocklet.environments.find(
130
+ (e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SPACE_ENDPOINT
131
+ )?.value;
132
+ if (isEmpty(this.spacesEndpoint)) {
133
+ throw new Error('spacesEndpoint cannot be empty');
134
+ }
135
+
136
+ this.blockletWallet = await this.getBlockletWallet();
137
+ }
138
+
139
+ async export() {
140
+ await Promise.all(
141
+ this.storages.map((storage) => {
142
+ storage.ensureParams(this);
143
+ return storage.export();
144
+ })
145
+ );
146
+ }
147
+
148
+ async syncToSpaces() {
149
+ const wallet = await this.getBlockletWallet();
150
+ const spaceClient = new SpaceClient({
151
+ endpoint: this.spacesEndpoint,
152
+ wallet,
153
+ });
154
+
155
+ // FIXME: 在 Spaces 里面预览出 blocklet 的样式,需要规划一个好的数据结构
156
+ const { errorCount } = await spaceClient.send(
157
+ new SyncFolderPushCommand({
158
+ source: join(this.blockletBackupDir, '/'),
159
+ target: join('.did-objects', this.blocklet.appDid),
160
+ debug: true,
161
+ retryCount: 3,
162
+ filter: (object) => {
163
+ return object.name !== '.DS_Store';
164
+ },
165
+ })
166
+ );
167
+
168
+ if (errorCount !== 0) {
169
+ throw new Error(`Sync to spaces encountered ${errorCount} error`);
170
+ }
171
+ }
172
+
173
+ async getBlockletWallet() {
174
+ const blockletInfo = await states.blocklet.getBlocklet(this.input.did);
175
+ const nodeInfo = await states.node.read();
176
+ const { wallet } = getBlockletInfo(blockletInfo, nodeInfo.sk);
177
+ return wallet;
178
+ }
179
+
180
+ async destroy() {
181
+ if (existsSync(this.blockletBackupDir)) {
182
+ removeSync(this.blockletBackupDir);
183
+ }
184
+ }
185
+ }
186
+
187
+ module.exports = {
188
+ SpacesBackup,
189
+ };
@@ -0,0 +1,55 @@
1
+ class BaseRestore {
2
+ /**
3
+ *
4
+ * @type {import('./spaces').SpaceRestoreInput}
5
+ * @memberof BaseRestore
6
+ */
7
+ input;
8
+
9
+ /**
10
+ * @description 当前 blocklet 的数据目录
11
+ * @type {string}
12
+ * @memberof BaseRestore
13
+ */
14
+ blockletRestoreDir;
15
+
16
+ /**
17
+ *
18
+ * @description spaces 的 endpoint
19
+ * @type {import('@ocap/wallet').WalletObject}
20
+ * @memberof BaseRestore
21
+ */
22
+ blockletWallet;
23
+
24
+ /**
25
+ *
26
+ * @description server 的数据目录
27
+ * @type {string}
28
+ * @memberof BaseRestore
29
+ */
30
+ serverDataDir;
31
+
32
+ constructor(input) {
33
+ this.input = input;
34
+ }
35
+
36
+ /**
37
+ *
38
+ *
39
+ * @param {import('./spaces').SpacesRestore} spaces
40
+ * @memberof BaseRestore
41
+ */
42
+ ensureParams(spaces) {
43
+ this.blockletRestoreDir = spaces.blockletRestoreDir;
44
+ this.blockletWallet = spaces.blockletWallet;
45
+ this.serverDataDir = spaces.serverDataDir;
46
+ }
47
+
48
+ async import() {
49
+ throw new Error('not implemented');
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ BaseRestore,
55
+ };
@@ -0,0 +1,84 @@
1
+ const { removeSync, outputJsonSync, readJSONSync } = require('fs-extra');
2
+ const { cloneDeep } = require('lodash');
3
+ const { join } = require('path');
4
+ const security = require('@abtnode/util/lib/security');
5
+ const { BaseRestore } = require('./base');
6
+
7
+ class BlockletExtrasRestore extends BaseRestore {
8
+ filename = 'blocklet-extras.json';
9
+
10
+ async import() {
11
+ const blockletExtra = await this.getBlockletExtra();
12
+ removeSync(join(this.blockletRestoreDir, this.filename));
13
+ outputJsonSync(join(this.blockletRestoreDir, this.filename), blockletExtra);
14
+ }
15
+
16
+ /**
17
+ *
18
+ * @description
19
+ * @return {Promise<import('@abtnode/client').BlockletState>}
20
+ * @memberof BlockletExtrasRestore
21
+ */
22
+ async getBlockletExtra() {
23
+ /**
24
+ * @type {import('@abtnode/client').BlockletState}
25
+ */
26
+ const blockletExtra = readJSONSync(join(this.blockletRestoreDir, this.filename));
27
+
28
+ return this.cleanData(blockletExtra);
29
+ }
30
+
31
+ /**
32
+ *
33
+ * @description 清理数据并加密
34
+ * @param {import('@abtnode/client').BlockletState} blockletExtraInput
35
+ * @return {Promise<void>}
36
+ * @memberof BlockletExtrasRestore
37
+ */
38
+ async cleanData(blockletExtraInput) {
39
+ const blockletExtra = cloneDeep(blockletExtraInput);
40
+
41
+ const queue = [blockletExtra];
42
+ while (queue.length) {
43
+ const currentBlockletExtra = queue.pop();
44
+
45
+ // 加解密
46
+ this.useBlockletDecryptConfigs(currentBlockletExtra.configs);
47
+
48
+ if (currentBlockletExtra?.children) {
49
+ queue.push(...currentBlockletExtra.children);
50
+ }
51
+ }
52
+
53
+ return blockletExtra;
54
+ }
55
+
56
+ /**
57
+ *
58
+ * @description 清理数据并加密
59
+ * @param {import('@abtnode/client').ConfigEntry[]} configs
60
+ * @return {void}
61
+ * @memberof BlockletExtrasRestore
62
+ */
63
+ useBlockletDecryptConfigs(configs) {
64
+ for (const config of configs) {
65
+ // secure 为 true 的配置才需要被加密保存上次到 did spaces
66
+ if (config.secure) {
67
+ const encryptByBlocklet = config.value;
68
+
69
+ // 再用 blocklet secret 加密,然后才可以上传到 spaces
70
+ const decryptByBlocklet = security.decrypt(
71
+ encryptByBlocklet,
72
+ this.blockletWallet.address,
73
+ Buffer.from(this.blockletWallet.secretKey)
74
+ );
75
+
76
+ config.value = decryptByBlocklet;
77
+ }
78
+ }
79
+
80
+ return configs;
81
+ }
82
+ }
83
+
84
+ module.exports = { BlockletExtrasRestore };
@@ -0,0 +1,24 @@
1
+ const { existsSync, removeSync } = require('fs-extra');
2
+ const { join } = require('path');
3
+ const StreamZip = require('node-stream-zip');
4
+ const { BaseRestore } = require('./base');
5
+
6
+ class BlockletsRestore extends BaseRestore {
7
+ filename = 'blocklets.zip';
8
+
9
+ async import() {
10
+ const blockletZipPath = join(this.blockletRestoreDir, this.filename);
11
+
12
+ if (!existsSync(blockletZipPath)) {
13
+ throw new Error(`file not found: ${blockletZipPath}`);
14
+ }
15
+
16
+ // eslint-disable-next-line new-cap
17
+ const zip = new StreamZip.async({ file: blockletZipPath });
18
+ await zip.extract(null, join(this.blockletRestoreDir, 'blocklets'));
19
+ await zip.close();
20
+ removeSync(blockletZipPath);
21
+ }
22
+ }
23
+
24
+ module.exports = { BlockletsRestore };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @typedef {{
3
+ * endpoint: string;
4
+ * blockletSecretKey: string;
5
+ * }} SpaceRestoreInput
6
+ */
7
+
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
+ const validUrl = require('valid-url');
14
+ const { BlockletExtrasRestore } = require('./blocklet-extras');
15
+ const { BlockletsRestore } = require('./blocklets');
16
+
17
+ class SpacesRestore {
18
+ /**
19
+ *
20
+ * @type {SpaceRestoreInput}
21
+ * @memberof SpacesRestore
22
+ */
23
+ input;
24
+
25
+ /**
26
+ * @description 当前 blocklet 的数据目录
27
+ * @type {string}
28
+ * @memberof SpacesRestore
29
+ */
30
+ blockletRestoreDir;
31
+
32
+ /**
33
+ *
34
+ * @description server 的数据目录
35
+ * @type {string}
36
+ * @memberof SpacesRestore
37
+ */
38
+ serverDataDir;
39
+
40
+ /**
41
+ *
42
+ * @description spaces 的 endpoint
43
+ * @type {import('@ocap/wallet').WalletObject}
44
+ * @memberof SpacesRestore
45
+ */
46
+ blockletWallet;
47
+
48
+ storages;
49
+
50
+ /**
51
+ *
52
+ * @param {SpaceRestoreInput} input
53
+ * @memberof SpacesRestore
54
+ */
55
+ constructor(input) {
56
+ this.verify(input);
57
+ this.input = input;
58
+ this.storages = [new BlockletExtrasRestore(this.input), new BlockletsRestore(this.input)];
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param {SpaceRestoreInput} input
64
+ * @returns {void}
65
+ * @memberof SpacesRestore
66
+ */
67
+ verify(input) {
68
+ if (!validUrl.isWebUri(input.endpoint)) {
69
+ throw new Error(`endpoint(${input.endpoint}) must be a WebUri`);
70
+ }
71
+ }
72
+
73
+ async initialize() {
74
+ this.serverDataDir = process.env.ABT_NODE_DATA_DIR;
75
+ this.blockletWallet = await this.getBlockletWallet();
76
+
77
+ this.blockletRestoreDir = join(process.env.ABT_NODE_DATA_DIR, 'tmp/restore', this.blockletWallet.address);
78
+ if (existsSync(this.blockletRestoreDir)) {
79
+ rmdirSync(this.blockletRestoreDir, { recursive: true });
80
+ }
81
+ ensureDirSync(this.blockletRestoreDir);
82
+ }
83
+
84
+ /**
85
+ *
86
+ * @returns {Promise<void>}
87
+ * @memberof SpacesRestore
88
+ */
89
+ async restore() {
90
+ await this.initialize();
91
+ await this.syncFromSpaces();
92
+ await this.import();
93
+ }
94
+
95
+ async getBlockletWallet() {
96
+ // @FIXME: blocklet 钱包类型如何得知呢?
97
+ const wallet = fromSecretKey(this.input.blockletSecretKey, WalletType({ role: types.RoleType.ROLE_APPLICATION }));
98
+
99
+ return wallet;
100
+ }
101
+
102
+ async syncFromSpaces() {
103
+ const { endpoint } = this.input;
104
+ const wallet = await this.getBlockletWallet();
105
+
106
+ const spaceClient = new SpaceClient({
107
+ endpoint,
108
+ wallet,
109
+ });
110
+
111
+ const { errorCount } = await spaceClient.send(
112
+ new SyncFolderPullCommand({
113
+ source: join('.did-objects', this.blockletWallet.address, '/'),
114
+ target: this.blockletRestoreDir,
115
+ debug: true,
116
+ retryCount: 3,
117
+ })
118
+ );
119
+
120
+ if (errorCount !== 0) {
121
+ throw new Error(`Sync from spaces encountered ${errorCount} error`);
122
+ }
123
+ }
124
+
125
+ async import() {
126
+ await Promise.all(
127
+ this.storages.map((storage) => {
128
+ storage.ensureParams(this);
129
+ return storage.import();
130
+ })
131
+ );
132
+ }
133
+ }
134
+
135
+ module.exports = {
136
+ SpacesRestore,
137
+ };
package/lib/event.js CHANGED
@@ -238,7 +238,11 @@ module.exports = ({
238
238
  logger.info('take snapshot after updated blocklet app', { event: eventName, did: blocklet.meta.did, hash });
239
239
  }
240
240
 
241
- if (blocklet.status && !isBeforeInstalled(blocklet.status)) {
241
+ if (
242
+ ![BlockletEvents.removed, BlockletEvents.dataCleaned].includes(eventName) &&
243
+ blocklet.status &&
244
+ !isBeforeInstalled(blocklet.status)
245
+ ) {
242
246
  try {
243
247
  await blockletManager.runtimeMonitor.monit(blocklet.meta.did);
244
248
  } catch (error) {
package/lib/index.js CHANGED
@@ -216,6 +216,8 @@ function ABTNode(options) {
216
216
  updateWhoCanAccess: blockletManager.updateWhoCanAccess.bind(blockletManager),
217
217
  updateComponentTitle: blockletManager.updateComponentTitle.bind(blockletManager),
218
218
  updateComponentMountPoint: blockletManager.updateComponentMountPoint.bind(blockletManager),
219
+ backupToSpaces: blockletManager.backupToSpaces.bind(blockletManager),
220
+ restoreFromSpaces: blockletManager.restoreFromSpaces.bind(blockletManager),
219
221
 
220
222
  // For diagnose purpose
221
223
  syncBlockletStatus: blockletManager.status.bind(blockletManager),
@@ -259,8 +261,9 @@ function ABTNode(options) {
259
261
  createTransferInvitation: teamAPI.createTransferInvitation.bind(teamAPI),
260
262
  getInvitation: teamAPI.getInvitation.bind(teamAPI),
261
263
  getInvitations: teamAPI.getInvitations.bind(teamAPI),
262
- processInvitation: teamAPI.processInvitation.bind(teamAPI),
264
+ checkInvitation: teamAPI.checkInvitation.bind(teamAPI),
263
265
  deleteInvitation: teamAPI.deleteInvitation.bind(teamAPI),
266
+ closeInvitation: teamAPI.closeInvitation.bind(teamAPI),
264
267
 
265
268
  // Account
266
269
  getUsers: teamAPI.getUsers.bind(teamAPI),
@@ -14,6 +14,7 @@ const getTmpDir = require('@abtnode/util/lib/get-tmp-directory');
14
14
  const downloadFile = require('@abtnode/util/lib/download-file');
15
15
  const { updateBlockletDocument } = require('@abtnode/util/lib/did-document');
16
16
  const getBlockletInfo = require('@blocklet/meta/lib/info');
17
+ const { forEachBlocklet } = require('@blocklet/meta/lib/util');
17
18
  const {
18
19
  DOMAIN_FOR_DEFAULT_SITE,
19
20
  DOMAIN_FOR_IP_SITE_REGEXP,
@@ -768,47 +769,77 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
768
769
  return false;
769
770
  }
770
771
 
771
- const tasks = (blocklet.meta.interfaces || [])
772
- .filter((x) => x.type === BLOCKLET_INTERFACE_TYPE_WELLKNOWN)
773
- .map(async (x) => {
774
- const pathPrefix = normalizePathPrefix(x.prefix);
775
- if (!pathPrefix.startsWith(WELLKNOWN_PATH_PREFIX)) {
776
- throw new Error(`Wellknown path prefix must start with: ${WELLKNOWN_PATH_PREFIX}`);
777
- }
772
+ /**
773
+ * 1. component blocklet 不允许有相同多的 wellknown
774
+ * 1. wellknown 可以访问的路由:
775
+ * /.well-known/xxx
776
+ * /{wellknown owner blocklet}/.well-known/xxx
777
+ */
778
778
 
779
- const port = findInterfacePortByName(blocklet, x.name);
780
- const existedRule = hasRuleByPrefix(wellknownSite, pathPrefix);
779
+ const isWellknownInterface = (x) => x.type === BLOCKLET_INTERFACE_TYPE_WELLKNOWN;
781
780
 
782
- const rule = {
783
- from: { pathPrefix },
784
- to: {
785
- did: blocklet.meta.did,
786
- port,
787
- type: ROUTING_RULE_TYPES.GENERAL_PROXY,
788
- interfaceName: x.name,
789
- },
790
- isProtected: true,
791
- };
781
+ const handler = async (tmpBlocklet, tmpInterface, mountPoint) => {
782
+ let pathPrefix = normalizePathPrefix(tmpInterface.prefix);
783
+ if (!pathPrefix.startsWith(WELLKNOWN_PATH_PREFIX)) {
784
+ throw new Error(`Wellknown path prefix must start with: ${WELLKNOWN_PATH_PREFIX}`);
785
+ }
792
786
 
793
- if (!existedRule) {
794
- await routerManager.addRoutingRule({ id: wellknownSite.id, rule, skipCheckDynamicBlacklist: true }, context);
795
- return true;
796
- }
787
+ const tmpMountPoint = mountPoint || tmpBlocklet.mountPoint;
788
+ if (tmpMountPoint) {
789
+ pathPrefix = joinUrl(tmpMountPoint, pathPrefix);
790
+ }
797
791
 
798
- // 兼容代码,旧的 rule 没有 to.did 字段
799
- if (port !== existedRule.to.port || existedRule.to.did !== blocklet.meta.did) {
800
- existedRule.to = rule.to;
792
+ const port = findInterfacePortByName(tmpBlocklet, tmpInterface.name);
793
+ const existedRule = hasRuleByPrefix(wellknownSite, pathPrefix);
801
794
 
802
- await routerManager.updateRoutingRule(
803
- { id: wellknownSite.id, rule: existedRule, skipProtectedRuleChecking: true },
804
- context
805
- );
795
+ const rule = {
796
+ from: { pathPrefix },
797
+ to: {
798
+ did: tmpBlocklet.meta.did,
799
+ port,
800
+ type: ROUTING_RULE_TYPES.GENERAL_PROXY,
801
+ interfaceName: tmpInterface.name,
802
+ },
803
+ isProtected: true,
804
+ };
806
805
 
807
- return true;
808
- }
806
+ if (!existedRule) {
807
+ await routerManager.addRoutingRule({ id: wellknownSite.id, rule, skipCheckDynamicBlacklist: true }, context);
808
+ return true;
809
+ }
809
810
 
810
- return false;
811
- });
811
+ // 兼容代码,旧的 rule 没有 to.did 字段
812
+ if (port !== existedRule.to.port || existedRule.to.did !== tmpBlocklet.meta.did) {
813
+ existedRule.to = rule.to;
814
+
815
+ await routerManager.updateRoutingRule(
816
+ { id: wellknownSite.id, rule: existedRule, skipProtectedRuleChecking: true },
817
+ context
818
+ );
819
+
820
+ return true;
821
+ }
822
+
823
+ return false;
824
+ };
825
+
826
+ const tasks = [];
827
+
828
+ forEachBlocklet(
829
+ blocklet,
830
+ (b) => {
831
+ (b.meta.interfaces || []).forEach((item) => {
832
+ if (isWellknownInterface(item)) {
833
+ tasks.push(handler(b, item));
834
+
835
+ if (!b.mountPoint || b.mountPoint !== '/') {
836
+ tasks.push(handler(b, item, '/')); // 在站点的根路由下挂载一个
837
+ }
838
+ }
839
+ });
840
+ },
841
+ { sync: true }
842
+ );
812
843
 
813
844
  const changes = await Promise.all(tasks);
814
845
  return changes.some(Boolean);
@@ -199,6 +199,10 @@ class BlockletExtrasState extends BaseState {
199
199
  return super.update({ did }, { $set: entity }, { upsert: true });
200
200
  }
201
201
 
202
+ async getMeta(did) {
203
+ return super.findOne({ did }, { did: 1, controller: 1, meta: 1 });
204
+ }
205
+
202
206
  async updateExpireInfo({ did, expiredAt } = {}) {
203
207
  const entity = { did, expiredAt };
204
208
  await validateExpiredInfo(entity);