@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.
package/lib/api/team.js CHANGED
@@ -408,6 +408,26 @@ class TeamAPI extends EventEmitter {
408
408
 
409
409
  // Invite member
410
410
 
411
+ /**
412
+ * @type InvitationSession {
413
+ * type: 'invite';
414
+ * role: string;
415
+ * remark: string;
416
+ * expireDate: number;
417
+ * inviter: {
418
+ * did: string;
419
+ * email?: string;
420
+ * fullName?: string;
421
+ * role?: string;
422
+ * };
423
+ * teamDid: string;
424
+ * status: '' | 'success';
425
+ * receiver: {
426
+ * did: string;
427
+ * };
428
+ * }
429
+ * @returns
430
+ */
411
431
  async createMemberInvitation({ teamDid, role, expireTime, remark }, context) {
412
432
  await this.teamManager.checkEnablePassportIssuance(teamDid);
413
433
 
@@ -484,15 +504,17 @@ class TeamAPI extends EventEmitter {
484
504
  expireDate: new Date(invitation.expireDate).toString(),
485
505
  inviter: invitation.inviter,
486
506
  teamDid: invitation.teamDid,
507
+ status: invitation.status,
508
+ receiver: invitation.receiver,
487
509
  };
488
510
  }
489
511
 
490
- async getInvitations({ teamDid }) {
512
+ async getInvitations({ teamDid, filter }) {
491
513
  const state = await this.getSessionState(teamDid);
492
514
 
493
515
  const invitations = await state.find({ type: 'invite' });
494
516
 
495
- return invitations.map((d) => ({
517
+ return invitations.filter(filter || ((x) => x.status !== 'success')).map((d) => ({
496
518
  // eslint-disable-next-line no-underscore-dangle
497
519
  inviteId: d._id,
498
520
  role: d.role,
@@ -500,6 +522,8 @@ class TeamAPI extends EventEmitter {
500
522
  expireDate: new Date(d.expireDate).toString(),
501
523
  inviter: d.inviter,
502
524
  teamDid: d.teamDid,
525
+ status: d.status,
526
+ receiver: d.receiver,
503
527
  }));
504
528
  }
505
529
 
@@ -516,7 +540,7 @@ class TeamAPI extends EventEmitter {
516
540
  return true;
517
541
  }
518
542
 
519
- async processInvitation({ teamDid, inviteId }) {
543
+ async checkInvitation({ teamDid, inviteId }) {
520
544
  const state = await this.getSessionState(teamDid);
521
545
 
522
546
  const invitation = await state.read(inviteId);
@@ -538,16 +562,33 @@ class TeamAPI extends EventEmitter {
538
562
  throw new Error(`Role does not exist: ${role}`);
539
563
  }
540
564
 
541
- await state.end(inviteId);
542
-
543
- logger.info('Invitation session completed successfully', { inviteId, role });
544
-
545
565
  return {
546
566
  role,
547
567
  remark,
548
568
  };
549
569
  }
550
570
 
571
+ async closeInvitation({ teamDid, inviteId, status, receiver, timeout = 30 * 1000 }) {
572
+ const state = await this.getSessionState(teamDid);
573
+
574
+ const invitation = await state.read(inviteId);
575
+
576
+ if (!invitation) {
577
+ throw new Error(`The invitation does not exist: ${inviteId}`);
578
+ }
579
+
580
+ await state.update(inviteId, { status, receiver });
581
+
582
+ setTimeout(async () => {
583
+ try {
584
+ logger.info('Invitation session closed', { inviteId });
585
+ await state.end(inviteId);
586
+ } catch (error) {
587
+ logger.error('close invitation failed', { error });
588
+ }
589
+ }, timeout);
590
+ }
591
+
551
592
  // Issue Passport
552
593
 
553
594
  async createPassportIssuance({ teamDid, ownerDid, name }) {
@@ -134,6 +134,8 @@ const handleInstanceInStore = require('../../util/public-to-store');
134
134
  const { getNFTState, getServerDidDomain } = require('../../util');
135
135
  const { BlockletRuntimeMonitor } = require('../../monitor/blocklet-runtime-monitor');
136
136
  const getHistoryList = require('../../monitor/get-history-list');
137
+ const { SpacesBackup } = require('../storage/backup/spaces');
138
+ const { SpacesRestore } = require('../storage/restore/spaces');
137
139
  const installFromBackup = require('./helper/install-from-backup');
138
140
 
139
141
  const {
@@ -609,6 +611,35 @@ class BlockletManager extends BaseBlockletManager {
609
611
  return blocklet;
610
612
  }
611
613
 
614
+ /**
615
+ *
616
+ *
617
+ * @param {import('@abtnode/client').BackupToSpacesParams} input
618
+ * @memberof BlockletManager
619
+ */
620
+ async backupToSpaces(input) {
621
+ const spacesBackup = new SpacesBackup(input);
622
+ await spacesBackup.backup();
623
+ }
624
+
625
+ /**
626
+ *
627
+ *
628
+ * @param {import('@abtnode/client').RequestRestoreFromSpacesInput} input
629
+ * @memberof BlockletManager
630
+ */
631
+ async restoreFromSpaces(input) {
632
+ const spacesRestore = new SpacesRestore(input);
633
+ await spacesRestore.restore();
634
+
635
+ // FIXME: 需要改成队列执行,本次失败,下次还需要重试,页面刷新后也能知道最新的状态
636
+ await this._installFromBackup({
637
+ url: `file://${spacesRestore.blockletRestoreDir}`,
638
+ blockletSecretKey: spacesRestore.blockletWallet.secretKey,
639
+ moveDir: true,
640
+ });
641
+ }
642
+
612
643
  async restart({ did }, context) {
613
644
  logger.info('restart blocklet', { did });
614
645
 
@@ -1617,6 +1648,11 @@ class BlockletManager extends BaseBlockletManager {
1617
1648
  * /data
1618
1649
  * /blocklet.json
1619
1650
  * /blocklet_extras.json
1651
+ * @see Blocklet 端到端备份方案的设计与实现(一期) https://team.arcblock.io/comment/discussions/e62084d5-fafa-489e-91d5-defcd6e93223
1652
+ * @param {{ url: string, blockletSecretKey: string, moveDir: boolean}} [{ url }={}]
1653
+ * @param {Record<string, string>} [context={}]
1654
+ * @return {Promise<any>}
1655
+ * @memberof BlockletManager
1620
1656
  */
1621
1657
  async _installFromBackup({ url, blockletSecretKey, moveDir } = {}, context = {}) {
1622
1658
  return installFromBackup({ url, blockletSecretKey, moveDir, context, manager: this, states });
@@ -2090,10 +2126,9 @@ class BlockletManager extends BaseBlockletManager {
2090
2126
 
2091
2127
  if (controller?.nftId) {
2092
2128
  // sometimes nedb will throw error if use states.blocklet.count({ ['controller.nftId']: controller.nftId })
2093
- const blocklets = await states.blocklet.find({}, { controller: 1 });
2094
- const count = blocklets.filter((x) => x.controller?.nftId === controller.nftId).length;
2129
+ const installedCount = await states.blockletExtras.count({ 'controller.nftId': controller.nftId });
2095
2130
 
2096
- if (count >= (controller.appMaxCount || 1)) {
2131
+ if (installedCount >= (controller.appMaxCount || 1)) {
2097
2132
  throw new Error(
2098
2133
  `You can only install ${controller.appMaxCount} blocklet with this credential: ${controller.nftId}`
2099
2134
  );
@@ -2691,7 +2726,6 @@ class BlockletManager extends BaseBlockletManager {
2691
2726
  source,
2692
2727
  deployedFrom,
2693
2728
  children,
2694
- controller,
2695
2729
  });
2696
2730
 
2697
2731
  await validateBlocklet(blocklet);
@@ -3798,7 +3832,8 @@ class BlockletManager extends BaseBlockletManager {
3798
3832
 
3799
3833
  async _createNotification(did, notification) {
3800
3834
  try {
3801
- const isExternal = await states.blocklet.isExternalBlocklet(did);
3835
+ const extra = await states.blockletExtras.getMeta(did);
3836
+ const isExternal = !!extra?.controller;
3802
3837
 
3803
3838
  if (isExternal) {
3804
3839
  return;
@@ -0,0 +1,27 @@
1
+ const { removeSync, outputJsonSync } = require('fs-extra');
2
+ const { join } = require('path');
3
+ const states = require('../../../states');
4
+ const { BaseBackup } = require('./base');
5
+
6
+ /**
7
+ *
8
+ *
9
+ * @class AuditLogBackup
10
+ */
11
+ class AuditLogBackup extends BaseBackup {
12
+ filename = 'audit-log.json';
13
+
14
+ async export() {
15
+ /**
16
+ * @type {import('@abtnode/client').AuditLog}
17
+ */
18
+ const auditLogs = await states.auditLog.find({
19
+ scope: this.blocklet.meta.did,
20
+ });
21
+
22
+ removeSync(join(this.blockletBackupDir, this.filename));
23
+ outputJsonSync(join(this.blockletBackupDir, this.filename), auditLogs);
24
+ }
25
+ }
26
+
27
+ module.exports = { AuditLogBackup };
@@ -0,0 +1,63 @@
1
+ class BaseBackup {
2
+ /**
3
+ *
4
+ * @type {import('./spaces').SpaceBackupInput}
5
+ * @memberof BaseBackup
6
+ */
7
+ input;
8
+
9
+ /**
10
+ * @description blocklet state 对象
11
+ * @type {import('@abtnode/client').BlockletState}
12
+ * @memberof BaseBackup
13
+ */
14
+ blocklet;
15
+
16
+ /**
17
+ * @description 当前 blocklet 的数据目录
18
+ * @type {string}
19
+ * @memberof BaseBackup
20
+ */
21
+ blockletBackupDir;
22
+
23
+ /**
24
+ *
25
+ * @description spaces 的 endpoint
26
+ * @type {import('@ocap/wallet').WalletObject}
27
+ * @memberof BaseBackup
28
+ */
29
+ blockletWallet;
30
+
31
+ /**
32
+ *
33
+ * @description server 的数据目录
34
+ * @type {string}
35
+ * @memberof BaseBackup
36
+ */
37
+ serverDataDir;
38
+
39
+ constructor(input) {
40
+ this.input = input;
41
+ }
42
+
43
+ /**
44
+ *
45
+ *
46
+ * @param {import('./spaces').SpacesBackup} spacesBackup
47
+ * @memberof BaseBackup
48
+ */
49
+ ensureParams(spacesBackup) {
50
+ this.blocklet = spacesBackup.blocklet;
51
+ this.serverDataDir = spacesBackup.serverDataDir;
52
+ this.blockletBackupDir = spacesBackup.blockletBackupDir;
53
+ this.blockletWallet = spacesBackup.blockletWallet;
54
+ }
55
+
56
+ async export() {
57
+ throw new Error('not implemented');
58
+ }
59
+ }
60
+
61
+ module.exports = {
62
+ BaseBackup,
63
+ };
@@ -0,0 +1,105 @@
1
+ const { removeSync, outputJsonSync, readFileSync } = require('fs-extra');
2
+ const { isEmpty, cloneDeep } = require('lodash');
3
+ const { join } = require('path');
4
+ const security = require('@abtnode/util/lib/security');
5
+ const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
6
+ const states = require('../../../states');
7
+ const { BaseBackup } = require('./base');
8
+
9
+ class BlockletExtrasBackup extends BaseBackup {
10
+ filename = 'blocklet-extras.json';
11
+
12
+ async export() {
13
+ const blockletExtra = await this.getBlockletExtra();
14
+ removeSync(join(this.blockletBackupDir, this.filename));
15
+ outputJsonSync(join(this.blockletBackupDir, this.filename), blockletExtra);
16
+ }
17
+
18
+ /**
19
+ *
20
+ * @description
21
+ * @return {Promise<import('@abtnode/client').BlockletState>}
22
+ * @memberof BlockletExtrasBackup
23
+ */
24
+ async getBlockletExtra() {
25
+ /**
26
+ * @type {import('@abtnode/client').BlockletState}
27
+ */
28
+ const blockletExtra = await states.blockletExtras.findOne({
29
+ did: this.blocklet.meta.did,
30
+ });
31
+ if (isEmpty(blockletExtra)) {
32
+ throw new Error('blockletExtra cannot be empty');
33
+ }
34
+
35
+ return this.cleanDate(blockletExtra);
36
+ }
37
+
38
+ /**
39
+ *
40
+ * @description 清理数据并加密
41
+ * @param {import('@abtnode/client').BlockletState} blockletExtraInput
42
+ * @return {Promise<void>}
43
+ * @memberof BlockletExtrasBackup
44
+ */
45
+ async cleanDate(blockletExtraInput) {
46
+ const blockletExtra = cloneDeep(blockletExtraInput);
47
+
48
+ const queue = [blockletExtra];
49
+ while (queue.length) {
50
+ const currentBlockletExtra = queue.pop();
51
+
52
+ // 删除父 blocklet 的某些数据
53
+ if (currentBlockletExtra._id) {
54
+ delete currentBlockletExtra._id;
55
+ delete currentBlockletExtra.createdAt;
56
+ delete currentBlockletExtra.updatedAt;
57
+ }
58
+
59
+ // 加解密
60
+ this.useBlockletEncryptConfigs(currentBlockletExtra.configs);
61
+
62
+ if (currentBlockletExtra?.children) {
63
+ queue.push(...currentBlockletExtra.children);
64
+ }
65
+ }
66
+
67
+ return blockletExtra;
68
+ }
69
+
70
+ /**
71
+ *
72
+ * @description 清理数据并加密
73
+ * @param {import('@abtnode/client').ConfigEntry[]} configs
74
+ * @return {void}
75
+ * @memberof BlockletExtrasBackup
76
+ */
77
+ useBlockletEncryptConfigs(configs) {
78
+ // 准备加解密所需的参数
79
+ // @see: https://github.com/ArcBlock/blocklet-server/blob/f561ba7290285f2e23dccb6d5323eb4d43c3cc3e/core/state/lib/index.js#L59
80
+ const dek = readFileSync(join(this.serverDataDir, '.sock'));
81
+
82
+ for (const config of configs) {
83
+ // 置空 blocklet 的密钥,但是保留其他属性,这很重要
84
+ if (config.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK) {
85
+ config.value = '';
86
+ } else if (config.secure) {
87
+ // secure 为 true 的配置才需要被加密保存上次到 did spaces
88
+
89
+ const encryptByServer = config.value;
90
+ // 先从 blocklet server 解密
91
+ // @see https://github.com/ArcBlock/blocklet-server/blob/f40338168a66893f325464cea79ae54c43f623b1/core/state/lib/blocklet/extras.js#L139
92
+ const decryptByServer = security.decrypt(encryptByServer, this.blocklet.meta.did, dek);
93
+ // 再用 blocklet secret 加密,然后才可以上传到 spaces
94
+ const encryptByBlocklet = security.encrypt(
95
+ decryptByServer,
96
+ this.blockletWallet.address,
97
+ Buffer.from(this.blockletWallet.secretKey)
98
+ );
99
+ config.value = encryptByBlocklet;
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = { BlockletExtrasBackup };
@@ -0,0 +1,53 @@
1
+ const { removeSync, outputJsonSync } = require('fs-extra');
2
+ const { cloneDeep } = require('lodash');
3
+ const { join } = require('path');
4
+ const { BaseBackup } = require('./base');
5
+
6
+ class BlockletBackup extends BaseBackup {
7
+ filename = 'blocklet.json';
8
+
9
+ async export() {
10
+ const blocklet = await this.cleanData();
11
+ removeSync(join(this.blockletBackupDir, this.filename));
12
+ outputJsonSync(join(this.blockletBackupDir, this.filename), blocklet);
13
+ }
14
+
15
+ /**
16
+ * @description 清理数据
17
+ * @see blocklet.db 中需要删除哪些字段呢? https://github.com/ArcBlock/blocklet-server/issues/6120#issuecomment-1383798348
18
+ * @return {Promise<void>}
19
+ * @memberof BlockletBackup
20
+ */
21
+ async cleanData() {
22
+ const originalBlocklet = cloneDeep(this.blocklet);
23
+
24
+ /** @type {import('@abtnode/client').ComponentState[]} */
25
+ const queue = [originalBlocklet];
26
+
27
+ // 广度优先遍历
28
+ while (queue.length) {
29
+ const currentBlocklet = queue.pop();
30
+
31
+ // 父组件才需要删除的属性
32
+ if (currentBlocklet._id) {
33
+ delete currentBlocklet._id;
34
+ delete currentBlocklet.createdAt;
35
+ delete currentBlocklet.startedAt;
36
+ delete currentBlocklet.installedAt;
37
+ }
38
+
39
+ // 子组件和父组件都需要删除的属性
40
+ delete currentBlocklet.status;
41
+ delete currentBlocklet.ports;
42
+ delete currentBlocklet.environments;
43
+
44
+ if (currentBlocklet.children) {
45
+ queue.push(...currentBlocklet.children);
46
+ }
47
+ }
48
+
49
+ return originalBlocklet;
50
+ }
51
+ }
52
+
53
+ module.exports = { BlockletBackup };
@@ -0,0 +1,80 @@
1
+ const { removeSync, existsSync, ensureDirSync, createWriteStream } = require('fs-extra');
2
+ const { join, dirname } = require('path');
3
+ const archiver = require('archiver');
4
+ const { BaseBackup } = require('./base');
5
+
6
+ class BlockletsBackup extends BaseBackup {
7
+ /**
8
+ *
9
+ * @returns {Promise<void>}
10
+ * @memberof BlockletsBackup
11
+ */
12
+ async export() {
13
+ const blockletMetas = this.getBlockletMetas(this.blocklet);
14
+ const serverBlockletsDir = join(this.serverDataDir, 'blocklets');
15
+
16
+ const dirs = [];
17
+ for (const blockletMeta of blockletMetas) {
18
+ const sourceDir = join(serverBlockletsDir, blockletMeta.name, blockletMeta.version);
19
+ const destDir = join(blockletMeta.name, blockletMeta.version);
20
+ dirs.push({ sourceDir, destDir });
21
+ }
22
+
23
+ await this.dirsToZip(dirs, join(this.blockletBackupDir, 'blocklets.zip'));
24
+ }
25
+
26
+ /**
27
+ *
28
+ *
29
+ * @param {import('@abtnode/client').BlockletState} blocklet
30
+ * @returns {Array<{name: string, version: string}>}
31
+ * @memberof BlockletsBackup
32
+ */
33
+ getBlockletMetas(blocklet) {
34
+ if (!blocklet?.meta?.bundleName || !blocklet?.meta?.version) {
35
+ return [];
36
+ }
37
+
38
+ const metas = [];
39
+ metas.push({
40
+ name: blocklet.meta.bundleName,
41
+ version: blocklet.meta.version,
42
+ });
43
+
44
+ for (const child of blocklet.children) {
45
+ metas.push(...this.getBlockletMetas(child));
46
+ }
47
+
48
+ return metas;
49
+ }
50
+
51
+ /**
52
+ * @param {Array<{sourceDir: string, destDir: string}>} dirs: /some/folder/to/compress
53
+ * @param {String} zipPath: /path/to/created.zip
54
+ * @returns {Promise}
55
+ * @memberof BlockletsBackup
56
+ */
57
+ async dirsToZip(dirs, zipPath) {
58
+ ensureDirSync(dirname(zipPath));
59
+ if (existsSync(zipPath)) {
60
+ removeSync(zipPath);
61
+ }
62
+
63
+ const archive = archiver('zip', { zlib: { level: 9 } });
64
+ const stream = createWriteStream(zipPath);
65
+
66
+ return new Promise((resolve, reject) => {
67
+ archive.on('error', (err) => reject(err));
68
+ stream.on('close', () => resolve());
69
+
70
+ for (const dir of dirs) {
71
+ archive.directory(dir.sourceDir, dir.destDir);
72
+ }
73
+
74
+ archive.pipe(stream);
75
+ archive.finalize();
76
+ });
77
+ }
78
+ }
79
+
80
+ module.exports = { BlockletsBackup };
@@ -0,0 +1,19 @@
1
+ const { copySync } = require('fs-extra');
2
+ const { join } = require('path');
3
+ const { BaseBackup } = require('./base');
4
+
5
+ class DataBackup extends BaseBackup {
6
+ /**
7
+ *
8
+ * @returns {Promise<void>}
9
+ * @memberof BlockletsBackup
10
+ */
11
+ async export() {
12
+ const blockletDataDir = join(this.serverDataDir, 'data', this.blocklet.meta.name);
13
+ const blockletBackupDataDir = join(this.blockletBackupDir, 'data');
14
+
15
+ copySync(blockletDataDir, blockletBackupDataDir, { overwrite: true });
16
+ }
17
+ }
18
+
19
+ module.exports = { DataBackup };
@@ -0,0 +1,23 @@
1
+ const { ensureDirSync, copySync, statSync } = require('fs-extra');
2
+ const { join } = require('path');
3
+ const { BaseBackup } = require('./base');
4
+
5
+ class LogsBackup extends BaseBackup {
6
+ async export() {
7
+ const sourceLogsDir = join(this.serverDataDir, 'logs', this.blocklet.meta.name);
8
+ ensureDirSync(sourceLogsDir);
9
+
10
+ const targetLogsDir = join(this.blockletBackupDir, 'logs');
11
+
12
+ copySync(sourceLogsDir, targetLogsDir, {
13
+ overwrite: true,
14
+ filter: (src) => {
15
+ return !statSync(src).isSymbolicLink();
16
+ },
17
+ });
18
+ }
19
+ }
20
+
21
+ module.exports = {
22
+ LogsBackup,
23
+ };
@@ -0,0 +1,19 @@
1
+ const { removeSync, outputJsonSync } = require('fs-extra');
2
+ const { join } = require('path');
3
+ const states = require('../../../states');
4
+ const { BaseBackup } = require('./base');
5
+
6
+ class RoutingRuleBackup extends BaseBackup {
7
+ filename = 'routing_rule.json';
8
+
9
+ async export() {
10
+ const routingRule = await states.site.findOne({
11
+ domain: `${this.blocklet.meta.did}.blocklet-domain-group`,
12
+ });
13
+
14
+ removeSync(join(this.blockletBackupDir, this.filename));
15
+ outputJsonSync(join(this.blockletBackupDir, this.filename), routingRule);
16
+ }
17
+ }
18
+
19
+ module.exports = { RoutingRuleBackup };