@abtnode/auth 1.16.11-beta-069c3537 → 1.16.11-next-a232f5fb

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/auth.js CHANGED
@@ -191,6 +191,10 @@ const messages = {
191
191
  en: 'Missing chain host',
192
192
  zh: '缺少链的端点地址',
193
193
  },
194
+ missingLauncherSession: {
195
+ en: 'Missing launcherUrl and launcherSessionId',
196
+ zh: '缺少启动器会话参数',
197
+ },
194
198
  invalidBlocklet: {
195
199
  en: 'Invalid Blocklet',
196
200
  zh: '无效的 Blocklet',
@@ -249,10 +253,14 @@ const messages = {
249
253
  en: 'Blocklet Space NFT ID is required',
250
254
  zh: '应用空间 NFT ID 是必须的',
251
255
  },
252
- nftAlreadyConsume: {
256
+ nftAlreadyConsumed: {
253
257
  en: 'This NFT has already been used',
254
258
  zh: '该 NFT 已经被使用过了',
255
259
  },
260
+ sessionAlreadyConsumed: {
261
+ en: 'This session has already been used',
262
+ zh: '该会话已经被使用过了',
263
+ },
256
264
  nftAlreadyExpired: {
257
265
  en: 'This NFT has expired',
258
266
  zh: '该 NFT 已经过期',
@@ -0,0 +1,89 @@
1
+ const Joi = require('joi');
2
+ const axios = require('@abtnode/util/lib/axios');
3
+ const joinURL = require('url-join');
4
+ const pRetry = require('p-retry');
5
+ const { stableStringify } = require('@arcblock/vc');
6
+ const { toBase58 } = require('@ocap/util');
7
+ const getNodeWallet = require('@abtnode/util/lib/get-app-wallet');
8
+ const formatError = require('@abtnode/util/lib/format-error');
9
+
10
+ const logger = require('@abtnode/logger')('@abtnode/auth:launcher');
11
+
12
+ const schema = Joi.object({
13
+ launcherSessionId: Joi.string().required(),
14
+ launcherUrl: Joi.string().uri().required(),
15
+ });
16
+
17
+ const doRequest = async (serverSk, { launcherUrl, pathname, payload, method = 'post' }) => {
18
+ if (!serverSk) {
19
+ throw new Error('serverSk is required to request launcher');
20
+ }
21
+
22
+ const wallet = getNodeWallet(serverSk);
23
+
24
+ const fn = async () => {
25
+ const { data } = await axios({
26
+ method,
27
+ url: joinURL(launcherUrl, pathname),
28
+ data: method !== 'get' ? payload : {},
29
+ params: method === 'get' ? payload : {},
30
+ headers: {
31
+ 'x-server-sig': toBase58(wallet.sign(stableStringify(payload))),
32
+ },
33
+ });
34
+
35
+ return data;
36
+ };
37
+
38
+ const delay = 10 * 1000;
39
+ return pRetry(fn, {
40
+ retries: 3,
41
+ minTimeout: delay,
42
+ maxTimeout: delay,
43
+ onFailedAttempt: (error) => {
44
+ logger.error('failed to call launcher', { error, launcherUrl, pathname, payload });
45
+ // Exclude retrying for 4XX response codes
46
+ if (error.response && error.response.status >= 400 && error.response.status < 500) {
47
+ throw error;
48
+ }
49
+ },
50
+ });
51
+ };
52
+
53
+ const getLauncherSession = async (serverSk, params) => {
54
+ const { error } = schema.validate(params);
55
+ if (error) {
56
+ return { error: formatError(error) };
57
+ }
58
+
59
+ const { launcherSessionId, launcherUrl } = params;
60
+ try {
61
+ const result = await doRequest(serverSk, {
62
+ launcherUrl,
63
+ pathname: `/api/launches/${launcherSessionId}`,
64
+ payload: {},
65
+ method: 'get',
66
+ });
67
+ return { error: '', launcherSession: result.launch };
68
+ } catch (err) {
69
+ return { error: formatError(err) };
70
+ }
71
+ };
72
+
73
+ const getLauncherUser = async (serverSk, params) => {
74
+ const { launcherSessionId, launcherUrl } = params;
75
+ const result = await doRequest(serverSk, {
76
+ launcherUrl,
77
+ pathname: `/api/launches/${launcherSessionId}/user`,
78
+ payload: {},
79
+ method: 'get',
80
+ });
81
+
82
+ return result.user;
83
+ };
84
+
85
+ module.exports = {
86
+ doRequest,
87
+ getLauncherSession,
88
+ getLauncherUser,
89
+ };
package/lib/passport.js CHANGED
@@ -29,7 +29,7 @@ const validatePassport = (d) => {
29
29
  return value;
30
30
  };
31
31
 
32
- const createPassport = async ({ name, node, locale = 'en', teamDid, endpoint, role: inputRole } = {}) => {
32
+ const createPassport = async ({ name, node, locale = 'en', teamDid, endpoint, role: inputRole = null } = {}) => {
33
33
  const passportNotFound = {
34
34
  en: (x) => `The passport was not found: ${x}`,
35
35
  zh: (x) => `未找到通行证: ${x}`,
@@ -66,7 +66,7 @@ const createPassportVC = ({
66
66
  passport,
67
67
  endpoint,
68
68
  types = [],
69
- tag,
69
+ tag = '',
70
70
  ownerProfile,
71
71
  preferredColor,
72
72
  expirationDate,
package/lib/server.js CHANGED
@@ -47,6 +47,7 @@ const {
47
47
  createUserPassport,
48
48
  getPassportClaimUrl,
49
49
  } = require('./passport');
50
+ const { getLauncherSession } = require('./launcher');
50
51
  const logger = require('./logger');
51
52
 
52
53
  const secret = process.env.ABT_NODE_SESSION_SECRET;
@@ -245,6 +246,35 @@ const authenticateByNFT = async ({ node, claims, userDid, challenge, locale, isA
245
246
  };
246
247
  };
247
248
 
249
+ const authenticateByLauncher = async ({ node, claims, launcherSessionId, launcherUrl, chainHost }) => {
250
+ const info = await node.getNodeInfo();
251
+ const { error, launcherSession } = await getLauncherSession(info.sk, { launcherSessionId, launcherUrl });
252
+ if (error) {
253
+ throw new Error(error);
254
+ }
255
+
256
+ const claim = claims.find((x) => x.type === 'keyPair');
257
+ return {
258
+ role: SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER,
259
+ teamDid: info.did,
260
+ user: {
261
+ did: claim.userDid,
262
+ pk: claim.userPk,
263
+ },
264
+ extra: {
265
+ controller: {
266
+ nftId: launcherSession.nftDid,
267
+ nftOwner: launcherSession.userDid,
268
+ chainHost,
269
+ appMaxCount: 1,
270
+ launcherUrl,
271
+ launcherSessionId,
272
+ ownerDid: claim.userDid,
273
+ },
274
+ },
275
+ };
276
+ };
277
+
248
278
  const authenticateBySession = async ({ node, userDid, locale, allowedRoles = ['owner', 'admin', 'member'] }) => {
249
279
  const info = await node.getNodeInfo();
250
280
  const user = await getUser(node, info.did, userDid);
@@ -549,10 +579,12 @@ const ensureBlockletPermission = async ({
549
579
  claims,
550
580
  challenge,
551
581
  locale,
552
- blocklet,
553
- isAuth,
554
582
  chainHost,
583
+ isAuth = false,
584
+ blocklet = null,
555
585
  allowedRoles = ['owner', 'admin', 'member'],
586
+ launcherUrl = '',
587
+ launcherSessionId = '',
556
588
  }) => {
557
589
  let result;
558
590
  if (authMethod === 'vc') {
@@ -576,6 +608,14 @@ const ensureBlockletPermission = async ({
576
608
  isAuth,
577
609
  chainHost,
578
610
  });
611
+ } else if (authMethod === 'launcher') {
612
+ result = await authenticateByLauncher({
613
+ node,
614
+ claims,
615
+ launcherUrl,
616
+ launcherSessionId,
617
+ chainHost,
618
+ });
579
619
  } else {
580
620
  result = await authenticateBySession({
581
621
  node,
@@ -597,12 +637,12 @@ const ensureBlockletPermission = async ({
597
637
  const createLaunchBlockletHandler =
598
638
  (node, authMethod) =>
599
639
  async ({ claims, challenge, userDid, updateSession, req, didwallet, extraParams }) => {
600
- const { locale, blockletMetaUrl, title, description, chainHost } = extraParams;
640
+ const { locale, blockletMetaUrl, title, description, chainHost, launcherSessionId, launcherUrl } = extraParams;
601
641
  logger.info('createLaunchBlockletHandler', extraParams);
602
642
 
603
- const keyPair = claims.find((x) => x.type === 'keyPair');
604
- if (!keyPair) {
605
- logger.error('app keyPair must be provided');
643
+ const claim = claims.find((x) => x.type === 'keyPair');
644
+ if (!claim) {
645
+ logger.error('keyPair claim must be provided');
606
646
  throw new Error(messages.missingKeyPair[locale]);
607
647
  }
608
648
 
@@ -616,6 +656,11 @@ const createLaunchBlockletHandler =
616
656
  throw new Error(messages.missingChainHost[locale]);
617
657
  }
618
658
 
659
+ if (authMethod === 'launcher' && !(launcherSessionId && launcherUrl)) {
660
+ logger.error('launcherUrl and launcherSessionId must be provided');
661
+ throw new Error(messages.missingLauncherSession[locale]);
662
+ }
663
+
619
664
  let blocklet;
620
665
  let blockletWalletType;
621
666
  if (blockletMetaUrl) {
@@ -644,13 +689,13 @@ const createLaunchBlockletHandler =
644
689
  claims,
645
690
  challenge,
646
691
  locale,
647
- isAuth: false,
648
692
  chainHost,
649
693
  blocklet,
694
+ launcherSessionId,
695
+ launcherUrl,
650
696
  });
651
697
 
652
698
  let controller;
653
-
654
699
  let sessionToken = '';
655
700
  if (authMethod === 'vc') {
656
701
  sessionToken = createAuthToken({
@@ -660,52 +705,71 @@ const createLaunchBlockletHandler =
660
705
  secret,
661
706
  expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
662
707
  });
663
- }
664
- if (authMethod === 'nft') {
665
- if (role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
666
- controller = extra.controller;
667
- sessionToken = createBlockletControllerAuthToken({
668
- did: userDid,
669
- role,
670
- controller,
671
- secret,
672
- expiresIn: EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE,
673
- });
674
- } else {
675
- sessionToken = createAuthTokenByOwnershipNFT({
676
- did: userDid,
677
- role,
678
- secret,
679
- expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
680
- });
681
- }
708
+ } else if (role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
709
+ // launch with serverless nft or launcher session
710
+ controller = extra.controller;
711
+ sessionToken = createBlockletControllerAuthToken({
712
+ did: userDid,
713
+ role,
714
+ controller,
715
+ secret,
716
+ expiresIn: EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE,
717
+ });
718
+ } else if (authMethod === 'nft') {
719
+ // launch with server nft
720
+ sessionToken = createAuthTokenByOwnershipNFT({
721
+ did: userDid,
722
+ role,
723
+ secret,
724
+ expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
725
+ });
682
726
  }
683
727
 
684
728
  if (sessionToken) {
685
729
  await updateSession({ sessionToken }, true);
686
730
  }
687
731
 
688
- const appSk = toHex(keyPair.secret);
689
- const appDid = getApplicationWallet(appSk, undefined, blockletWalletType).address;
690
- await updateSession({ appDid });
732
+ const appSk = toHex(claim.secret);
733
+ const wallet = getApplicationWallet(appSk, undefined, blockletWalletType);
734
+ const appDid = wallet.address;
735
+ const { id: sessionId } = await node.startSession({
736
+ data: {
737
+ appDid,
738
+ userDid,
739
+ ownerDid: claim.userDid,
740
+ ownerPk: claim.userPk,
741
+ lastLoginIp: get(req, 'headers[x-real-ip]') || '',
742
+ context: formatContext(req),
743
+ locale,
744
+ launcherSessionId,
745
+ launcherUrl,
746
+ },
747
+ });
748
+
749
+ await updateSession({ appDid, sessionId });
691
750
 
751
+ // FIXME: @zhenqiang do we still need this here? since the appDid changes each time
692
752
  if (blocklet) {
693
753
  // 检查是否已安装,这里不做升级的处理
694
754
  const existedBlocklet = await node.getBlocklet({ did: appDid });
695
-
696
- // 如果是 serverless, 并且已经消费过了,但是没有安装,则抛出异常
697
- if (!existedBlocklet && role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER && isNFTConsumed(nft)) {
698
- throw new Error(messages.nftAlreadyConsumed[locale]);
699
- }
700
-
701
755
  if (existedBlocklet) {
702
756
  await updateSession({ isInstalled: true });
703
757
  logger.info('blocklet already exists', { appDid });
704
758
  return;
705
759
  }
760
+
761
+ // 如果是 serverless, 并且已经消费过了,但是没有安装,则抛出异常
762
+ if (role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
763
+ if (authMethod === 'nft' && isNFTConsumed(nft)) {
764
+ throw new Error(messages.nftAlreadyConsumed[locale]);
765
+ }
766
+ if (authMethod === 'launcher' && (await node.isLauncherSessionConsumed({ launcherUrl, launcherSessionId }))) {
767
+ throw new Error(messages.sessionAlreadyConsumed[locale]);
768
+ }
769
+ }
706
770
  }
707
771
 
708
- logger.info('start install blocklet', { blockletMetaUrl, title, description });
772
+ logger.info('start install blocklet', { blockletMetaUrl, title, description, controller });
709
773
  await node.installBlocklet(
710
774
  {
711
775
  url: blockletMetaUrl,
@@ -713,7 +777,7 @@ const createLaunchBlockletHandler =
713
777
  description,
714
778
  appSk,
715
779
  skSource: didwallet?.version ? `${didwallet.os}-wallet-v${didwallet.version}` : '',
716
- delay: 1000 * 4, // delay 4 seconds to download, wait for ws connection from frontend
780
+ delay: 2000, // wait for ws connection from frontend
717
781
  downloadTokenList: extraParams?.previousWorkflowData?.downloadTokenList,
718
782
  controller: role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER ? controller : null,
719
783
  },
@@ -754,9 +818,9 @@ const createRotateKeyPairHandler =
754
818
  const { locale, appDid } = extraParams;
755
819
  logger.info('createRotateKeyPairHandler', extraParams);
756
820
 
757
- const keyPair = claims.find((x) => x.type === 'keyPair');
758
- if (!keyPair) {
759
- logger.error('app keyPair must be provided');
821
+ const claim = claims.find((x) => x.type === 'keyPair');
822
+ if (!claim) {
823
+ logger.error('keyPair claim must be provided');
760
824
  throw new Error(messages.missingKeyPair[locale]);
761
825
  }
762
826
 
@@ -783,16 +847,47 @@ const createRotateKeyPairHandler =
783
847
  await node.configBlocklet(
784
848
  {
785
849
  did: blocklet.meta.did,
786
- configs: [{ key: 'BLOCKLET_APP_SK', value: toHex(keyPair.secret), secure: true }],
850
+ configs: [{ key: 'BLOCKLET_APP_SK', value: toHex(claim.secret), secure: true }],
787
851
  skipHook: true,
788
852
  },
789
853
  formatContext(Object.assign(req, { user: { did: userDid, fullName: 'Owner', role: 'owner' } }))
790
854
  );
791
855
  };
792
856
 
857
+ const handleRestoreByLauncherSession = async ({ node, userDid, updateSession, extraParams }) => {
858
+ const { chainHost, locale, launcherUrl, launcherSessionId } = extraParams;
859
+ if (await node.isLauncherSessionConsumed({ launcherUrl, launcherSessionId })) {
860
+ throw new Error(messages.sessionAlreadyConsumed[locale]);
861
+ }
862
+
863
+ const { launcherSession } = await node.getLauncherSession({ launcherUrl, launcherSessionId, external: false });
864
+ const controller = {
865
+ nftId: launcherSession.nftDid,
866
+ nftOwner: launcherSession.userDid,
867
+ chainHost,
868
+ appMaxCount: 1,
869
+ launcherUrl,
870
+ launcherSessionId,
871
+ ownerDid: userDid, // FIXME: @wangshijun is this incorrect
872
+ };
873
+
874
+ const sessionToken = createBlockletControllerAuthToken({
875
+ did: userDid,
876
+ role: SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER,
877
+ controller,
878
+ secret,
879
+ expiresIn: EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE,
880
+ });
881
+
882
+ await updateSession({ sessionToken }, true);
883
+
884
+ return controller;
885
+ };
886
+
793
887
  const createRestoreByNftHandler =
794
888
  (node, authMethod) =>
795
- async ({ claims, challenge, userDid, updateSession, extraParams: { chainHost, locale } }) => {
889
+ async ({ claims, challenge, userDid, updateSession, extraParams }) => {
890
+ const { chainHost, locale, launcherUrl, launcherSessionId } = extraParams;
796
891
  const { role, extra } = await ensureBlockletPermission({
797
892
  authMethod,
798
893
  node,
@@ -800,8 +895,9 @@ const createRestoreByNftHandler =
800
895
  claims,
801
896
  challenge,
802
897
  locale,
803
- isAuth: false,
804
898
  chainHost,
899
+ launcherUrl,
900
+ launcherSessionId,
805
901
  });
806
902
 
807
903
  let sessionToken = '';
@@ -838,7 +934,7 @@ const createRestoreByNftHandler =
838
934
 
839
935
  const createServerlessInstallGuard = (node) => {
840
936
  return async ({ extraParams }) => {
841
- const { locale, blockletMetaUrl } = extraParams;
937
+ const { locale, blockletMetaUrl, launcherUrl, launcherSessionId } = extraParams;
842
938
  const [info, blocklet] = await Promise.all([
843
939
  node.getNodeInfo(),
844
940
  blockletMetaUrl ? node.getBlockletMetaFromUrl({ url: blockletMetaUrl, checkPrice: true }) : null,
@@ -851,6 +947,13 @@ const createServerlessInstallGuard = (node) => {
851
947
  throw new Error(messages.notSupported[locale]);
852
948
  }
853
949
  }
950
+
951
+ // check if launcher session consumed
952
+ if (launcherUrl && launcherSessionId) {
953
+ if (await node.isLauncherSessionConsumed({ launcherUrl, launcherSessionId })) {
954
+ throw new Error(messages.sessionAlreadyConsumed[locale]);
955
+ }
956
+ }
854
957
  };
855
958
  };
856
959
 
@@ -860,6 +963,7 @@ module.exports = {
860
963
  authenticateByVc,
861
964
  authenticateByNFT,
862
965
  authenticateBySession,
966
+ authenticateByLauncher,
863
967
  getRotateKeyPairClaims,
864
968
  getOwnershipNFTClaim,
865
969
  getLaunchBlockletClaims,
@@ -876,4 +980,5 @@ module.exports = {
876
980
  getAuthPrincipalForMigrateAppToV2,
877
981
  getAuthPrincipalForTransferAppOwnerShip,
878
982
  createServerlessInstallGuard,
983
+ handleRestoreByLauncherSession,
879
984
  };
@@ -1,10 +1,18 @@
1
1
  const get = require('lodash/get');
2
2
 
3
- const getServerAuthMethod = (info, type) => {
3
+ const getServerAuthMethod = (info, type, launcherSessionId = '', authorized = false) => {
4
+ if (launcherSessionId) {
5
+ return 'launcher';
6
+ }
7
+
4
8
  if (type === 'serverless') {
5
9
  return 'nft';
6
10
  }
7
11
 
12
+ if (authorized) {
13
+ return 'session';
14
+ }
15
+
8
16
  if (info.initialized) {
9
17
  return 'vc';
10
18
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.11-beta-069c3537",
6
+ "version": "1.16.11-next-a232f5fb",
7
7
  "description": "Simple lib to manage auth in ABT Node",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -20,16 +20,17 @@
20
20
  "author": "linchen <linchen1987@foxmail.com> (http://github.com/linchen1987)",
21
21
  "license": "Apache-2.0",
22
22
  "dependencies": {
23
- "@abtnode/constant": "1.16.11-beta-069c3537",
24
- "@abtnode/logger": "1.16.11-beta-069c3537",
25
- "@abtnode/util": "1.16.11-beta-069c3537",
23
+ "@abtnode/constant": "1.16.11-next-a232f5fb",
24
+ "@abtnode/logger": "1.16.11-next-a232f5fb",
25
+ "@abtnode/util": "1.16.11-next-a232f5fb",
26
26
  "@arcblock/did": "1.18.80",
27
27
  "@arcblock/vc": "1.18.80",
28
- "@blocklet/constant": "1.16.11-beta-069c3537",
29
- "@blocklet/meta": "1.16.11-beta-069c3537",
28
+ "@blocklet/constant": "1.16.11-next-a232f5fb",
29
+ "@blocklet/meta": "1.16.11-next-a232f5fb",
30
30
  "@ocap/mcrypto": "1.18.80",
31
31
  "@ocap/util": "1.18.80",
32
32
  "@ocap/wallet": "1.18.80",
33
+ "fs-extra": "^10.1.0",
33
34
  "joi": "17.7.0",
34
35
  "jsonwebtoken": "^9.0.0",
35
36
  "lodash": "^4.17.21",
@@ -41,5 +42,5 @@
41
42
  "devDependencies": {
42
43
  "jest": "^27.5.1"
43
44
  },
44
- "gitHead": "a89c7f13901d2c2d50b09d68d6088ca705648ff6"
45
+ "gitHead": "1af264e9b6648aa191fbf23c67717262ac717697"
45
46
  }