@abtnode/core 1.16.11-beta-0ae58a71 → 1.16.11-beta-f74663ac

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.
Files changed (53) hide show
  1. package/lib/api/node.js +1 -1
  2. package/lib/api/team.js +5 -5
  3. package/lib/blocklet/downloader/blocklet-downloader.js +34 -19
  4. package/lib/blocklet/downloader/bundle-downloader.js +2 -2
  5. package/lib/blocklet/hooks.js +1 -1
  6. package/lib/blocklet/manager/base.js +9 -9
  7. package/lib/blocklet/manager/disk.js +286 -172
  8. package/lib/blocklet/manager/helper/install-application-from-backup.js +1 -1
  9. package/lib/blocklet/manager/helper/install-application-from-general.js +18 -3
  10. package/lib/blocklet/manager/helper/install-component-from-dev.js +0 -1
  11. package/lib/blocklet/manager/helper/install-component-from-upload.js +8 -4
  12. package/lib/blocklet/manager/helper/install-component-from-url.js +4 -2
  13. package/lib/blocklet/manager/helper/upgrade-components.js +7 -7
  14. package/lib/blocklet/manager/pm2-events.js +3 -3
  15. package/lib/blocklet/migration.js +2 -2
  16. package/lib/blocklet/storage/backup/base.js +1 -0
  17. package/lib/blocklet/storage/backup/blocklet-extras.js +1 -1
  18. package/lib/blocklet/storage/backup/blocklet.js +1 -1
  19. package/lib/blocklet/storage/backup/disk.js +1 -0
  20. package/lib/blocklet/storage/restore/blocklet-extras.js +1 -1
  21. package/lib/blocklet/storage/restore/blocklet.js +1 -1
  22. package/lib/blocklet/storage/restore/disk.js +1 -1
  23. package/lib/blocklet/storage/restore/spaces.js +1 -1
  24. package/lib/cert.js +5 -5
  25. package/lib/crons/rotate-pm2-logs/index.js +1 -1
  26. package/lib/event.js +11 -1
  27. package/lib/index.js +8 -2
  28. package/lib/migrations/1.16.11-component-status.js +22 -0
  29. package/lib/migrations/index.js +3 -3
  30. package/lib/monitor/blocklet-runtime-monitor.js +7 -2
  31. package/lib/processes/updater.js +1 -1
  32. package/lib/router/helper.js +5 -5
  33. package/lib/states/audit-log.js +1 -1
  34. package/lib/states/backup.js +5 -4
  35. package/lib/states/blocklet-extras.js +11 -1
  36. package/lib/states/blocklet.js +46 -39
  37. package/lib/states/cache.js +1 -1
  38. package/lib/states/node.js +6 -2
  39. package/lib/states/notification.js +1 -1
  40. package/lib/states/routing-snapshot.js +1 -1
  41. package/lib/states/site.js +1 -1
  42. package/lib/states/user.js +12 -6
  43. package/lib/team/manager.js +6 -6
  44. package/lib/util/blocklet.js +130 -118
  45. package/lib/util/launcher.js +238 -0
  46. package/lib/util/reset-node.js +19 -16
  47. package/lib/util/rpc.js +1 -1
  48. package/lib/util/store.js +1 -1
  49. package/lib/validators/space-gateway.js +3 -0
  50. package/lib/validators/util.js +3 -0
  51. package/lib/webhook/sender/api/index.js +1 -1
  52. package/lib/webhook/sender/slack/index.js +1 -1
  53. package/package.json +17 -17
@@ -3,10 +3,8 @@
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const dayjs = require('dayjs');
6
- const joinURL = require('url-join');
7
6
  const shelljs = require('shelljs');
8
7
  const os = require('os');
9
- const pRetry = require('p-retry');
10
8
  const tar = require('tar');
11
9
  const get = require('lodash/get');
12
10
  const uniq = require('lodash/uniq');
@@ -19,19 +17,16 @@ const diff = require('deep-diff');
19
17
  const createArchive = require('archiver');
20
18
  const isUrl = require('is-url');
21
19
  const semver = require('semver');
22
- const axios = require('@abtnode/util/lib/axios');
23
- const { stableStringify } = require('@arcblock/vc');
24
20
  const { chainInfo: chainInfoSchema } = require('@arcblock/did-auth/lib/schema');
