@abtnode/core 1.16.14-beta-a898bfcb → 1.16.14-beta-d802cd3c

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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-classes-per-file */
1
2
  /* eslint-disable no-underscore-dangle */
2
3
  /* eslint-disable no-await-in-loop */
3
4
  const fs = require('fs-extra');
@@ -25,6 +26,7 @@ const {
25
26
  APP_STRUCT_VERSION,
26
27
  BLOCKLET_CACHE_TTL,
27
28
  MONITOR_RECORD_INTERVAL_SEC,
29
+ INSTALL_ACTIONS,
28
30
  } = require('@abtnode/constant');
29
31
 
30
32
  const getBlockletEngine = require('@blocklet/meta/lib/engine');
@@ -45,7 +47,6 @@ const {
45
47
  forEachComponentV2Sync,
46
48
  findComponentByIdV2,
47
49
  } = require('@blocklet/meta/lib/util');
48
- const getComponentProcessId = require('@blocklet/meta/lib/get-component-process-id');
49
50
  const { update: updateMetaFile } = require('@blocklet/meta/lib/file');
50
51
  const { titleSchema, updateMountPointSchema, environmentNameSchema } = require('@blocklet/meta/lib/schema');
51
52
  const { emailConfigSchema } = require('@blocklet/sdk/lib/validators/email');
@@ -76,6 +77,12 @@ const {
76
77
  SUSPENDED_REASON,
77
78
  } = require('@blocklet/constant');
78
79
  const isUndefined = require('lodash/isUndefined');
80
+ const { WELLKNOWN_SERVICE_PATH_PREFIX } = require('@abtnode/constant');
81
+ const { signV2 } = require('@arcblock/jwt');
82
+ const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
83
+ const pLimit = require('p-limit');
84
+ const pRetry = require('p-retry');
85
+
79
86
  const { consumeServerlessNFT, consumeLauncherSession } = require('../../util/launcher');
80
87
  const util = require('../../util');
