@abtnode/auth 1.16.45-beta-20250612-231219-481217be → 1.16.45-beta-20250618-073451-6e48fb62
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/bind-wallet.js +65 -0
- package/lib/server.js +4 -4
- package/lib/util/bind-wallet.js +299 -0
- package/lib/util/client.js +185 -0
- package/lib/util/federated.js +13 -0
- package/lib/util/transfer-passport.js +188 -0
- package/package.json +13 -10
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { onConnect, onApprove, authPrincipal } = require('./util/bind-wallet');
|
|
2
|
+
const logger = require('./logger');
|
|
3
|
+
|
|
4
|
+
const createBindWalletRoute = ({ node }) => ({
|
|
5
|
+
action: 'bind-wallet',
|
|
6
|
+
authPrincipal: false,
|
|
7
|
+
claims: {
|
|
8
|
+
authPrincipal: ({ extraParams: { locale, previousUserDid, email } }) => {
|
|
9
|
+
return authPrincipal({ locale, previousUserDid, email });
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
onConnect: ({ req, userDid, extraParams: { locale, passportId = '', componentId, previousUserDid, isService } }) => {
|
|
13
|
+
return onConnect({
|
|
14
|
+
node,
|
|
15
|
+
request: req,
|
|
16
|
+
userDid,
|
|
17
|
+
locale,
|
|
18
|
+
passportId,
|
|
19
|
+
componentId,
|
|
20
|
+
previousUserDid,
|
|
21
|
+
isService,
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
onAuth: async ({
|
|
26
|
+
claims,
|
|
27
|
+
userDid,
|
|
28
|
+
userPk,
|
|
29
|
+
extraParams: { locale, previousUserDid, skipMigrateAccount, isService },
|
|
30
|
+
req,
|
|
31
|
+
baseUrl,
|
|
32
|
+
}) => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await onApprove({
|
|
35
|
+
node,
|
|
36
|
+
request: req,
|
|
37
|
+
locale,
|
|
38
|
+
userDid,
|
|
39
|
+
userPk,
|
|
40
|
+
baseUrl,
|
|
41
|
+
claims,
|
|
42
|
+
previousUserDid,
|
|
43
|
+
skipMigrateAccount:
|
|
44
|
+
typeof skipMigrateAccount === 'boolean' ? skipMigrateAccount : skipMigrateAccount === 'true',
|
|
45
|
+
isService: typeof isService === 'boolean' ? isService : isService === 'true',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (result.nextWorkflowData && skipMigrateAccount) {
|
|
49
|
+
result.nextWorkflowData = {
|
|
50
|
+
...result.nextWorkflowData,
|
|
51
|
+
skipCheckOwner: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.error('login.error', { error: err, userDid });
|
|
58
|
+
throw new Error(err.message);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
createBindWalletRoute,
|
|
65
|
+
};
|
package/lib/server.js
CHANGED
|
@@ -174,7 +174,7 @@ const authenticateByVc = async ({
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// check user approved
|
|
177
|
-
const user = await getUser(node, teamDid, userDid);
|
|
177
|
+
const user = await getUser(node, teamDid, userDid, { enableConnectedAccount: true });
|
|
178
178
|
if (user && !user.approved) {
|
|
179
179
|
throw new CustomError(403, messages.notAllowed[locale]);
|
|
180
180
|
}
|
|
@@ -265,7 +265,7 @@ const authenticateByNFT = async ({ node, claims, userDid, challenge, locale, isA
|
|
|
265
265
|
throw new CustomError(400, messages.tagNotMatch[locale]);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
const user = await getUser(node, info.did, userDid);
|
|
268
|
+
const user = await getUser(node, info.did, userDid, { enableConnectedAccount: true });
|
|
269
269
|
return {
|
|
270
270
|
role: ROLES.OWNER,
|
|
271
271
|
teamDid: info.did,
|
|
@@ -307,7 +307,7 @@ const authenticateByLauncher = async ({ node, claims, launcherSessionId, launche
|
|
|
307
307
|
|
|
308
308
|
const authenticateBySession = async ({ node, userDid, locale, allowedRoles = ['owner', 'admin', 'member'] }) => {
|
|
309
309
|
const info = await node.getNodeInfo();
|
|
310
|
-
const user = await getUser(node, info.did, userDid);
|
|
310
|
+
const user = await getUser(node, info.did, userDid, { enableConnectedAccount: true });
|
|
311
311
|
if (!user) {
|
|
312
312
|
throw new CustomError(404, messages.userNotFound[locale]);
|
|
313
313
|
}
|
|
@@ -882,7 +882,7 @@ const getBlockletPermissionChecker =
|
|
|
882
882
|
const { locale = 'en' } = extraParams;
|
|
883
883
|
|
|
884
884
|
const info = await node.getNodeInfo();
|
|
885
|
-
const user = await getUser(node, info.did, userDid);
|
|
885
|
+
const user = await getUser(node, info.did, userDid, { enableConnectedAccount: true });
|
|
886
886
|
if (!user) {
|
|
887
887
|
throw new CustomError(404, messages.userNotFound[locale]);
|
|
888
888
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const createTranslator = require('@abtnode/util/lib/translate');
|
|
2
|
+
const { LOGIN_PROVIDER } = require('@blocklet/constant');
|
|
3
|
+
const { getSourceAppPid } = require('@blocklet/sdk/lib/util/login');
|
|
4
|
+
const { extractUserAvatar } = require('@abtnode/util/lib/user');
|
|
5
|
+
const { fromAppDid } = require('@arcblock/did-ext');
|
|
6
|
+
const getRequestIP = require('@abtnode/util/lib/get-request-ip');
|
|
7
|
+
const merge = require('lodash/merge');
|
|
8
|
+
const formatContext = require('@abtnode/util/lib/format-context');
|
|
9
|
+
|
|
10
|
+
const { shouldSyncFederated, getUserAvatarUrl, getFederatedMaster, migrateFederatedAccount } = require('./federated');
|
|
11
|
+
const { messages, getApplicationInfo } = require('../auth');
|
|
12
|
+
const { upsertToPassports } = require('../passport');
|
|
13
|
+
const { transferPassport } = require('./transfer-passport');
|
|
14
|
+
const { declareAccount, migrateAccount } = require('./client');
|
|
15
|
+
const logger = require('../logger');
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
authPrincipal: ({ email, locale, previousUserDid }) => {
|
|
19
|
+
const user = email || previousUserDid;
|
|
20
|
+
|
|
21
|
+
const message = locale === 'zh' ? `将你的 DID Wallet 与账号 ${user} 绑定` : `Connect your DID Wallet with ${user}`;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
description: message,
|
|
25
|
+
supervised: true,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
onConnect: async ({ node, request, userDid, locale, previousUserDid, isService }) => {
|
|
29
|
+
const translations = {
|
|
30
|
+
en: {
|
|
31
|
+
notFound: "Couldn't find account information.",
|
|
32
|
+
alreadyBindOAuth: 'Your wallet account ({did}) is already bond to another email.',
|
|
33
|
+
alreadyBindWallet: 'Your email is already bond to another wallet account {did}.',
|
|
34
|
+
alreadyMainAccount:
|
|
35
|
+
'Your wallet account is already bond to this app. You cannot bind it again. Please use another wallet account or create a new one to try again.',
|
|
36
|
+
},
|
|
37
|
+
zh: {
|
|
38
|
+
notFound: '无法获取账户信息。',
|
|
39
|
+
alreadyBindOAuth: '你的钱包账户 {did} 已经与其他账户绑定。',
|
|
40
|
+
alreadyBindWallet: '当前账户已经绑定过钱包账户 {did}。',
|
|
41
|
+
alreadyMainAccount: '你的钱包账户 {did} 已绑定过该应用,无法重复绑定,请切换或新建一个钱包账户再次尝试。',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const t = createTranslator({ translations });
|
|
45
|
+
const { did: teamDid } = isService ? await request.getBlockletInfo() : await node.getNodeInfo();
|
|
46
|
+
|
|
47
|
+
const walletUser = await node.getUser({ teamDid, user: { did: userDid } });
|
|
48
|
+
if (walletUser) {
|
|
49
|
+
throw new Error(t('alreadyMainAccount', locale, { did: userDid }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const oauthUser = await node.getUser({
|
|
53
|
+
teamDid,
|
|
54
|
+
user: {
|
|
55
|
+
did: previousUserDid,
|
|
56
|
+
},
|
|
57
|
+
options: {
|
|
58
|
+
enableConnectedAccount: true,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
if (!oauthUser) {
|
|
62
|
+
throw new Error(t('notFound', locale, { email: oauthUser.email }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sourceProvider = oauthUser.sourceProvider || LOGIN_PROVIDER.WALLET;
|
|
66
|
+
const oauthConnectedAccounts = oauthUser.connectedAccounts || [];
|
|
67
|
+
const exist = oauthConnectedAccounts.find((item) => item.provider === LOGIN_PROVIDER.WALLET);
|
|
68
|
+
if (exist) {
|
|
69
|
+
throw new Error(t('alreadyBindWallet', locale, { email: oauthUser.email, did: exist.did }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const bindUser = await node.getUser({
|
|
73
|
+
teamDid,
|
|
74
|
+
user: {
|
|
75
|
+
did: userDid,
|
|
76
|
+
},
|
|
77
|
+
options: {
|
|
78
|
+
enableConnectedAccount: true,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (bindUser) {
|
|
83
|
+
const bindConnectedAccounts = bindUser.connectedAccounts || [];
|
|
84
|
+
if (bindConnectedAccounts.find((item) => item.provider === sourceProvider)) {
|
|
85
|
+
throw new Error(t('alreadyBindOAuth', locale, { email: oauthUser.email, did: userDid }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const claims = {
|
|
90
|
+
profile: {
|
|
91
|
+
type: 'profile',
|
|
92
|
+
description: messages.description[locale],
|
|
93
|
+
items: ['fullName', 'avatar'],
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// 至少需要一个 claim
|
|
98
|
+
if (oauthUser.avatar) {
|
|
99
|
+
delete claims.profile;
|
|
100
|
+
}
|
|
101
|
+
if (Object.keys(claims).length > 0) {
|
|
102
|
+
return claims;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [];
|
|
106
|
+
},
|
|
107
|
+
onApprove: async ({
|
|
108
|
+
node,
|
|
109
|
+
request,
|
|
110
|
+
locale,
|
|
111
|
+
userDid,
|
|
112
|
+
userPk,
|
|
113
|
+
claims,
|
|
114
|
+
previousUserDid,
|
|
115
|
+
baseUrl,
|
|
116
|
+
skipMigrateAccount = false,
|
|
117
|
+
isService = false,
|
|
118
|
+
}) => {
|
|
119
|
+
// 在这里要对 server 还是 service 进行区分
|
|
120
|
+
const nodeInfo = await node.getNodeInfo();
|
|
121
|
+
|
|
122
|
+
let teamDid = nodeInfo.did;
|
|
123
|
+
let wallet = null;
|
|
124
|
+
let blockletInfo = null;
|
|
125
|
+
let sourceAppPid = null;
|
|
126
|
+
let blocklet = null;
|
|
127
|
+
if (isService) {
|
|
128
|
+
blocklet = await request.getBlocklet();
|
|
129
|
+
blockletInfo = await request.getBlockletInfo();
|
|
130
|
+
sourceAppPid = getSourceAppPid(request);
|
|
131
|
+
const { did: blockletDid, wallet: blockletWallet } = blockletInfo;
|
|
132
|
+
teamDid = blockletDid;
|
|
133
|
+
wallet = blockletWallet;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const oauthUser = await node.getUser({ teamDid, user: { did: previousUserDid } });
|
|
137
|
+
|
|
138
|
+
// Check user approved
|
|
139
|
+
let bindUser = await node.getUser({
|
|
140
|
+
teamDid,
|
|
141
|
+
user: {
|
|
142
|
+
did: userDid,
|
|
143
|
+
},
|
|
144
|
+
options: {
|
|
145
|
+
enableConnectedAccount: true,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
if (bindUser && !bindUser.approved) {
|
|
149
|
+
throw new Error(messages.notAllowedAppUser[locale]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { dataDir } = await getApplicationInfo({ node, nodeInfo, teamDid });
|
|
153
|
+
|
|
154
|
+
const profileOld = claims.find((x) => x.type === 'profile') || { avatar: null };
|
|
155
|
+
const avatar = await extractUserAvatar(oauthUser.avatar || profileOld.avatar, { dataDir });
|
|
156
|
+
const profile = {
|
|
157
|
+
fullName: oauthUser.fullName,
|
|
158
|
+
avatar,
|
|
159
|
+
email: oauthUser.email,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (isService) {
|
|
163
|
+
if (sourceAppPid) {
|
|
164
|
+
try {
|
|
165
|
+
await migrateFederatedAccount({
|
|
166
|
+
// 目前只允许未注册过的钱包绑定 auth0,所以直接传入钱包生成的 userDid 和 userPk
|
|
167
|
+
toUserDid: userDid,
|
|
168
|
+
toUserPk: userPk,
|
|
169
|
+
fromUserDid: previousUserDid,
|
|
170
|
+
blockletInfo,
|
|
171
|
+
blocklet,
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logger.error('Failed to migrate federated account', {
|
|
175
|
+
error,
|
|
176
|
+
toUserDid: userDid,
|
|
177
|
+
fromUserDid: previousUserDid,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (error?.response?.data) {
|
|
181
|
+
throw new Error(error.response.data);
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const connectedAccounts = oauthUser?.connectedAccounts || [];
|
|
187
|
+
const sourceProvider = oauthUser?.sourceProvider;
|
|
188
|
+
const oauthAccount = connectedAccounts.find((item) => item.provider === sourceProvider);
|
|
189
|
+
const userWallet = fromAppDid(oauthAccount.id, wallet.secretKey);
|
|
190
|
+
await declareAccount({ wallet: userWallet, blocklet });
|
|
191
|
+
if (!skipMigrateAccount) {
|
|
192
|
+
await migrateAccount({ wallet: userWallet, blocklet, user: { did: userDid, pk: userPk } });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TODO: 获取当前登录使用的 passport(无法获取到 passport.id)
|
|
198
|
+
// 使用最近一次使用的 passport 来代替
|
|
199
|
+
const mergePassport = (oauthUser.passports || []).reduce((sum, cur) => {
|
|
200
|
+
return upsertToPassports(sum, cur);
|
|
201
|
+
}, bindUser?.passports || []);
|
|
202
|
+
const mergeProfile = merge(profile, {
|
|
203
|
+
email: bindUser?.email,
|
|
204
|
+
fullName: bindUser?.fullName,
|
|
205
|
+
avatar: bindUser?.avatar,
|
|
206
|
+
inviter: bindUser?.inviter,
|
|
207
|
+
generation: bindUser?.generation,
|
|
208
|
+
emailVerified: bindUser?.emailVerified,
|
|
209
|
+
phoneVerified: bindUser?.phoneVerified,
|
|
210
|
+
});
|
|
211
|
+
const currentTime = new Date().toISOString();
|
|
212
|
+
|
|
213
|
+
const connectedAccount = {
|
|
214
|
+
provider: LOGIN_PROVIDER.WALLET,
|
|
215
|
+
did: userDid,
|
|
216
|
+
pk: userPk,
|
|
217
|
+
lastLoginAt: currentTime,
|
|
218
|
+
firstLoginAt: currentTime,
|
|
219
|
+
userInfo: {
|
|
220
|
+
wallet: request.context.didwallet,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
await node.updateUser({
|
|
225
|
+
teamDid,
|
|
226
|
+
user: {
|
|
227
|
+
did: oauthUser.did,
|
|
228
|
+
pk: oauthUser.pk,
|
|
229
|
+
...mergeProfile,
|
|
230
|
+
lastLoginIp: getRequestIP(request),
|
|
231
|
+
connectedAccounts: [connectedAccount],
|
|
232
|
+
passports: mergePassport,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (isService) {
|
|
237
|
+
const masterSite = getFederatedMaster(blocklet);
|
|
238
|
+
// NOTICE: 采用异步来更新,不阻塞接口的正常响应
|
|
239
|
+
if (shouldSyncFederated(sourceAppPid, blocklet)) {
|
|
240
|
+
const syncUserData = {
|
|
241
|
+
did: oauthUser.did,
|
|
242
|
+
pk: oauthUser.pk,
|
|
243
|
+
...mergeProfile,
|
|
244
|
+
connectedAccount: [connectedAccount],
|
|
245
|
+
};
|
|
246
|
+
if (syncUserData.avatar) {
|
|
247
|
+
syncUserData.avatar = getUserAvatarUrl(syncUserData.avatar, blocklet);
|
|
248
|
+
}
|
|
249
|
+
node.syncFederated({
|
|
250
|
+
did: teamDid,
|
|
251
|
+
data: {
|
|
252
|
+
users: [
|
|
253
|
+
{
|
|
254
|
+
...syncUserData,
|
|
255
|
+
action: 'connectAccount',
|
|
256
|
+
sourceAppPid: sourceAppPid || masterSite.appPid,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!bindUser) {
|
|
265
|
+
bindUser = {
|
|
266
|
+
...oauthUser,
|
|
267
|
+
// 发送 passport 的对象要设置为 wallet-did
|
|
268
|
+
did: userDid,
|
|
269
|
+
pk: userPk,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// FIXME:@zhanghan 统一登录的 passport 相关问题后续统一处理
|
|
274
|
+
await transferPassport(oauthUser, bindUser, {
|
|
275
|
+
req: request,
|
|
276
|
+
node,
|
|
277
|
+
nodeInfo,
|
|
278
|
+
teamDid,
|
|
279
|
+
baseUrl,
|
|
280
|
+
revokePassport: true,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await node.createAuditLog(
|
|
284
|
+
{
|
|
285
|
+
action: 'connectAccount',
|
|
286
|
+
args: { teamDid, connectedAccount, provider: LOGIN_PROVIDER.WALLET, userDid: oauthUser.did },
|
|
287
|
+
context: formatContext(Object.assign(request, { user: oauthUser })),
|
|
288
|
+
result: bindUser,
|
|
289
|
+
},
|
|
290
|
+
node
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
nextWorkflowData: {
|
|
295
|
+
userDid,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const { getChainClient } = require('@abtnode/util/lib/get-chain-client');
|
|
2
|
+
const { MAIN_CHAIN_ENDPOINT } = require('@abtnode/constant');
|
|
3
|
+
const { getBlockletChainInfo } = require('@blocklet/meta/lib/util');
|
|
4
|
+
const jwt = require('jsonwebtoken');
|
|
5
|
+
const jwksClient = require('jwks-rsa');
|
|
6
|
+
const { toStakeAddress } = require('@arcblock/did-util');
|
|
7
|
+
const { sign } = require('@arcblock/jwt');
|
|
8
|
+
const { toBN, fromUnitToToken } = require('@ocap/util');
|
|
9
|
+
const { toTxHash } = require('@ocap/mcrypto');
|
|
10
|
+
const getBlockletInfo = require('@blocklet/meta/lib/info');
|
|
11
|
+
|
|
12
|
+
const logger = require('../logger');
|
|
13
|
+
|
|
14
|
+
async function declareAccountByChain({ chainHost, wallet }) {
|
|
15
|
+
const chainClient = getChainClient(chainHost);
|
|
16
|
+
const { address } = wallet;
|
|
17
|
+
const { state } = await chainClient.getAccountState({ address }, { ignoreFields: ['context'] });
|
|
18
|
+
if (state) {
|
|
19
|
+
logger.info('skip declare account on chain done', { chain: chainHost, did: address });
|
|
20
|
+
} else {
|
|
21
|
+
logger.warn('declare account on chain deprecated', { chain: chainHost, did: address });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function declareAccount({ wallet, blocklet }) {
|
|
26
|
+
const chainHostList = [MAIN_CHAIN_ENDPOINT];
|
|
27
|
+
// 本质上是获取环境变量为 CHAIN_HOST 的值
|
|
28
|
+
const { host: chainHost } = getBlockletChainInfo(blocklet);
|
|
29
|
+
if (chainHost && chainHost !== 'none') {
|
|
30
|
+
chainHostList.push(chainHost);
|
|
31
|
+
}
|
|
32
|
+
const waitingList = [...new Set(chainHostList)].map((item) => declareAccountByChain({ chainHost: item, wallet }));
|
|
33
|
+
await Promise.all(waitingList);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function migrateAccountByChain({ chainHost, user, wallet, blocklet }) {
|
|
37
|
+
const client = getChainClient(chainHost);
|
|
38
|
+
|
|
39
|
+
// Do not migrate if target account already exists
|
|
40
|
+
const { state: exist } = await client.getAccountState({ address: user.did });
|
|
41
|
+
if (exist) {
|
|
42
|
+
logger.info('migrate account on chain done', { chain: chainHost, did: user.did });
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ensure staked for gas
|
|
47
|
+
const info = getBlockletInfo(blocklet);
|
|
48
|
+
await ensureStakedForGas({ client, chainHost, wallet: info.wallet });
|
|
49
|
+
|
|
50
|
+
// send migrate tx with app as gas payer
|
|
51
|
+
const tx = await client.signAccountMigrateTx({
|
|
52
|
+
tx: {
|
|
53
|
+
itx: {
|
|
54
|
+
address: user.did,
|
|
55
|
+
pk: user.pk,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
wallet,
|
|
59
|
+
});
|
|
60
|
+
const { buffer } = await client.encodeAccountMigrateTx({ tx, wallet });
|
|
61
|
+
const hash = await client.sendAccountMigrateTx(
|
|
62
|
+
{ tx, wallet },
|
|
63
|
+
{
|
|
64
|
+
headers: {
|
|
65
|
+
'x-gas-payer-sig': sign(info.wallet.address, info.wallet.secretKey, { txHash: toTxHash(buffer) }),
|
|
66
|
+
'x-gas-payer-pk': info.wallet.publicKey,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
logger.info('migration account done', { chain: chainHost, did: user.did, hash });
|
|
72
|
+
return hash;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function migrateAccount({ wallet, blocklet, user }) {
|
|
76
|
+
const chainHostList = [MAIN_CHAIN_ENDPOINT];
|
|
77
|
+
const { host: chainHost } = getBlockletChainInfo(blocklet);
|
|
78
|
+
if (chainHost && chainHost !== 'none') {
|
|
79
|
+
chainHostList.push(chainHost);
|
|
80
|
+
}
|
|
81
|
+
const waitingList = [...new Set(chainHostList)].map((item) =>
|
|
82
|
+
migrateAccountByChain({ chainHost: item, wallet, user, blocklet })
|
|
83
|
+
);
|
|
84
|
+
await Promise.all(waitingList);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function ensureStakedForGas({ client, chainHost, wallet }) {
|
|
88
|
+
// check if already staked
|
|
89
|
+
const address = toStakeAddress(wallet.address, wallet.address);
|
|
90
|
+
const { state: stake } = await client.getStakeState({ address });
|
|
91
|
+
if (stake) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// try stake for gas if we have enough balance
|
|
96
|
+
const { state: account } = await client.getAccountState({ address: wallet.address });
|
|
97
|
+
const result = await client.getForgeState({});
|
|
98
|
+
const { token, txConfig } = result.state;
|
|
99
|
+
const holding = (account?.tokens || []).find((x) => x.address === token.address);
|
|
100
|
+
if (toBN(holding?.value || '0').lte(toBN(txConfig.txGas.minStake))) {
|
|
101
|
+
throw new Error(`App do not have enough balance to stake for gas on chain: ${chainHost}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
const [hash] = await client.stake({
|
|
106
|
+
to: wallet.address,
|
|
107
|
+
message: 'stake-for-gas',
|
|
108
|
+
tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, token.decimal) }],
|
|
109
|
+
wallet,
|
|
110
|
+
});
|
|
111
|
+
logger.info(`App staked for gas on chain ${chainHost}`, { hash });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function getJWK(kid, jwksUri) {
|
|
115
|
+
const client = jwksClient({
|
|
116
|
+
cache: true,
|
|
117
|
+
jwksUri,
|
|
118
|
+
});
|
|
119
|
+
const key = await new Promise((resolve, reject) => {
|
|
120
|
+
client.getSigningKey(kid, (error, result) => {
|
|
121
|
+
if (error) {
|
|
122
|
+
return reject(error);
|
|
123
|
+
}
|
|
124
|
+
return resolve(result);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
publicKey: key.getPublicKey(),
|
|
129
|
+
kid: key.kid,
|
|
130
|
+
alg: key.alg,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Verify the authenticity of a token by decoding, verifying the algorithm, and matching claims.
|
|
136
|
+
* @author https://github.com/stefanprokopdev/verify-apple-id-token
|
|
137
|
+
* @param {Object} params - Object containing idToken, jwksUri, nonce, iss, clientId
|
|
138
|
+
* @param {string} params.idToken - JWT token
|
|
139
|
+
* @param {string} params.jwksUri - JWK set URI
|
|
140
|
+
* @param {string} params.nonce - Nonce
|
|
141
|
+
* @param {string} params.iss - Issuer
|
|
142
|
+
* @param {string} params.clientId - Client ID
|
|
143
|
+
* @return {Object} Decoded JWT claims if token is valid
|
|
144
|
+
*/
|
|
145
|
+
async function verifyIdToken(params) {
|
|
146
|
+
const decoded = jwt.decode(params.idToken, { complete: true });
|
|
147
|
+
const { kid, alg: jwtAlg } = decoded.header;
|
|
148
|
+
|
|
149
|
+
const { publicKey, alg: jwkAlg } = await getJWK(kid, params.jwksUri);
|
|
150
|
+
|
|
151
|
+
if (jwtAlg !== jwkAlg) {
|
|
152
|
+
throw new Error(`The alg does not match the jwk configuration - alg: ${jwtAlg} | expected: ${jwkAlg}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const jwtClaims = jwt.verify(params.idToken, publicKey, {
|
|
156
|
+
algorithms: [jwkAlg],
|
|
157
|
+
nonce: params.nonce,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (jwtClaims?.iss !== params.iss) {
|
|
161
|
+
throw new Error(`The iss does not match the Apple URL - iss: ${jwtClaims.iss} | expected: ${params.iss}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isFounded = [].concat(jwtClaims.aud).some((aud) => [].concat(params.clientId).includes(aud));
|
|
165
|
+
|
|
166
|
+
if (isFounded) {
|
|
167
|
+
['email_verified', 'is_private_email'].forEach((field) => {
|
|
168
|
+
if (jwtClaims[field] !== undefined) {
|
|
169
|
+
jwtClaims[field] = Boolean(jwtClaims[field]);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return jwtClaims;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(
|
|
177
|
+
`The aud parameter does not include this client - is: ${jwtClaims.aud} | expected: ${params.clientId}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
declareAccount,
|
|
183
|
+
migrateAccount,
|
|
184
|
+
verifyIdToken,
|
|
185
|
+
};
|
package/lib/util/federated.js
CHANGED
|
@@ -180,6 +180,18 @@ function generateSiteInfo({ nodeInfo, blocklet, domainAliases }) {
|
|
|
180
180
|
return siteInfo;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
async function migrateFederatedAccount({ blocklet, blockletInfo, fromUserDid, toUserDid, toUserPk }) {
|
|
184
|
+
const masterSite = getFederatedMaster(blocklet);
|
|
185
|
+
const { permanentWallet } = blockletInfo;
|
|
186
|
+
const result = await callFederated({
|
|
187
|
+
action: 'migrateAccount',
|
|
188
|
+
site: masterSite,
|
|
189
|
+
permanentWallet,
|
|
190
|
+
data: { fromUserDid, toUserDid, toUserPk },
|
|
191
|
+
});
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
183
195
|
module.exports = {
|
|
184
196
|
callFederated,
|
|
185
197
|
getFederatedSiteEnv,
|
|
@@ -191,4 +203,5 @@ module.exports = {
|
|
|
191
203
|
generateSiteInfo,
|
|
192
204
|
isMaster,
|
|
193
205
|
safeGetFederated,
|
|
206
|
+
migrateFederatedAccount,
|
|
194
207
|
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const isEmpty = require('lodash/isEmpty');
|
|
2
|
+
const { getActivePassports } = require('@abtnode/util/lib/passport');
|
|
3
|
+
const { VC_TYPE_NODE_PASSPORT, VC_TYPE_GENERAL_PASSPORT, NOTIFICATION_SEND_CHANNEL } = require('@abtnode/constant');
|
|
4
|
+
const { getUserAvatarUrl } = require('@abtnode/util/lib/user');
|
|
5
|
+
const { getBlockletAppIdList } = require('@blocklet/meta/lib/util');
|
|
6
|
+
const getNodeWallet = require('@abtnode/util/lib/get-app-wallet');
|
|
7
|
+
const pick = require('lodash/pick');
|
|
8
|
+
const uniq = require('lodash/uniq');
|
|
9
|
+
const { LOGIN_PROVIDER } = require('@blocklet/constant');
|
|
10
|
+
|
|
11
|
+
const { PASSPORT_LOG_ACTION, PASSPORT_ISSUE_ACTION } = require('@abtnode/constant');
|
|
12
|
+
const { sendToUser } = require('@blocklet/sdk/lib/util/send-notification');
|
|
13
|
+
const { getPassportStatusEndpoint, getApplicationInfo } = require('../auth');
|
|
14
|
+
const { createPassportVC, upsertToPassports, createUserPassport } = require('../passport');
|
|
15
|
+
|
|
16
|
+
const PASSPORT_VC_TYPES = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
|
|
17
|
+
|
|
18
|
+
const getPassportName = (passport) => {
|
|
19
|
+
return passport?.credentialSubject?.passport?.title || passport?.credentialSubject?.passport?.name || '';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
async function transferPassport(fromUser, toUser, { req, teamDid, node, nodeInfo, revokePassport = false, baseUrl }) {
|
|
23
|
+
if (!fromUser || !toUser || !teamDid) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const info = await getApplicationInfo({ node, nodeInfo, teamDid });
|
|
27
|
+
const { name: issuerName, wallet: issuerWallet, passportColor } = info;
|
|
28
|
+
|
|
29
|
+
const isService = teamDid !== nodeInfo.did;
|
|
30
|
+
|
|
31
|
+
let issuerDidList = uniq([info.wallet.address, ...getBlockletAppIdList(info)]);
|
|
32
|
+
|
|
33
|
+
let appUrl = null;
|
|
34
|
+
if (isService) {
|
|
35
|
+
const blockletInfo = await req.getBlockletInfo();
|
|
36
|
+
const { wallet: blockletWallet } = blockletInfo;
|
|
37
|
+
issuerDidList = uniq([blockletWallet.address, ...getBlockletAppIdList(blockletInfo)]);
|
|
38
|
+
appUrl = blockletInfo.appUrl;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const waitPassportList = getActivePassports(fromUser, issuerDidList);
|
|
42
|
+
|
|
43
|
+
if (waitPassportList.length === 0) {
|
|
44
|
+
// 没有待转移的 passport,无需进行下面的步骤
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const attachments = await Promise.all(
|
|
49
|
+
waitPassportList.map(async (x) => {
|
|
50
|
+
const _baseUrl = isService ? x.endpoint || appUrl : baseUrl || x.endpoint;
|
|
51
|
+
const vcParams = {
|
|
52
|
+
issuerName,
|
|
53
|
+
issuerWallet: isService ? issuerWallet : getNodeWallet(nodeInfo.sk),
|
|
54
|
+
ownerDid: toUser.did,
|
|
55
|
+
passport: { ...pick(x, ['name', 'title', 'specVersion']), endpoint: _baseUrl },
|
|
56
|
+
endpoint: getPassportStatusEndpoint({
|
|
57
|
+
baseUrl: _baseUrl,
|
|
58
|
+
userDid: toUser.did,
|
|
59
|
+
teamDid,
|
|
60
|
+
}),
|
|
61
|
+
ownerProfile: {
|
|
62
|
+
email: toUser.email,
|
|
63
|
+
fullName: toUser.fullName,
|
|
64
|
+
avatar: getUserAvatarUrl(_baseUrl, toUser.avatar),
|
|
65
|
+
},
|
|
66
|
+
preferredColor: passportColor,
|
|
67
|
+
types: !isService ? [VC_TYPE_NODE_PASSPORT] : x.types,
|
|
68
|
+
purpose: !isService || isEmpty(x.types) ? 'login' : 'verification',
|
|
69
|
+
display: x.display,
|
|
70
|
+
tag: info.did,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const vc = await createPassportVC(vcParams);
|
|
74
|
+
return {
|
|
75
|
+
type: 'vc',
|
|
76
|
+
data: {
|
|
77
|
+
credential: vc,
|
|
78
|
+
tag: x.name,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const insertPassportList = attachments.map((item, index) => {
|
|
85
|
+
const passport = createUserPassport(item.data.credential, { role: item.data.tag, display: item.data.display });
|
|
86
|
+
return {
|
|
87
|
+
...passport,
|
|
88
|
+
...pick(waitPassportList[index], ['firstLoginAt', 'lastLoginAt', 'lastLoginIp']),
|
|
89
|
+
userDid: toUser.did,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const passportNameList = attachments.map((x) => x.data.credential.name || getPassportName(x));
|
|
94
|
+
|
|
95
|
+
let sender = {};
|
|
96
|
+
if (!isService) {
|
|
97
|
+
sender = {
|
|
98
|
+
appDid: info.wallet.address,
|
|
99
|
+
appSk: info.wallet.secretKey,
|
|
100
|
+
};
|
|
101
|
+
} else {
|
|
102
|
+
const { wallet } = await req.getBlockletInfo();
|
|
103
|
+
sender = {
|
|
104
|
+
appDid: wallet.address,
|
|
105
|
+
appSk: wallet.secretKey,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await sendToUser(
|
|
110
|
+
toUser.did,
|
|
111
|
+
{
|
|
112
|
+
title: 'Transfer passports',
|
|
113
|
+
body: `You received passport ${passportNameList.join(', ')} because you just bind your DID Wallet to ${
|
|
114
|
+
fromUser.email
|
|
115
|
+
}`,
|
|
116
|
+
attachments,
|
|
117
|
+
},
|
|
118
|
+
sender,
|
|
119
|
+
{
|
|
120
|
+
channels: [NOTIFICATION_SEND_CHANNEL.WALLET, NOTIFICATION_SEND_CHANNEL.PUSH], // 不需要推送到其他 channels
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const passports = insertPassportList.reduce((acc, item) => {
|
|
125
|
+
return upsertToPassports(acc, item);
|
|
126
|
+
}, fromUser.passports || []);
|
|
127
|
+
const toUserExist = await node.getUser({
|
|
128
|
+
teamDid,
|
|
129
|
+
user: { did: toUser.did },
|
|
130
|
+
options: {
|
|
131
|
+
enableConnectedAccount: false,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// HACK: 默认情况下,应该将新 passport 添加到 toUser,但某些情况下,toUser 是绑定到 fromUser 的 connectAccount,所以需要将新通行证添加到 fromUser
|
|
135
|
+
if (toUserExist) {
|
|
136
|
+
await node.updateUser({
|
|
137
|
+
teamDid,
|
|
138
|
+
user: {
|
|
139
|
+
did: toUser.did,
|
|
140
|
+
pk: toUser.pk,
|
|
141
|
+
passports,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
await node.updateUser({
|
|
146
|
+
teamDid,
|
|
147
|
+
user: {
|
|
148
|
+
did: fromUser.did,
|
|
149
|
+
pk: fromUser.pk,
|
|
150
|
+
passports,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await Promise.all(
|
|
156
|
+
insertPassportList.map((item) => {
|
|
157
|
+
return node.createPassportLog(
|
|
158
|
+
teamDid,
|
|
159
|
+
{
|
|
160
|
+
passportId: item.id,
|
|
161
|
+
action: PASSPORT_LOG_ACTION.ISSUE,
|
|
162
|
+
operatorDid: fromUser.did,
|
|
163
|
+
metadata: {
|
|
164
|
+
action: PASSPORT_ISSUE_ACTION.ISSUE_ON_TRANSFER,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
req
|
|
168
|
+
);
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (revokePassport) {
|
|
173
|
+
const revokePendingList = waitPassportList
|
|
174
|
+
.filter((item) => item.id)
|
|
175
|
+
.map((item) => {
|
|
176
|
+
if (fromUser.sourceProvider === LOGIN_PROVIDER.AUTH0) {
|
|
177
|
+
return node.removeUserPassport({ teamDid, userDid: fromUser.did, passportId: item.id });
|
|
178
|
+
}
|
|
179
|
+
return node.revokeUserPassport({ teamDid, userDid: fromUser.did, passportId: item.id });
|
|
180
|
+
});
|
|
181
|
+
await Promise.all(revokePendingList);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
transferPassport,
|
|
187
|
+
PASSPORT_VC_TYPES,
|
|
188
|
+
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.16.45-beta-
|
|
6
|
+
"version": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
7
7
|
"description": "Simple lib to manage auth in ABT Node",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -20,30 +20,33 @@
|
|
|
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.45-beta-
|
|
24
|
-
"@abtnode/logger": "1.16.45-beta-
|
|
25
|
-
"@abtnode/util": "1.16.45-beta-
|
|
23
|
+
"@abtnode/constant": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
24
|
+
"@abtnode/logger": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
25
|
+
"@abtnode/util": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
26
26
|
"@arcblock/did": "1.20.14",
|
|
27
27
|
"@arcblock/did-auth": "1.20.14",
|
|
28
|
+
"@arcblock/did-ext": "1.20.14",
|
|
29
|
+
"@arcblock/did-util": "1.20.14",
|
|
28
30
|
"@arcblock/jwt": "1.20.14",
|
|
29
|
-
"@arcblock/nft-display": "^2.13.
|
|
31
|
+
"@arcblock/nft-display": "^2.13.70",
|
|
30
32
|
"@arcblock/validator": "1.20.14",
|
|
31
33
|
"@arcblock/vc": "1.20.14",
|
|
32
|
-
"@blocklet/constant": "1.16.45-beta-
|
|
34
|
+
"@blocklet/constant": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
33
35
|
"@blocklet/error": "^0.2.5",
|
|
34
|
-
"@blocklet/meta": "1.16.45-beta-
|
|
35
|
-
"@blocklet/sdk": "1.16.45-beta-
|
|
36
|
+
"@blocklet/meta": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
37
|
+
"@blocklet/sdk": "1.16.45-beta-20250618-073451-6e48fb62",
|
|
36
38
|
"@ocap/client": "1.20.14",
|
|
37
39
|
"@ocap/mcrypto": "1.20.14",
|
|
38
40
|
"@ocap/util": "1.20.14",
|
|
39
41
|
"@ocap/wallet": "1.20.14",
|
|
40
|
-
"@simplewebauthn/server": "^13.
|
|
42
|
+
"@simplewebauthn/server": "^13.1.1",
|
|
41
43
|
"axios": "^1.7.9",
|
|
42
44
|
"flat": "^5.0.2",
|
|
43
45
|
"fs-extra": "^11.2.0",
|
|
44
46
|
"is-url": "^1.2.4",
|
|
45
47
|
"joi": "17.12.2",
|
|
46
48
|
"jsonwebtoken": "^9.0.0",
|
|
49
|
+
"jwks-rsa": "^3.1.0",
|
|
47
50
|
"lodash": "^4.17.21",
|
|
48
51
|
"p-retry": "^4.6.2",
|
|
49
52
|
"semver": "^7.6.3",
|
|
@@ -53,5 +56,5 @@
|
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"jest": "^29.7.0"
|
|
55
58
|
},
|
|
56
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "afbde3291aeb63211f12b73b6c8cf5dc422cbf7f"
|
|
57
60
|
}
|