25
21
 
26
22
  const { fromSecretKey } = require('@ocap/wallet');
27
- const { toHex, toBase58, isHex, toDid, toAddress } = require('@ocap/util');
23
+ const { toHex, isHex, toDid, toAddress } = require('@ocap/util');
28
24
  const { isValid: isValidDid, isEthereumDid } = require('@arcblock/did');
29
25
  const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
30
26
  const pm2 = require('@abtnode/util/lib/async-pm2');
31
27
  const sleep = require('@abtnode/util/lib/sleep');
32
28
  const getPm2ProcessInfo = require('@abtnode/util/lib/get-pm2-process-info');
33
29
  const killProcessOccupiedPorts = require('@abtnode/util/lib/kill-process-occupied-ports');
34
- const getNodeWallet = require('@abtnode/util/lib/get-app-wallet');
35
30
  const { formatEnv } = require('@abtnode/util/lib/security');
36
31
  const ensureEndpointHealthy = require('@abtnode/util/lib/ensure-endpoint-healthy');
37
32
  const CustomError = require('@abtnode/util/lib/custom-error');
@@ -79,6 +74,7 @@ const {
79
74
  findWebInterface,
80
75
  forEachBlockletSync,
81
76
  forEachChildSync,
77
+ forEachComponentV2Sync,
82
78
  isComponentBlocklet,
83
79
  getSharedConfigObj,
84
80
  getComponentName,
@@ -86,6 +82,8 @@ const {
86
82
  getBlockletAppIdList,
87
83
  isBeforeInstalled,
88
84
  getChainInfo,
85
+ isInProgress,
86
+ isRunning,
89
87
  } = require('@blocklet/meta/lib/util');
90
88
  const toBlockletDid = require('@blocklet/meta/lib/did');
91
89
  const { titleSchema, descriptionSchema, logoSchema } = require('@blocklet/meta/lib/schema');
@@ -100,7 +98,7 @@ const { validate: validateEngine, get: getEngine } = require('../blocklet/manage
100
98
 
101
99
  const isRequirementsSatisfied = require('./requirement');
102
100
  const { getDidDomainForBlocklet } = require('./get-domain-for-blocklet');
103
- const { expandBundle, findInterfacePortByName, prettyURL, getNFTState, templateReplace } = require('./index');
101
+ const { expandBundle, findInterfacePortByName, prettyURL, templateReplace } = require('./index');
104
102
  const externalIpUtil = require('./get-accessible-external-node-ip');
105
103
  const { getIpDnsDomainForBlocklet } = require('./get-domain-for-blocklet');
106
104
  const { replaceDomainSlot } = require('./index');
@@ -373,8 +371,9 @@ const getComponentSystemEnvironments = (blocklet) => {
373
371
  }
374
372
 
375
373
  return {
376
- BLOCKLET_REAL_DID: blocklet.env.id,
374
+ BLOCKLET_REAL_DID: blocklet.env.id, // <appDid>/componentDid> e.g. xxxxx/xxxxx
377
375
  BLOCKLET_REAL_NAME: blocklet.env.name,
376
+ BLOCKLET_COMPONENT_DID: blocklet.meta.did, // component meta did e.g. xxxxxx
378
377
  BLOCKLET_DATA_DIR: blocklet.env.dataDir,
379
378
  BLOCKLET_LOG_DIR: blocklet.env.logsDir,
380
379
  BLOCKLET_CACHE_DIR: blocklet.env.cacheDir,
@@ -506,7 +505,15 @@ const getHealthyCheckTimeout = (blocklet, { checkHealthImmediately } = {}) => {
506
505
  */
507
506
  const startBlockletProcess = async (
508
507
  blocklet,
509
- { preStart = noop, postStart = noopAsync, nodeEnvironments, nodeInfo, e2eMode, skippedProcessIds = [] } = {}
508
+ {
509
+ preStart = noop,
510
+ postStart = noopAsync,
511
+ nodeEnvironments,
512
+ nodeInfo,
513
+ e2eMode,
514
+ skippedProcessIds = [],
515
+ componentDids,
516
+ } = {}
510
517
  ) => {
511
518
  if (!blocklet) {
512
519
  throw new Error('blocklet should not be empty');
@@ -522,7 +529,12 @@ const startBlockletProcess = async (
522
529
  const { appMain, processId, appCwd, logsDir } = b.env;
523
530
 
524
531
  if (skippedProcessIds.includes(processId)) {
525
- logger.info(`skip start process ${processId}`);
532
+ logger.info(`skip start skipped process ${processId}`);
533
+ return;
534
+ }
535
+
536
+ if (shouldSkipComponent(b.meta.did, componentDids)) {
537
+ logger.info(`skip start process not selected: ${b.meta.did}`, { processId });
526
538
  return;
527
539
  }
528
540
 
@@ -623,31 +635,15 @@ const startBlockletProcess = async (
623
635
  * Stop all precesses of a blocklet
624
636
  * @param {*} blocklet should contain env props
625
637
  */
626
- const stopBlockletProcess = async (blocklet, { preStop = noop, skippedProcessIds = [] } = {}) => {
627
- await forEachBlocklet(
628
- blocklet,
629
- async (b, { ancestors }) => {
630
- if (b.meta?.group === BlockletGroup.gateway) {
631
- return;
632
- }
633
-
634
- if (skippedProcessIds.includes(b.env.processId)) {
635
- logger.info(`skip stop process ${b.env.processId}`);
636
- return;
637
- }
638
-
639
- await preStop(b, { ancestors });
640
- await deleteProcess(b.env.processId);
641
- },
642
- { parallel: true }
643
- );
638
+ const stopBlockletProcess = (blocklet, { preStop = noop, skippedProcessIds = [], componentDids } = {}) => {
639
+ return deleteBlockletProcess(blocklet, { preDelete: preStop, skippedProcessIds, componentDids });
644
640
  };
645
641
 
646
642
  /**
647
643
  * Delete all precesses of a blocklet
648
644
  * @param {*} blocklet should contain env props
649
645
  */
650
- const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProcessIds = [] } = {}) => {
646
+ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProcessIds = [], componentDids } = {}) => {
651
647
  await forEachBlocklet(
652
648
  blocklet,
653
649
  async (b, { ancestors }) => {
@@ -657,7 +653,12 @@ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProces
657
653
  }
658
654
 
659
655
  if (skippedProcessIds.includes(b.env.processId)) {
660
- logger.info(`skip delete process ${b.env.processId}`);
656
+ logger.info(`skip delete skipped process ${b.env.processId}`);
657
+ return;
658
+ }
659
+
660
+ if (shouldSkipComponent(b.meta?.did, componentDids)) {
661
+ logger.info(`skip delete process not selected: ${b.meta.did}`, { processId: b.env.processId });
661
662
  return;
662
663
  }
663
664
 
@@ -672,52 +673,25 @@ const deleteBlockletProcess = async (blocklet, { preDelete = noop, skippedProces
672
673
  * Reload all precesses of a blocklet
673
674
  * @param {*} blocklet should contain env props
674
675
  */
675
- const reloadBlockletProcess = async (blocklet) =>
676
+ const reloadBlockletProcess = (blocklet, { componentDids } = {}) =>
676
677
  forEachBlocklet(
677
678
  blocklet,
678
679
  async (b) => {
679
680
  if (b.meta.group === BlockletGroup.gateway) {
680
681
  return;
681
682
  }
683
+
684
+ if (shouldSkipComponent(b.meta.did, componentDids)) {
685
+ logger.info(`skip reload process not selected: ${b.meta.did}`, { processId: b.env.processId });
686
+ return;
687
+ }
688
+
682
689
  logger.info('reload process', { processId: b.env.processId });
683
690
  await reloadProcess(b.env.processId);
684
691
  },
685
692
  { parallel: false }
686
693
  );
687
694
 
688
- const getBlockletStatusFromProcess = async (blocklet) => {
689
- const tasks = [];
690
- await forEachBlocklet(blocklet, (b) => {
691
- if (b.meta.group !== BlockletGroup.gateway) {
692
- tasks.push(getProcessState(b.env.processId));
693
- }
694
- });
695
-
696
- const list = await Promise.all(tasks);
697
-
698
- if (!list.length) {
699
- return blocklet.status;
700
- }
701
-
702
- return getRootBlockletStatus(list);
703
- };
704
-
705
- /**
706
- * @param {Array<BlockletStatus>} statusList
707
- */
708
- const getRootBlockletStatus = (statusList = []) => {
709
- const noRunningStatus = statusList.find((x) => x !== BlockletStatus.running);
710
- if (noRunningStatus) {
711
- if (statusList.some((x) => x === BlockletStatus.error)) {
712
- return BlockletStatus.error;
713
- }
714
-
715
- return noRunningStatus;
716
- }
717
-
718
- return BlockletStatus.running;
719
- };
720
-
721
695
  /**
722
696
  * @param {*} processId
723
697
  * @returns {BlockletStatus}
@@ -740,7 +714,7 @@ const getProcessInfo = (processId, { throwOnNotExist = true } = {}) =>
740
714
 
741
715
  const deleteProcess = (processId) =>
742
716
  new Promise((resolve, reject) => {
743
- pm2.delete(processId, async (err) => {
717
+ pm2.delete(processId, (err) => {
744
718
  if (isUsefulError(err)) {
745
719
  logger.error('blocklet process delete failed', { error: err });
746
720
  return reject(err);
@@ -751,7 +725,7 @@ const deleteProcess = (processId) =>
751
725
 
752
726
  const reloadProcess = (processId) =>
753
727
  new Promise((resolve, reject) => {
754
- pm2.reload(processId, async (err) => {
728
+ pm2.reload(processId, (err) => {
755
729
  if (err) {
756
730
  if (isUsefulError(err)) {
757
731
  logger.error('blocklet reload failed', { error: err });
@@ -885,7 +859,7 @@ const parseComponents = async (component, context = {}) => {
885
859
  };
886
860
 
887
861
  const validateBlocklet = (blocklet) =>
888
- forEachBlocklet(blocklet, async (b) => {
862
+ forEachBlocklet(blocklet, (b) => {
889
863
  isRequirementsSatisfied(b.meta.requirements);
890
864
  validateEngine(getBlockletEngineNameByPlatform(b.meta));
891
865
  });
@@ -905,19 +879,28 @@ const validateBlockletChainInfo = (blocklet) => {
905
879
  return chainInfo;
906
880
  };
907
881
 
908
- const checkBlockletProcessHealthy = async (blocklet, { minConsecutiveTime, timeout } = {}) => {
882
+ const checkBlockletProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, componentDids } = {}) => {
909
883
  await forEachBlocklet(
910
884
  blocklet,
911
885
  async (b) => {
912
- if (b.meta.group !== BlockletGroup.gateway) {
913
- await _checkProcessHealthy(b, { minConsecutiveTime, timeout });
886
+ if (b.meta.group === BlockletGroup.gateway) {
887
+ return;
914
888
  }
889
+
890
+ if (shouldSkipComponent(b.meta.did, componentDids)) {
891
+ logger.info('skip check component healthy not selected', { id: b.env.id, processId: b.env.processId });
892
+ return;
893
+ }
894
+
895
+ const logToTerminal = [blocklet.mode, b.mode].includes(BLOCKLET_MODES.DEVELOPMENT);
896
+
897
+ await _checkProcessHealthy(b, { minConsecutiveTime, timeout, logToTerminal });
915
898
  },
916
899
  { parallel: true }
917
900
  );
918
901
  };
919
902
 
920
- const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout }) => {
903
+ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout, logToTerminal }) => {
921
904
  const { meta, ports, env } = blocklet;
922
905
  const { name } = meta;
923
906
  const { processId } = env;
@@ -953,6 +936,14 @@ const _checkProcessHealthy = async (blocklet, { minConsecutiveTime, timeout }) =
953
936
  }
954
937
 
955
938
  const port = findInterfacePortByName({ meta, ports }, webInterface.name);
939
+ if (logToTerminal) {
940
+ // eslint-disable-next-line no-console
941
+ console.log(
942
+ `Checking endpoint healthy for ${meta.title}, port: ${port}, minConsecutiveTime: ${
943
+ minConsecutiveTime / 1000
944
+ }s, timeout: ${timeout / 1000}s`
945
+ );
946
+ }
956
947
  try {
957
948
  await ensureEndpointHealthy({
958
949
  port,
@@ -1538,51 +1529,6 @@ const getBlockletURLForLauncher = async ({ blocklet, nodeInfo }) => {
1538
1529
  const didDomain = getDidDomainForBlocklet({ appPid: blocklet.appPid, didDomain: nodeInfo.didDomain });
1539
1530
  return `https://${didDomain}`;
1540
1531
  };
1541
-
1542
- const consumeServerlessNFT = async ({ nftId, nodeInfo, blocklet }) => {
1543
- try {
1544
- const state = await getNFTState(blocklet.controller.chainHost, nftId);
1545
- if (!state) {
1546
- throw new Error(`get nft state failed, chainHost: ${blocklet.controller.chainHost}, nftId: ${nftId}`);
1547
- }
1548
-
1549
- const appURL = await getBlockletURLForLauncher({ blocklet, nodeInfo });
1550
-
1551
- logger.info('app url when consuming nft', { appURL, nftId });
1552
-
1553
- const body = { nftId, appURL };
1554
-
1555
- const { launcherUrl } = state.data.value;
1556
-
1557
- const wallet = getNodeWallet(nodeInfo.sk);
1558
- const func = async () => {
1559
- const { data } = await axios.post(joinURL(launcherUrl, '/api/serverless/consume'), body, {
1560
- headers: {
1561
- 'x-sig': toBase58(wallet.sign(stableStringify(body))),
1562
- },
1563
- });
1564
-
1565
- return data;
1566
- };
1567
-
1568
- const delay = 10 * 1000;
1569
- const result = await pRetry(func, {
1570
- retries: 3,
1571
- minTimeout: delay,
1572
- maxTimeout: delay,
1573
- onFailedAttempt: (error) => {
1574
- logger.error(`attempt consume nft ${nftId} failed`, { error });
1575
- },
1576
- });
1577
-
1578
- logger.info('consume serverless nft success', { nftId, hash: result.hash });
1579
- } catch (error) {
1580
- logger.error('consume serverless nft failed', { nftId, error });
1581
-
1582
- throw new Error(`consume nft ${nftId} failed`);
1583
- }
1584
- };
1585
-
1586
1532
  const createDataArchive = (dataDir, fileName) => {
1587
1533
  const zipPath = path.join(os.tmpdir(), fileName);
1588
1534
  if (fs.existsSync(zipPath)) {
@@ -1952,13 +1898,77 @@ const getBlockletDidDomainList = (blocklet, nodeInfo) => {
1952
1898
  return domainAliases;
1953
1899
  };
1954
1900
 
1901
+ const getBlockletStatus = (blocklet) => {
1902
+ const fallbackStatus = BlockletStatus.stopped;
1903
+
1904
+ if (!blocklet) {
1905
+ return fallbackStatus;
1906
+ }
1907
+
1908
+ if (!blocklet.children?.length) {
1909
+ if (blocklet.meta?.group === BlockletGroup.gateway) {
1910
+ return blocklet.status;
1911
+ }
1912
+
1913
+ if (blocklet.status === BlockletStatus.added) {
1914
+ return BlockletStatus.added;
1915
+ }
1916
+
1917
+ // for backward compatibility
1918
+ if (!blocklet.structVersion) {
1919
+ return blocklet.status;
1920
+ }
1921
+
1922
+ return fallbackStatus;
1923
+ }
1924
+
1925
+ let inProgressStatus;
1926
+ let runningStatus;
1927
+ let status;
1928
+
1929
+ forEachComponentV2Sync(blocklet, (component) => {
1930
+ if (component.meta?.group === BlockletGroup.gateway) {
1931
+ return;
1932
+ }
1933
+
1934
+ if (isInProgress(component.status)) {
1935
+ if (!inProgressStatus) {
1936
+ inProgressStatus = component.status;
1937
+ }
1938
+ return;
1939
+ }
1940
+
1941
+ if (isRunning(component.status)) {
1942
+ runningStatus = component.status;
1943
+ return;
1944
+ }
1945
+
1946
+ status = component.status;
1947
+ });
1948
+
1949
+ return inProgressStatus || runningStatus || status;
1950
+ };
1951
+
1952
+ const shouldSkipComponent = (componentDid, whiteList) => {
1953
+ if (!whiteList || !Array.isArray(whiteList)) {
1954
+ return false;
1955
+ }
1956
+
1957
+ const arr = whiteList.filter(Boolean);
1958
+
1959
+ if (!arr.length) {
1960
+ return false;
1961
+ }
1962
+
1963
+ return !arr.includes(componentDid);
1964
+ };
1965
+
1955
1966
  const exceedRedemptionPeriod = (expirationDate) => {
1956
1967
  return dayjs().diff(dayjs(expirationDate), 'day') > EXPIRED_BLOCKLET_DATA_RETENTION_DAYS;
1957
1968
  };
1958
1969
 
1959
1970
  module.exports = {
1960
1971
  updateBlockletFallbackLogo,
1961
- consumeServerlessNFT,
1962
1972
  forEachBlocklet,
1963
1973
  getBlockletMetaFromUrl: (url) => getBlockletMetaFromUrl(url, { logger }),
1964
1974
  parseComponents,
@@ -1975,7 +1985,6 @@ module.exports = {
1975
1985
  stopBlockletProcess,
1976
1986
  deleteBlockletProcess,
1977
1987
  reloadBlockletProcess,
1978
- getBlockletStatusFromProcess,
1979
1988
  checkBlockletProcessHealthy,
1980
1989
  getHealthyCheckTimeout,
1981
1990
  getProcessInfo,
@@ -2014,6 +2023,9 @@ module.exports = {
2014
2023
  getFixedBundleSource,
2015
2024
  ensureAppLogo,
2016
2025
  getBlockletDidDomainList,
2026
+ getProcessState,
2027
+ getBlockletStatus,
2028
+ shouldSkipComponent,
2017
2029
  getBlockletURLForLauncher,
2018
2030
  exceedRedemptionPeriod,
2019
2031
  };
@@ -0,0 +1,238 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const joinUrl = require('url-join');
4
+ const pick = require('lodash/pick');
5
+ const joinURL = require('url-join');
6
+ const { parseUserAvatar, extractUserAvatar, getAvatarFile, getAvatarByUrl } = require('@abtnode/util/lib/user');
7
+ const { Hasher } = require('@ocap/mcrypto');
8
+ const { getDisplayName } = require('@blocklet/meta/lib/util');
9
+ const getBlockletInfo = require('@blocklet/meta/lib/info');
10
+
11
+ const { getLauncherUser, getLauncherSession: getLauncherSessionRaw, doRequest } = require('@abtnode/auth/lib/launcher');
12
+ const { createAuthToken, getPassportStatusEndpoint } = require('@abtnode/auth/lib/auth');
13
+ const { createPassportVC, createPassport, createUserPassport } = require('@abtnode/auth/lib/passport');
14
+
15
+ const { BlockletStatus } = require('@blocklet/constant');
16
+ const {
17
+ WELLKNOWN_SERVICE_PATH_PREFIX,
18
+ NODE_DATA_DIR_NAME,
19
+ USER_AVATAR_URL_PREFIX,
20
+ ROLES,
21
+ } = require('@abtnode/constant');
22
+
23
+ const logger = require('@abtnode/logger')('@abtnode/core:util:launcher');
24
+
25
+ const { getNFTState } = require('./index');
26
+ const { getBlockletURLForLauncher } = require('./blocklet');
27
+ const states = require('../states');
28
+
29
+ const consumeServerlessNFT = async ({ nftId, blocklet }) => {
30
+ try {
31
+ const state = await getNFTState(blocklet.controller.chainHost, nftId);
32
+ if (!state) {
33
+ throw new Error(`Get nft state failed, chainHost: ${blocklet.controller.chainHost}, nftId: ${nftId}`);
34
+ }
35
+
36
+ const nodeInfo = await states.node.read();
37
+ const appURL = await getBlockletURLForLauncher({ blocklet, nodeInfo });
38
+ logger.info('Consuming serverless nft params', { appURL, nftId });
39
+
40
+ const { launcherUrl } = state.data.value;
41
+ const result = await doRequest(nodeInfo.sk, {
42
+ launcherUrl,
43
+ pathname: '/api/serverless/consume',
44
+ payload: { nftId, appURL },
45
+ method: 'post',
46
+ });
47
+
48
+ logger.info('Consume serverless nft success', { nftId, hash: result.hash });
49
+ } catch (error) {
50
+ logger.error('Consume serverless nft failed', { nftId, error });
51
+ throw new Error(`consume nft ${nftId} failed`);
52
+ }
53
+ };
54
+
55
+ const consumeLauncherSession = async ({ params, blocklet }) => {
56
+ try {
57
+ const info = await states.node.read();
58
+ const { appUrl, name } = getBlockletInfo(blocklet, info.sk);
59
+ const result = await doRequest(info.sk, {
60
+ launcherUrl: params.launcherUrl,
61
+ pathname: `/api/launches/${params.launcherSessionId}/installed`,
62
+ payload: { appDid: blocklet.appPid, appUrl, appName: name, ownerDid: params.ownerDid },
63
+ method: 'post',
64
+ });
65
+
66
+ logger.info('Consume launcher session success', { params, result });
67
+ } catch (error) {
68
+ logger.error('Consume launcher session failed', { params, error });
69
+ throw new Error(`Consume launcher session ${params.launcherSessionId} failed`);
70
+ }
71
+ };
72
+
73
+ const setupAppOwner = async (node, sessionId) => {
74
+ const session = await node.getSession({ id: sessionId });
75
+ if (!session) {
76
+ throw new Error(`Blocklet setup session not found in server: ${sessionId}`);
77
+ }
78
+
79
+ const { appDid, userDid, ownerDid, ownerPk, lastLoginIp, context, locale, launcherUrl, launcherSessionId } = session;
80
+ const [info, blocklet] = await Promise.all([states.node.read(), states.blocklet.getBlocklet(appDid)]);
81
+ if (!blocklet) {
82
+ throw new Error(`Blocklet not found in server: ${appDid}`);
83
+ }
84
+ if (blocklet.status !== BlockletStatus.installed) {
85
+ throw new Error(`Blocklet status not expected: ${appDid}`);
86
+ }
87
+
88
+ const { wallet } = getBlockletInfo(blocklet, info.sk);
89
+ const appUrl = blocklet.environments.find((x) => x.key === 'BLOCKLET_APP_URL').value;
90
+ const dataDir = blocklet.environments.find((x) => x.key === 'BLOCKLET_DATA_DIR').value;
91
+ const serverDataDir = path.join(node.dataDirs.data, NODE_DATA_DIR_NAME);
92
+
93
+ let user;
94
+ let appOwnerProfile;
95
+ // get user from launcher or server
96
+ if (launcherUrl && launcherSessionId) {
97
+ user = await getLauncherUser(info.sk, { launcherUrl, launcherSessionId });
98
+ if (!user) {
99
+ throw new Error(`Owner user not found from launcher: ${launcherUrl}`);
100
+ }
101
+ appOwnerProfile = pick(user, ['fullName', 'email', 'avatar']);
102
+ const avatarBase64 = await getAvatarByUrl(joinURL(launcherUrl, user.avatar));
103
+ appOwnerProfile.avatar = await extractUserAvatar(avatarBase64, { dataDir });
104
+ logger.info('Create owner from launcher for blocklet', { appDid, ownerDid, ownerPk, sessionId, appOwnerProfile });
105
+ } else {
106
+ user = await node.getUser({ teamDid: info.did, user: { did: userDid } });
107
+ if (!user) {
108
+ throw new Error(`Owner user not found in server: ${userDid}`);
109
+ }
110
+ appOwnerProfile = pick(user, ['fullName', 'email', 'avatar']);
111
+ if (user.avatar && user.avatar.startsWith(USER_AVATAR_URL_PREFIX)) {
112
+ const filename = user.avatar.split('/').pop();
113
+ const srcFile = getAvatarFile(serverDataDir, filename);
114
+ const destFile = getAvatarFile(dataDir, filename);
115
+ fs.mkdirpSync(path.dirname(destFile));
116
+ fs.copyFileSync(srcFile, destFile);
117
+ } else {
118
+ appOwnerProfile.avatar = extractUserAvatar(user.avatar, { dataDir });
119
+ }
120
+ logger.info('Create owner from session for blocklet', { appDid, ownerDid, ownerPk, sessionId, appOwnerProfile });
121
+ }
122
+
123
+ // create and send owner passport
124
+ const role = ROLES.OWNER;
125
+ const vc = createPassportVC({
126
+ issuerName: getDisplayName(blocklet),
127
+ issuerWallet: wallet,
128
+ ownerDid,
129
+ passport: await createPassport({
130
+ name: role,
131
+ node,
132
+ teamDid: appDid,
133
+ locale,
134
+ endpoint: appUrl,
135
+ }),
136
+ endpoint: getPassportStatusEndpoint({
137
+ baseUrl: joinUrl(appUrl, WELLKNOWN_SERVICE_PATH_PREFIX),
138
+ userDid: ownerDid,
139
+ teamDid: appDid,
140
+ }),
141
+ ownerProfile: { ...appOwnerProfile, avatar: await parseUserAvatar(user.avatar, { dataDir: serverDataDir }) },
142
+ preferredColor: 'default',
143
+ expirationDate: undefined,
144
+ });
145
+
146
+ // create app owner passport
147
+ const passport = createUserPassport(vc, { role });
148
+
149
+ // add app owner to database
150
+ const doc = await node.loginUser({
151
+ teamDid: appDid,
152
+ user: {
153
+ ...appOwnerProfile,
154
+ did: ownerDid,
155
+ pk: ownerPk,
156
+ passport,
157
+ locale,
158
+ lastLoginIp,
159
+ connectedAccount: {
160
+ provider: 'wallet',
161
+ did: ownerDid,
162
+ pk: ownerPk,
163
+ },
164
+ },
165
+ });
166
+ await node.createAuditLog(
167
+ {
168
+ action: 'addUser',
169
+ args: { teamDid: appDid, userDid: ownerDid, reason: 'launch blocklet' },
170
+ context: { ...context, user: doc },
171
+ result: doc,
172
+ },
173
+ node
174
+ );
175
+ logger.info('Create owner for blocklet', { appDid, ownerDid, sessionId });
176
+
177
+ // create owner login token that can be used to login on blocklet site
178
+ const appSecret = Hasher.SHA3.hash256(Buffer.concat([wallet.secretKey, wallet.address].map(Buffer.from)));
179
+ const setupToken = createAuthToken({
180
+ did: ownerDid,
181
+ passport,
182
+ role,
183
+ secret: appSecret,
184
+ expiresIn: '7d',
185
+ });
186
+
187
+ await node.setBlockletOwner({ did: appDid, owner: { did: ownerDid, pk: ownerPk } });
188
+ await node.endSession({ id: sessionId });
189
+ logger.info('Complete install for blocklet', { appDid, ownerDid, sessionId });
190
+
191
+ return {
192
+ session,
193
+ blocklet,
194
+ setupToken,
195
+ passport: vc,
196
+ };
197
+ };
198
+
199
+ const getLauncherSession = async ({ launcherUrl, launcherSessionId, external = true }) => {
200
+ const info = await states.node.read();
201
+ const result = await getLauncherSessionRaw(info.sk, { launcherUrl, launcherSessionId });
202
+
203
+ // strip sensitive data if call from external
204
+ if (external && result.launcherSession) {
205
+ result.launcherSession = pick(result.launcherSession, ['_id', 'status']);
206
+ }
207
+
208
+ return result;
209
+ };
210
+
211
+ // Check local first, then remote, FIXME: @wangshijun should we check on chain nft?
212
+ const isLauncherSessionConsumed = async (params) => {
213
+ let consumed = await states.blockletExtras.isLauncherSessionConsumed(params.launcherSessionId);
214
+ logger.info('Launcher session consumed at local?', { params, consumed });
215
+ if (consumed) {
216
+ return true;
217
+ }
218
+
219
+ const { error, launcherSession } = await getLauncherSession(params);
220
+ if (error) {
221
+ throw new Error(error);
222
+ }
223
+ if (!launcherSession) {
224
+ throw new Error('Launcher session not found');
225
+ }
226
+
227
+ consumed = launcherSession.status > 40;
228
+ logger.info('Launcher session consumed at remote?', { params, consumed });
229
+ return consumed;
230
+ };
231
+
232
+ module.exports = {
233
+ consumeServerlessNFT,
234
+ consumeLauncherSession,
235
+ setupAppOwner,
236
+ getLauncherSession,
237
+ isLauncherSessionConsumed,
238
+ };