81
88
  const {
@@ -118,7 +125,9 @@ const {
118
125
  shouldSkipComponent,
119
126
  exceedRedemptionPeriod,
120
127
  ensureAppPortsNotOccupied,
128
+ getComponentNamesWithVersion,
121
129
  } = require('../../util/blocklet');
130
+ const { getDidDomainForBlocklet } = require('../../util/get-domain-for-blocklet');
122
131
  const states = require('../../states');
123
132
  const BaseBlockletManager = require('./base');
124
133
  const { get: getEngine } = require('./engine');
@@ -147,9 +156,12 @@ const { getBackupFilesUrlFromEndpoint, getBackupEndpoint, getSpaceNameByEndpoint
147
156
  const { validateAddSpaceGateway, validateUpdateSpaceGateway } = require('../../validators/space-gateway');
148
157
  const { sessionConfigSchema } = require('../../validators/util');
149
158
 
159
+ const request = require('../../util/request');
160
+
150
161
  const { formatEnvironments, getBlockletMeta, validateOwner } = util;
151
162
 
152
163
  const statusLock = new Lock('blocklet-status-lock');
164
+ const limitSync = pLimit(1);
153
165
 
154
166
  const getHooksOutputFiles = (blocklet) => ({
155
167
  output: path.join(blocklet.env.logsDir, 'output.log'),
@@ -168,57 +180,7 @@ const pm2StatusMap = {
168
180
  */
169
181
  const getBlockletEngineNameByPlatform = (blockletMeta) => getBlockletEngine(blockletMeta).interpreter;
170
182
 
171
- /**
172
- * @param {{
173
- * newBlocklet,
174
- * oldBlocklet,
175
- * context: { forceStartProcessIds?: string[] },
176
- * }}
177
- * @returns {{ installedComponentNames: string[], skippedProcessIds: string[] }}
178
- */
179
- const getComponentChangedInfoForUpgrade = ({ newBlocklet, oldBlocklet, context = {} }) => {
180
- const { forceStartProcessIds = [] } = context;
181
- const idMap = {};
182
- const skippedProcessIds = [];
183
- const installedComponentNames = [];
184
-
185
- forEachBlockletSync(oldBlocklet, (b, { ancestors }) => {
186
- if (b.meta.dist?.integrity) {
187
- idMap[getComponentProcessId(b, ancestors)] = b.meta.dist?.integrity;
188
- }
189
- });
190
-
191
- forEachBlockletSync(newBlocklet, (b, { ancestors }) => {
192
- const id = getComponentProcessId(b, ancestors);
193
- if (forceStartProcessIds.includes(id)) {
194
- installedComponentNames.push(b.meta.title);
195
- return;
196
- }
197
-
198
- if (!b.meta.dist?.integrity || b.meta.dist.integrity === idMap[id]) {
199
- skippedProcessIds.push(id);
200
- } else {
201
- installedComponentNames.push(b.meta.title);
202
- }
203
- });
204
-
205
- return { skippedProcessIds, installedComponentNames };
206
- };
207
-
208
- class BlockletManager extends BaseBlockletManager {
209
- /**
210
- * Creates an instance of BlockletManager.
211
- * @param {{
212
- * dataDirs: ReturnType<typeof import('../../util/index').getDataDirs>,
213
- * startQueue: ReturnType<typeof import('../../util/queue')>,
214
- * installQueue: ReturnType<typeof import('../../util/queue')>,
215
- * backupQueue: ReturnType<typeof import('../../util/queue')>,
216
- * restoreQueue: ReturnType<typeof import('../../util/queue')>,
217
- * daemon: boolean
218
- * teamManager: import('../../team/manager.js')
219
- * }} { dataDirs, startQueue, installQueue, backupQueue, restoreQueue, daemon = false, teamManager }
220
- * @memberof BlockletManager
221
- */
183
+ class DiskBlockletManager extends BaseBlockletManager {
222
184
  constructor({ dataDirs, startQueue, installQueue, backupQueue, restoreQueue, daemon = false, teamManager }) {
223
185
  super();
224
186
 
@@ -469,7 +431,7 @@ class BlockletManager extends BaseBlockletManager {
469
431
  const componentDids = inputComponentDids?.length ? inputComponentDids : blocklet.children.map((x) => x.meta.did);
470
432
 
471
433
  const tasks = componentDids.map((componentDid) =>
472
- this._start({ did, throwOnError, checkHealthImmediately, e2eMode, componentDids: [componentDid] }, context)
434
+ this._start({ blocklet, throwOnError, checkHealthImmediately, e2eMode, componentDids: [componentDid] }, context)
473
435
  );
474
436
 
475
437
  return Promise.any(tasks).catch((err) => {
@@ -485,6 +447,8 @@ class BlockletManager extends BaseBlockletManager {
485
447
  // should check blocklet integrity
486
448
  const blocklet1 = inputBlocklet || (await this.ensureBlocklet(did, { e2eMode }));
487
449
 
450
+ did = blocklet1.meta.did; // eslint-disable-line no-param-reassign
451
+
488
452
  await this.checkControllerStatus(blocklet1, 'start');
489
453
 
490
454
  // validate requirement and engine
@@ -507,22 +471,12 @@ class BlockletManager extends BaseBlockletManager {
507
471
  // blocklet may be manually stopped durning starting
508
472
  // so error message would not be sent if blocklet is stopped
509
473
  // so we need update status first
510
- const doc1 = await states.blocklet.setBlockletStatus(did, BlockletStatus.starting, { componentDids });
474
+ const doc1 = await states.blocklet.setBlockletStatus(did, BlockletStatus.starting, {
475
+ componentDids,
476
+ });
511
477
  blocklet1.status = BlockletStatus.starting;
512
478
  this.emit(BlockletEvents.statusChange, doc1);
513
479
 
514
- if (blocklet1.mode === BLOCKLET_MODES.DEVELOPMENT) {
515
- const { logsDir } = blocklet1.env;
516
-
517
- try {
518
- fs.removeSync(logsDir);
519
- fs.mkdirSync(logsDir, { recursive: true });
520
- } catch {
521
- // Windows && Node.js 18.x 下会发生删除错误(ENOTEMPTY)
522
- // 但是这个错误并不影响后续逻辑,所以这里对这个错误做了 catch
523
- }
524
- }
525
-
526
480
  const blocklet = await ensureAppPortsNotOccupied({ blocklet: blocklet1, componentDids, states, manager: this });
527
481
 
528
482
  const getHookFn =
@@ -579,9 +533,11 @@ class BlockletManager extends BaseBlockletManager {
579
533
 
580
534
  const error = Array.isArray(err) ? err[0] : err;
581
535
  logger.error('Failed to start blocklet', { error, did, title: blocklet1.meta.title });
582
- const description = `Start blocklet ${blocklet1.meta.title} failed with error: ${error.message}`;
536
+ const description = `${getComponentNamesWithVersion(blocklet1, componentDids)} start failed for ${
537
+ blocklet1.meta.title
538
+ }: ${error.message}`;
583
539
  this._createNotification(did, {
584
- title: 'Start Blocklet Failed',
540
+ title: '',
585
541
  description,
586
542
  entityType: 'blocklet',
587
543
  entityId: did,
@@ -590,7 +546,7 @@ class BlockletManager extends BaseBlockletManager {
590
546
 
591
547
  await this.deleteProcess({ did, componentDids });
592
548
  const res = await states.blocklet.setBlockletStatus(did, BlockletStatus.error, { componentDids });
593
- this.emit(BlockletEvents.startFailed, { ...res, error: { message: error.message } });
549
+ this.emit(BlockletEvents.startFailed, { ...res, componentDids, error: { message: error.message } });
594
550
  this.emit(BlockletEvents.statusChange, { ...res, error: { message: error.message } });
595
551
 
596
552
  if (throwOnError) {
@@ -647,7 +603,7 @@ class BlockletManager extends BaseBlockletManager {
647
603
  this.emit(BlockletEvents.statusChange, res);
648
604
 
649
605
  // send notification to wallet
650
- this.emit(BlockletEvents.stopped, res);
606
+ this.emit(BlockletEvents.stopped, { ...res, componentDids });
651
607
 
652
608
  this.emit(BlockletInternalEvents.componentsUpdated, {
653
609
  appDid: blocklet.appDid,
@@ -739,9 +695,12 @@ class BlockletManager extends BaseBlockletManager {
739
695
  const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped, { componentDids });
740
696
  this.emit(BlockletEvents.statusChange, state);
741
697
 
698
+ const description = `${getComponentNamesWithVersion(result, componentDids)} restart failed for ${
699
+ result.meta.title
700
+ }: ${err.message || 'queue exception'}`;
742
701
  this._createNotification(did, {
743
- title: 'Blocklet Restart Failed',
744
- description: `Blocklet ${result.meta.title} restart failed with error: ${err.message || 'queue exception'}`,
702
+ title: '',
703
+ description,
745
704
  entityType: 'blocklet',
746
705
  entityId: did,
747
706
  severity: 'error',
@@ -941,23 +900,33 @@ class BlockletManager extends BaseBlockletManager {
941
900
 
942
901
  await this._updateDependents(rootDid);
943
902
 
903
+ // support edge case
904
+ if (newBlocklet.children.length === 0) {
905
+ await states.blocklet.setBlockletStatus(newBlocklet.meta.did, BlockletStatus.stopped);
906
+ }
907
+
944
908
  this.emit(BlockletEvents.upgraded, { blocklet: newBlocklet, context: { ...context, createAuditLog: false } }); // trigger router refresh
945
909
 
946
910
  this._createNotification(newBlocklet.meta.did, {
947
- title: 'Component Deleted',
948
- description: `Component ${child.meta.title} of ${newBlocklet.meta.title} is successfully deleted.`,
911
+ title: '',
912
+ description: `${child.meta.title} is successfully deleted for ${newBlocklet.meta.title}.`,
949
913
  entityType: 'blocklet',
950
914
  entityId: newBlocklet.meta.did,
951
915
  severity: 'success',
952
916
  action: `/blocklets/${newBlocklet.meta.did}/components`,
953
917
  });
954
918
 
919
+ this.emit(BlockletEvents.componentRemoved, {
920
+ ...blocklet,
921
+ componentDids: [child.meta.did],
922
+ });
923
+
955
924
  this.emit(BlockletInternalEvents.componentsUpdated, {
956
925
  appDid: blocklet.appDid,
957
926
  components: getComponentsInternalInfo(newBlocklet),
958
927
  });
959
928
 
960
- return newBlocklet;
929
+ return { ...newBlocklet, deletedComponent: child };
961
930
  }
962
931
 
963
932
  async cancelDownload({ did: inputDid }) {
@@ -1346,6 +1315,8 @@ class BlockletManager extends BaseBlockletManager {
1346
1315
  return newState;
1347
1316
  }
1348
1317
 
1318
+ // -------------------------------------
1319
+
1349
1320
  // TODO: this method can be removed if title is not changed anymore
1350
1321
  async updateComponentTitle({ did, rootDid: inputRootDid, title }) {
1351
1322
  await titleSchema.validateAsync(title);
@@ -1527,7 +1498,7 @@ class BlockletManager extends BaseBlockletManager {
1527
1498
  addToUpdates(component.meta.did, status);
1528
1499
  logger.info('will sync status from pm2', { did, status, oldStatus, componentDid: component.meta.did });
1529
1500
  }
1530
- } catch {
1501
+ } catch (error) {
1531
1502
  if (
1532
1503
  ![
1533
1504
  BlockletStatus.added,
@@ -1536,11 +1507,21 @@ class BlockletManager extends BaseBlockletManager {
1536
1507
  BlockletStatus.installing,
1537
1508
  BlockletStatus.installed,
1538
1509
  BlockletStatus.upgrading,
1539
- ].includes(component.status)
1510
+ ].includes(component.status) &&
1511
+ (error.code !== 'BLOCKLET_PROCESS_404' ||
1512
+ ![BlockletStatus.stopped, BlockletStatus.error].includes(component.status))
1540
1513
  ) {
1514
+ const oldStatus = component.status;
1541
1515
  const status = BlockletStatus.stopped;
1542
1516
  component.status = status;
1543
1517
  addToUpdates(component.meta.did, status);
1518
+ logger.info('will sync status from pm2', {
1519
+ did,
1520
+ status,
1521
+ oldStatus,
1522
+ componentDid: component.meta.did,
1523
+ error: error.message,
1524
+ });
1544
1525
  }
1545
1526
  }
1546
1527
  },
@@ -1784,7 +1765,7 @@ class BlockletManager extends BaseBlockletManager {
1784
1765
  * @param {{
1785
1766
  * blocklet: {},
1786
1767
  * context: {},
1787
- * postAction: 'install' | 'upgrade',
1768
+ * postAction: 'install' | 'upgradeComponent' | 'installComponent',
1788
1769
  * oldBlocklet: {},
1789
1770
  * throwOnError: Error,
1790
1771
  * skipCheckStatusBeforeDownload: boolean,
@@ -1835,18 +1816,19 @@ class BlockletManager extends BaseBlockletManager {
1835
1816
  }
1836
1817
 
1837
1818
  // download bundle
1819
+ const childrenToDownload = (blocklet.children || []).filter((x) => {
1820
+ if (componentDids?.length) {
1821
+ return componentDids.includes(x.meta.did);
1822
+ }
1823
+ return x;
1824
+ });
1825
+
1838
1826
  try {
1839
- const blockletForDownload = {
1840
- ...blocklet,
1841
- children: (blocklet.children || []).filter((x) => {
1842
- if (componentDids?.length) {
1843
- return componentDids.includes(x.meta.did);
1844
- }
1845
- return x;
1846
- }),
1847
- };
1848
1827
  const { isCancelled } = await this._downloadBlocklet(
1849
- blockletForDownload,
1828
+ {
1829
+ ...blocklet,
1830
+ children: childrenToDownload,
1831
+ },
1850
1832
  Object.assign({}, context, { skipCheckIntegrity })
1851
1833
  );
1852
1834
 
@@ -1876,8 +1858,10 @@ class BlockletManager extends BaseBlockletManager {
1876
1858
  },
1877
1859
  });
1878
1860
  this._createNotification(did, {
1879
- title: 'Blocklet Download Failed',
1880
- description: `Blocklet ${title} download failed with error: ${err.message}`,
1861
+ title: '',
1862
+ description: `${childrenToDownload
1863
+ .map((x) => `${x.meta.title}@${x.meta.version}`)
1864
+ .join(', ')} download failed for ${title}: ${err.message}`,
1881
1865
  entityType: 'blocklet',
1882
1866
  entityId: did,
1883
1867
  severity: 'error',
@@ -1910,12 +1894,18 @@ class BlockletManager extends BaseBlockletManager {
1910
1894
  throw new Error('blocklet status changed durning download');
1911
1895
  }
1912
1896
 
1913
- if (postAction === 'install') {
1897
+ if (postAction === INSTALL_ACTIONS.INSTALL) {
1914
1898
  const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.installing, { componentDids });
1915
1899
  this.emit(BlockletEvents.statusChange, state);
1916
1900
  }
1917
1901
 
1918
- if (postAction === 'upgrade') {
1902
+ if (
1903
+ [
1904
+ INSTALL_ACTIONS.INSTALL_COMPONENT,
1905
+ INSTALL_ACTIONS.UPGRADE_COMPONENT,
1906
+ 'upgrade', // for backward compatibility
1907
+ ].includes(postAction)
1908
+ ) {
1919
1909
  const state = await states.blocklet.setBlockletStatus(did, BlockletStatus.upgrading, { componentDids });
1920
1910
  this.emit(BlockletEvents.statusChange, state);
1921
1911
  }
@@ -1929,14 +1919,32 @@ class BlockletManager extends BaseBlockletManager {
1929
1919
  // install
1930
1920
  try {
1931
1921
  // install blocklet
1932
- if (postAction === 'install') {
1922
+ if (postAction === INSTALL_ACTIONS.INSTALL) {
1933
1923
  await this._onInstall({ blocklet, componentDids, context, oldBlocklet });
1934
1924
  return;
1935
1925
  }
1936
1926
 
1937
1927
  // upgrade blocklet
1938
- if (postAction === 'upgrade') {
1939
- await this._onUpgrade({ oldBlocklet, componentDids, newBlocklet: blocklet, context });
1928
+ if (
1929
+ [
1930
+ INSTALL_ACTIONS.INSTALL_COMPONENT,
1931
+ INSTALL_ACTIONS.UPGRADE_COMPONENT,
1932
+ 'upgrade', // for backward compatibility
1933
+ ].includes(postAction)
1934
+ ) {
1935
+ logger.info('do upgrade blocklet', { did, version, postAction, componentDids });
1936
+
1937
+ try {
1938
+ await this._upgradeBlocklet({
1939
+ newBlocklet: blocklet,
1940
+ oldBlocklet,
1941
+ componentDids,
1942
+ context,
1943
+ action: postAction,
1944
+ });
1945
+ } catch (err) {
1946
+ logger.error('blocklet onUpgrade error', { error: err });
1947
+ }
1940
1948
  }
1941
1949
  } catch (error) {
1942
1950
  if (throwOnError) {
@@ -1974,22 +1982,6 @@ class BlockletManager extends BaseBlockletManager {
1974
1982
  }
1975
1983
  }
1976
1984
 
1977
- async _onUpgrade({ oldBlocklet, newBlocklet, componentDids, context }) {
1978
- const { version, did } = newBlocklet.meta;
1979
- logger.info('do upgrade blocklet', { did, version });
1980
-
1981
- try {
1982
- await this._upgradeBlocklet({
1983
- newBlocklet,
1984
- oldBlocklet,
1985
- componentDids,
1986
- context,
1987
- });
1988
- } catch (err) {
1989
- logger.error('blocklet onUpgrade error', { error: err });
1990
- }
1991
- }
1992
-
1993
1985
  async _onRestart({ did, componentDids, context }) {
1994
1986
  await this.stop({ did, componentDids, updateStatus: false }, context);
1995
1987
  await this.start({ did, componentDids }, context);
@@ -2018,7 +2010,7 @@ class BlockletManager extends BaseBlockletManager {
2018
2010
  });
2019
2011
 
2020
2012
  this.emit(BlockletEvents.statusChange, res);
2021
- this.emit(BlockletEvents.started, res);
2013
+ this.emit(BlockletEvents.started, { ...res, componentDids });
2022
2014
  logger.info('blocklet healthy', { did, name, time: Date.now() - startedAt });
2023
2015
  } catch (error) {
2024
2016
  const status = await states.blocklet.getBlockletStatus(did);
@@ -2032,15 +2024,19 @@ class BlockletManager extends BaseBlockletManager {
2032
2024
  await this.deleteProcess({ did, componentDids }, context);
2033
2025
  const doc = await states.blocklet.setBlockletStatus(did, BlockletStatus.error, { componentDids });
2034
2026
 
2027
+ const description = `${getComponentNamesWithVersion(blocklet, componentDids)} start failed for ${title}: ${
2028
+ error.message
2029
+ }`;
2030
+
2035
2031
  this._createNotification(did, {
2036
- title: 'Blocklet Start Failed',
2037
- description: `Blocklet ${title} start failed: ${error.message}`,
2032
+ title: '',
2033
+ description,
2038
2034
  entityType: 'blocklet',
2039
2035
  entityId: did,
2040
2036
  severity: 'error',
2041
2037
  });
2042
2038
 
2043
- this.emit(BlockletEvents.startFailed, { meta, error });
2039
+ this.emit(BlockletEvents.startFailed, { ...doc, componentDids, error: { message: error.message } });
2044
2040
  this.emit(BlockletEvents.statusChange, { ...doc, error });
2045
2041
 
2046
2042
  if (throwOnError) {
@@ -2516,7 +2512,7 @@ class BlockletManager extends BaseBlockletManager {
2516
2512
  const { name, version } = meta;
2517
2513
  logger.error('failed to install blocklet', { name, did, version, error: err });
2518
2514
  try {
2519
- await this._rollback('install', did, oldBlocklet);
2515
+ await this._rollback(INSTALL_ACTIONS.INSTALL, did, oldBlocklet);
2520
2516
  this.emit(BlockletEvents.installFailed, {
2521
2517
  meta: { did },
2522
2518
  error: {
@@ -2538,22 +2534,14 @@ class BlockletManager extends BaseBlockletManager {
2538
2534
  }
2539
2535
  }
2540
2536
 
2541
- async _upgradeBlocklet({ newBlocklet, oldBlocklet, componentDids, context = {} }) {
2537
+ async _upgradeBlocklet({ newBlocklet, oldBlocklet, componentDids, action, context = {} }) {
2542
2538
  const { meta, source, deployedFrom, children } = newBlocklet;
2543
2539
  const { did, version, name, title } = meta;
2544
2540
 
2545
- // ids
2546
- const { skippedProcessIds, installedComponentNames } = getComponentChangedInfoForUpgrade({
2547
- newBlocklet,
2548
- oldBlocklet,
2549
- context,
2550
- });
2551
- context.skippedProcessIds = skippedProcessIds;
2552
-
2553
2541
  try {
2554
2542
  // delete old process
2555
2543
  try {
2556
- await this.deleteProcess({ did }, context);
2544
+ await this.deleteProcess({ did, componentDids }, context);
2557
2545
  logger.info('delete blocklet process for upgrading', { did, name });
2558
2546
  } catch (err) {
2559
2547
  logger.error('delete blocklet process for upgrading', { did, name, error: err });
@@ -2622,10 +2610,22 @@ class BlockletManager extends BaseBlockletManager {
2622
2610
  this.refreshListCache();
2623
2611
 
2624
2612
  try {
2625
- this.emit(BlockletEvents.upgraded, { blocklet, context });
2613
+ this.emit(BlockletEvents.upgraded, { blocklet, context }); // trigger router refresh
2614
+
2615
+ const notificationEvent =
2616
+ action === INSTALL_ACTIONS.INSTALL_COMPONENT
2617
+ ? BlockletEvents.componentInstalled
2618
+ : BlockletEvents.componentUpgraded;
2619
+ const actionName = action === INSTALL_ACTIONS.INSTALL_COMPONENT ? 'installed' : 'upgraded';
2620
+
2621
+ this.emit(notificationEvent, { ...blocklet, componentDids, context });
2622
+
2626
2623
  this._createNotification(did, {
2627
- title: 'Blocklet Upgrade Success',
2628
- description: `Blocklet ${title} upgrade successfully. (Component: ${installedComponentNames.join(', ')})`,
2624
+ title: '',
2625
+ description: `${getComponentNamesWithVersion(
2626
+ newBlocklet,
2627
+ componentDids
2628
+ )} is ${actionName} successfully for ${title}`,
2629
2629
  action: `/blocklets/${did}/overview`,
2630
2630
  entityType: 'blocklet',
2631
2631
  entityId: did,
@@ -2647,21 +2647,27 @@ class BlockletManager extends BaseBlockletManager {
2647
2647
 
2648
2648
  return blocklet;
2649
2649
  } catch (err) {
2650
- const b = await this._rollback('upgrade', did, oldBlocklet);
2650
+ const b = await this._rollback(action, did, oldBlocklet);
2651
2651
  logger.error('failed to upgrade blocklet', { did, version, name, error: err });
2652
2652
 
2653
2653
  this.emit(BlockletEvents.updated, b);
2654
2654
 
2655
- this.emit(BlockletEvents.upgradeFailed, {
2656
- blocklet: { ...oldBlocklet, error: { message: err.message } },
2655
+ const actionName = action === INSTALL_ACTIONS.INSTALL_COMPONENT ? 'install' : 'upgrade';
2656
+ const notificationEvent =
2657
+ action === INSTALL_ACTIONS.INSTALL_COMPONENT
2658
+ ? BlockletEvents.componentInstallFailed
2659
+ : BlockletEvents.componentUpgradeFailed;
2660
+
2661
+ this.emit(notificationEvent, {
2662
+ blocklet: { ...oldBlocklet, componentDids, error: { message: err.message } },
2657
2663
  context,
2658
2664
  });
2659
2665
 
2660
2666
  this._createNotification(did, {
2661
- title: 'Blocklet Upgrade Failed',
2662
- description: `Blocklet ${title} upgrade failed with error: ${
2667
+ title: '',
2668
+ description: `${getComponentNamesWithVersion(newBlocklet, componentDids)} ${actionName} failed for ${title}: ${
2663
2669
  err.message
2664
- }. (Component: ${installedComponentNames.join(', ')})`,
2670
+ }.`,
2665
2671
  entityType: 'blocklet',
2666
2672
  entityId: did,
2667
2673
  severity: 'error',
@@ -2735,12 +2741,12 @@ class BlockletManager extends BaseBlockletManager {
2735
2741
  }
2736
2742
 
2737
2743
  /**
2738
- * @param {string} action install, upgrade, downgrade
2744
+ * @param {string} action install, upgrade, installComponent, upgradeComponent
2739
2745
  * @param {string} did
2740
2746
  * @param {object} oldBlocklet
2741
2747
  */
2742
2748
  async _rollback(action, did, oldBlocklet) {
2743
- if (action === 'install') {
2749
+ if (action === INSTALL_ACTIONS.INSTALL) {
2744
2750
  const extraState = oldBlocklet?.extraState;
2745
2751
 
2746
2752
  // rollback blocklet extra state
@@ -2754,7 +2760,13 @@ class BlockletManager extends BaseBlockletManager {
2754
2760
  return this._deleteBlocklet({ did, keepData: true });
2755
2761
  }
2756
2762
 
2757
- if (['upgrade', 'downgrade'].includes(action)) {
2763
+ if (
2764
+ [
2765
+ INSTALL_ACTIONS.INSTALL_COMPONENT,
2766
+ INSTALL_ACTIONS.UPGRADE_COMPONENT,
2767
+ 'upgrade', // for backward compatibility
2768
+ ].includes(action)
2769
+ ) {
2758
2770
  const { extraState, ...blocklet } = oldBlocklet;
2759
2771
  // rollback blocklet state
2760
2772
  const result = await states.blocklet.updateBlocklet(did, blocklet);
@@ -2945,7 +2957,19 @@ class BlockletManager extends BaseBlockletManager {
2945
2957
  return;
2946
2958
  }
2947
2959
 
2948
- await states.notification.create(notification);
2960
+ let blockletUrl;
2961
+ try {
2962
+ const blocklet = await this.getBlocklet(did);
2963
+ if (blocklet) {
2964
+ const urls = blocklet.site?.domainAliases || [];
2965
+ const customUrl = urls.find((x) => !x.isProtected)?.value;
2966
+ blockletUrl = `http://${customUrl || getDidDomainForBlocklet({ appPid: blocklet.appPid })}`;
2967
+ }
2968
+ } catch (error) {
2969
+ logger.error('[_createNotification] get blocklet url failed', { error });
2970
+ }
2971
+
2972
+ await states.notification.create({ ...notification, blockletUrl });
2949
2973
  } catch (error) {
2950
2974
  logger.error('create notification failed', { error });
2951
2975
  }
@@ -3259,7 +3283,7 @@ class BlockletManager extends BaseBlockletManager {
3259
3283
  );
3260
3284
  }
3261
3285
 
3262
- async _restoreFromDisk(input) {
3286
+ async _restoreFromDisk(input, context) {
3263
3287
  if (input.delay) {
3264
3288
  await sleep(input.delay);
3265
3289
  }
@@ -3283,6 +3307,7 @@ class BlockletManager extends BaseBlockletManager {
3283
3307
  states,
3284
3308
  move: true,
3285
3309
  sync: false, // use queue to download and install application
3310
+ context,
3286
3311
  });
3287
3312
  removeRestoreDir();
3288
3313
  } catch (error) {
@@ -3366,4 +3391,151 @@ class BlockletManager extends BaseBlockletManager {
3366
3391
  }
3367
3392
  }
3368
3393
 
3394
+ class FederatedBlockletManager extends DiskBlockletManager {
3395
+ /**
3396
+ * Joins federated login.
3397
+ * @param {{appUrl: string, did: string;}} options - The federated login appUrl.
3398
+ * @returns {Promise<any>} The data received from the appUrl.
3399
+ */
3400
+ async joinFederatedLogin({ appUrl, did }) {
3401
+ const url = new URL(appUrl);
3402
+ url.pathname = `${WELLKNOWN_SERVICE_PATH_PREFIX}/api/federated/join`;
3403
+
3404
+ const blocklet = await this.getBlocklet(did);
3405
+ const nodeInfo = await states.node.read();
3406
+ const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
3407
+ const { permanentWallet, wallet } = blockletInfo;
3408
+ const memberSite = {
3409
+ appId: wallet.address,
3410
+ appPid: permanentWallet.address,
3411
+ migratedFrom: blocklet.migratedFrom || [],
3412
+ appName: blockletInfo.name,
3413
+ appDescription: blockletInfo.description,
3414
+ appUrl: blockletInfo.appUrl,
3415
+ appLogo:
3416
+ blocklet.environmentObj.BLOCKLET_APP_LOGO ||
3417
+ normalizePathPrefix(`${WELLKNOWN_SERVICE_PATH_PREFIX}/blocklet/logo`) ||
3418
+ '/',
3419
+ appLogoRect: blocklet.environmentObj.BLOCKLET_APP_LOGO_RECT,
3420
+ did: blockletInfo.did,
3421
+ pk: permanentWallet.publicKey,
3422
+ serverId: nodeInfo.did,
3423
+ serverVersion: nodeInfo.version,
3424
+ version: blockletInfo.version,
3425
+ };
3426
+
3427
+ const { data } = await request.post(url.href, {
3428
+ // 初次申请时,member 不在站点群中,不需要对数据进行加密
3429
+ site: memberSite,
3430
+ });
3431
+ await states.blockletExtras.setSettings(blocklet.meta.did, {
3432
+ federated: {
3433
+ config: {
3434
+ appId: blocklet.appDid,
3435
+ appPid: blocklet.appPid || blocklet.appDid,
3436
+ isMaster: false,
3437
+ },
3438
+ sites: data.sites,
3439
+ },
3440
+ });
3441
+
3442
+ const newState = await this.getBlocklet(did);
3443
+ this.emit(BlockletEvents.updated, newState);
3444
+ return newState;
3445
+ }
3446
+
3447
+ async setFederated({ did, config }) {
3448
+ await states.blockletExtras.setSettings(did, { federated: config });
3449
+
3450
+ const newState = await this.getBlocklet(did);
3451
+ this.emit(BlockletEvents.updated, newState);
3452
+ return newState;
3453
+ }
3454
+
3455
+ async configFederated({ did, autoLogin = false }) {
3456
+ const blocklet = await this.getBlocklet(did);
3457
+ const federated = cloneDeep(blocklet.settings.federated || {});
3458
+ federated.config.autoLogin = autoLogin;
3459
+ await states.blockletExtras.setSettings(did, { federated });
3460
+
3461
+ const newState = await this.getBlocklet(did);
3462
+ this.emit(BlockletEvents.updated, newState);
3463
+ return newState;
3464
+ }
3465
+
3466
+ async auditFederatedLogin({ appId, did, status }) {
3467
+ const blocklet = await this.getBlocklet(did);
3468
+
3469
+ const federated = cloneDeep(blocklet.settings.federated || {});
3470
+ const memberSite = federated.sites.find((item) => item.appId === appId);
3471
+ memberSite.status = status;
3472
+ if ([null, undefined].includes(federated.config.isMaster)) {
3473
+ const masterSite = federated.sites.find((item) => item.appId === blocklet.meta.did);
3474
+
3475
+ masterSite.isMaster = true;
3476
+ federated.config.isMaster = true;
3477
+ }
3478
+ // 有审批操作的一方,自动成为 master
3479
+ const newState = await this.setFederated({
3480
+ did: blocklet.meta.did,
3481
+ config: federated,
3482
+ });
3483
+ const nodeInfo = await states.node.read();
3484
+ const { permanentWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
3485
+ let delegation;
3486
+ let roles = [];
3487
+ if (status === 'approved') {
3488
+ delegation = signV2(permanentWallet.address, permanentWallet.secretKey, {
3489
+ agentDid: `did:abt:${memberSite.appPid}`,
3490
+ permissions: [
3491
+ {
3492
+ role: 'DIDConnectAgent',
3493
+ claims: [
3494
+ 'authPrincipal',
3495
+ 'profile',
3496
+ 'signature',
3497
+ 'prepareTx',
3498
+ 'agreement',
3499
+ 'verifiableCredential',
3500
+ 'asset',
3501
+ // 'keyPair',
3502
+ // 'encryptionKey',
3503
+ ],
3504
+ },
3505
+ ],
3506
+ exp: Math.floor(new Date().getTime() / 1000) + 86400 * 365 * 100, // valid for 100 year
3507
+ });
3508
+ roles = await this.teamManager.getRoles(blocklet.meta.did);
3509
+ }
3510
+
3511
+ await request.post(`${memberSite.appUrl}/${WELLKNOWN_SERVICE_PATH_PREFIX}/api/federated/audit-res`, {
3512
+ signer: permanentWallet.address,
3513
+ data: signV2(permanentWallet.address, permanentWallet.secretKey, {
3514
+ appId: blocklet.meta.did,
3515
+ status,
3516
+ delegation,
3517
+ roles: roles.map((item) => pick(item, ['name', 'title', 'description'])),
3518
+ }),
3519
+ });
3520
+ const waitingList = federated.sites
3521
+ .filter((item) => item.appId !== federated.config.appId)
3522
+ .map((item) => {
3523
+ return limitSync(() =>
3524
+ pRetry(
3525
+ () =>
3526
+ request.post(`${item.appUrl}/${WELLKNOWN_SERVICE_PATH_PREFIX}/api/federated/sync`, {
3527
+ signer: permanentWallet.address,
3528
+ data: signV2(permanentWallet.address, permanentWallet.secretKey, { sites: federated.sites }),
3529
+ }),
3530
+ { retries: 3 }
3531
+ )
3532
+ );
3533
+ });
3534
+ await Promise.all(waitingList);
3535
+ return newState;
3536
+ }
3537
+ }
3538
+
3539
+ class BlockletManager extends FederatedBlockletManager {}
3540
+
3369
3541
  module.exports = BlockletManager;