@abtnode/core 1.16.14-beta-1936d3d0 → 1.16.14-beta-dd4f6a50
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/blocklet/manager/disk.js +138 -29
- package/lib/index.js +1 -0
- package/lib/states/audit-log.js +15 -6
- package/lib/util/blocklet.js +1 -0
- package/package.json +19 -19
|
@@ -51,6 +51,7 @@ const { update: updateMetaFile } = require('@blocklet/meta/lib/file');
|
|
|
51
51
|
const { titleSchema, updateMountPointSchema, environmentNameSchema } = require('@blocklet/meta/lib/schema');
|
|
52
52
|
const { emailConfigSchema } = require('@blocklet/sdk/lib/validators/email');
|
|
53
53
|
const Lock = require('@abtnode/util/lib/lock');
|
|
54
|
+
const defaults = require('lodash/defaults');
|
|
54
55
|
|
|
55
56
|
const {
|
|
56
57
|
BlockletStatus,
|
|
@@ -1405,6 +1406,9 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
1405
1406
|
|
|
1406
1407
|
if (validateConfig.cacheTtl) {
|
|
1407
1408
|
sessionConfig.cacheTtl = validateConfig.cacheTtl;
|
|
1409
|
+
} else {
|
|
1410
|
+
// NOTE: 将 cacheTtl 默认值设置为 3600s
|
|
1411
|
+
sessionConfig.cacheTtl = 60 * 60; // seconds
|
|
1408
1412
|
}
|
|
1409
1413
|
if (validateConfig.ttl) {
|
|
1410
1414
|
sessionConfig.ttl = validateConfig.ttl;
|
|
@@ -3416,21 +3420,24 @@ class DiskBlockletManager extends BaseBlockletManager {
|
|
|
3416
3420
|
class FederatedBlockletManager extends DiskBlockletManager {
|
|
3417
3421
|
/**
|
|
3418
3422
|
* Joins federated login.
|
|
3419
|
-
* @param {
|
|
3420
|
-
* @
|
|
3423
|
+
* @param {object} options
|
|
3424
|
+
* @param {{string}} options.did - blocklet pid
|
|
3425
|
+
* @param {{string}} options.appUrl - 申请加入统一登录的 master appUrl
|
|
3426
|
+
* @returns {Promise<any>} 更新后的 blocklet 数据
|
|
3421
3427
|
*/
|
|
3422
3428
|
async joinFederatedLogin({ appUrl, did }) {
|
|
3423
3429
|
const url = new URL(appUrl);
|
|
3430
|
+
// master service api 的地址
|
|
3424
3431
|
url.pathname = `${WELLKNOWN_SERVICE_PATH_PREFIX}/api/federated/join`;
|
|
3425
3432
|
|
|
3426
3433
|
const blocklet = await this.getBlocklet(did);
|
|
3427
3434
|
const nodeInfo = await states.node.read();
|
|
3428
3435
|
const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
|
|
3429
|
-
const { permanentWallet
|
|
3436
|
+
const { permanentWallet } = blockletInfo;
|
|
3430
3437
|
const memberSite = {
|
|
3431
|
-
appId:
|
|
3432
|
-
appPid:
|
|
3433
|
-
|
|
3438
|
+
appId: blocklet.appDid,
|
|
3439
|
+
appPid: blocklet.appPid,
|
|
3440
|
+
aliasDid: (blocklet.migratedFrom || []).map((item) => item.appDid),
|
|
3434
3441
|
appName: blockletInfo.name,
|
|
3435
3442
|
appDescription: blockletInfo.description,
|
|
3436
3443
|
appUrl: blockletInfo.appUrl,
|
|
@@ -3439,33 +3446,99 @@ class FederatedBlockletManager extends DiskBlockletManager {
|
|
|
3439
3446
|
normalizePathPrefix(`${WELLKNOWN_SERVICE_PATH_PREFIX}/blocklet/logo`) ||
|
|
3440
3447
|
'/',
|
|
3441
3448
|
appLogoRect: blocklet.environmentObj.BLOCKLET_APP_LOGO_RECT,
|
|
3442
|
-
did:
|
|
3449
|
+
did: permanentWallet.address,
|
|
3443
3450
|
pk: permanentWallet.publicKey,
|
|
3444
3451
|
serverId: nodeInfo.did,
|
|
3445
3452
|
serverVersion: nodeInfo.version,
|
|
3446
3453
|
version: blockletInfo.version,
|
|
3447
3454
|
};
|
|
3448
3455
|
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3456
|
+
logger.info('Apply to join federated login', {
|
|
3457
|
+
memberSite: pick(memberSite, ['appId', 'appPid', 'appName', 'appDescription', 'appUrl']),
|
|
3458
|
+
masterAppUrl: appUrl,
|
|
3452
3459
|
});
|
|
3453
|
-
|
|
3460
|
+
|
|
3461
|
+
let data;
|
|
3462
|
+
try {
|
|
3463
|
+
const result = await request.post(url.href, {
|
|
3464
|
+
// 初次申请时,member 不在站点群中,不需要对数据进行加密
|
|
3465
|
+
site: memberSite,
|
|
3466
|
+
});
|
|
3467
|
+
data = result.data;
|
|
3468
|
+
} catch (error) {
|
|
3469
|
+
logger.error('Failed to join federated login', { error, did, url: url.href });
|
|
3470
|
+
throw error;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
await states.blockletExtras.setSettings(blocklet.appPid, {
|
|
3454
3474
|
federated: {
|
|
3455
3475
|
config: {
|
|
3456
3476
|
appId: blocklet.appDid,
|
|
3457
|
-
appPid: blocklet.appPid
|
|
3477
|
+
appPid: blocklet.appPid,
|
|
3458
3478
|
isMaster: false,
|
|
3459
3479
|
},
|
|
3460
3480
|
sites: data.sites,
|
|
3461
3481
|
},
|
|
3462
3482
|
});
|
|
3463
3483
|
|
|
3484
|
+
const newState = await this.getBlocklet(did);
|
|
3485
|
+
this.emit(BlockletEvents.updated, {
|
|
3486
|
+
masterAppUrl: appUrl,
|
|
3487
|
+
});
|
|
3488
|
+
return newState;
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
async quitFederatedLogin({ did }) {
|
|
3492
|
+
const blocklet = await this.getBlocklet(did);
|
|
3493
|
+
const federated = defaults(cloneDeep(blocklet.settings.federated || {}), {
|
|
3494
|
+
config: {},
|
|
3495
|
+
sites: [],
|
|
3496
|
+
});
|
|
3497
|
+
const masterSite = federated.sites[0];
|
|
3498
|
+
if (masterSite && masterSite.isMaster !== false) {
|
|
3499
|
+
const nodeInfo = await states.node.read();
|
|
3500
|
+
const blockletInfo = getBlockletInfo(blocklet, nodeInfo.sk);
|
|
3501
|
+
const { permanentWallet } = blockletInfo;
|
|
3502
|
+
logger.info('Quit federated login', {
|
|
3503
|
+
memberSite: {
|
|
3504
|
+
appId: blocklet.appDid,
|
|
3505
|
+
appPid: blocklet.appPid,
|
|
3506
|
+
appName: blockletInfo.name,
|
|
3507
|
+
appDescription: blockletInfo.description,
|
|
3508
|
+
appUrl: blockletInfo.appUrl,
|
|
3509
|
+
},
|
|
3510
|
+
masterAppUrl: masterSite.appUrl,
|
|
3511
|
+
});
|
|
3512
|
+
const url = new URL(masterSite.appUrl);
|
|
3513
|
+
url.pathname = `${WELLKNOWN_SERVICE_PATH_PREFIX}/api/federated/quit`;
|
|
3514
|
+
try {
|
|
3515
|
+
await request.post(url.href, {
|
|
3516
|
+
signer: permanentWallet.address,
|
|
3517
|
+
data: signV2(permanentWallet.address, permanentWallet.secretKey, {
|
|
3518
|
+
memberPid: blocklet.appPid,
|
|
3519
|
+
}),
|
|
3520
|
+
});
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
logger.error('Failed to quit blocklet', { error, did, url: url.href });
|
|
3523
|
+
throw error;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
await states.blockletExtras.setSettings(blocklet.appPid, {
|
|
3528
|
+
federated: null,
|
|
3529
|
+
});
|
|
3464
3530
|
const newState = await this.getBlocklet(did);
|
|
3465
3531
|
this.emit(BlockletEvents.updated, newState);
|
|
3466
3532
|
return newState;
|
|
3467
3533
|
}
|
|
3468
3534
|
|
|
3535
|
+
/**
|
|
3536
|
+
* 更改 federated 配置
|
|
3537
|
+
* @param {object} param
|
|
3538
|
+
* @param {string} param.did blocklet pid
|
|
3539
|
+
* @param {object} param.config federated 配置内容
|
|
3540
|
+
* @returns {Promise<any>} 更新后的 blocklet 数据
|
|
3541
|
+
*/
|
|
3469
3542
|
async setFederated({ did, config }) {
|
|
3470
3543
|
await states.blockletExtras.setSettings(did, { federated: config });
|
|
3471
3544
|
|
|
@@ -3474,6 +3547,13 @@ class FederatedBlockletManager extends DiskBlockletManager {
|
|
|
3474
3547
|
return newState;
|
|
3475
3548
|
}
|
|
3476
3549
|
|
|
3550
|
+
/**
|
|
3551
|
+
* 配置 federated 设置
|
|
3552
|
+
* @param {object} param
|
|
3553
|
+
* @param {string} param.did blocklet pid
|
|
3554
|
+
* @param {boolean} param.autoLogin 是否自动登录
|
|
3555
|
+
* @returns {Promise<any>} 更新后的 blocklet 数据
|
|
3556
|
+
*/
|
|
3477
3557
|
async configFederated({ did, autoLogin = false }) {
|
|
3478
3558
|
const blocklet = await this.getBlocklet(did);
|
|
3479
3559
|
const federated = cloneDeep(blocklet.settings.federated || {});
|
|
@@ -3485,23 +3565,40 @@ class FederatedBlockletManager extends DiskBlockletManager {
|
|
|
3485
3565
|
return newState;
|
|
3486
3566
|
}
|
|
3487
3567
|
|
|
3488
|
-
|
|
3568
|
+
/**
|
|
3569
|
+
* 审核 federated 申请
|
|
3570
|
+
* @param {object} param
|
|
3571
|
+
* @param {string} param.did master blocklet pid
|
|
3572
|
+
* @param {string} param.memberPid member blocklet pid
|
|
3573
|
+
* @param {boolean} param.autoLogin 是否自动登录
|
|
3574
|
+
* @returns {Promise<any>} 更新后的 blocklet 数据
|
|
3575
|
+
*/
|
|
3576
|
+
async auditFederatedLogin({ memberPid, did, status }) {
|
|
3489
3577
|
const blocklet = await this.getBlocklet(did);
|
|
3490
3578
|
|
|
3491
|
-
const federated = cloneDeep(blocklet.settings.federated || {})
|
|
3492
|
-
|
|
3579
|
+
const federated = defaults(cloneDeep(blocklet.settings.federated || {}), {
|
|
3580
|
+
config: {},
|
|
3581
|
+
sites: [],
|
|
3582
|
+
});
|
|
3583
|
+
const memberSite = federated.sites.find((item) => item.appPid === memberPid);
|
|
3493
3584
|
memberSite.status = status;
|
|
3494
3585
|
if ([null, undefined].includes(federated.config.isMaster)) {
|
|
3495
|
-
const
|
|
3586
|
+
const { sites, config } = federated;
|
|
3587
|
+
const masterSite = sites.find((item) => item.appPid === blocklet.appPid);
|
|
3496
3588
|
|
|
3497
3589
|
masterSite.isMaster = true;
|
|
3498
|
-
|
|
3590
|
+
config.isMaster = true;
|
|
3499
3591
|
}
|
|
3500
3592
|
// 有审批操作的一方,自动成为 master
|
|
3501
3593
|
const newState = await this.setFederated({
|
|
3502
|
-
did: blocklet.
|
|
3594
|
+
did: blocklet.appPid,
|
|
3503
3595
|
config: federated,
|
|
3504
3596
|
});
|
|
3597
|
+
logger.info('Audit member join federated login', {
|
|
3598
|
+
memberSite: pick(memberSite, ['appId', 'appPid', 'appName', 'appDescription', 'appUrl']),
|
|
3599
|
+
status,
|
|
3600
|
+
});
|
|
3601
|
+
|
|
3505
3602
|
const nodeInfo = await states.node.read();
|
|
3506
3603
|
const { permanentWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
|
|
3507
3604
|
let delegation;
|
|
@@ -3520,25 +3617,37 @@ class FederatedBlockletManager extends DiskBlockletManager {
|
|
|
3520
3617
|
'agreement',
|
|
3521
3618
|
'verifiableCredential',
|
|
3522
3619
|
'asset',
|
|
3523
|
-
|
|
3524
|
-
// 'encryptionKey',
|
|
3620
|
+
'keyPair',
|
|
3621
|
+
// 'encryptionKey', // 备份还原应用时使用
|
|
3525
3622
|
],
|
|
3526
3623
|
},
|
|
3527
3624
|
],
|
|
3528
3625
|
exp: Math.floor(new Date().getTime() / 1000) + 86400 * 365 * 100, // valid for 100 year
|
|
3529
3626
|
});
|
|
3530
|
-
roles = await this.teamManager.getRoles(blocklet.
|
|
3627
|
+
roles = await this.teamManager.getRoles(blocklet.appPid);
|
|
3531
3628
|
}
|
|
3532
3629
|
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
delegation,
|
|
3539
|
-
roles: roles.map((item) => pick(item, ['name', 'title', 'description'])),
|
|
3540
|
-
}),
|
|
3630
|
+
const postUrl = `${memberSite.appUrl}/${WELLKNOWN_SERVICE_PATH_PREFIX}/api/federated/audit-res`;
|
|
3631
|
+
|
|
3632
|
+
logger.info('Audit member join federated login', {
|
|
3633
|
+
status,
|
|
3634
|
+
postUrl,
|
|
3541
3635
|
});
|
|
3636
|
+
try {
|
|
3637
|
+
const roleList = roles.map((item) => pick(item, ['name', 'title', 'description']));
|
|
3638
|
+
await request.post(postUrl, {
|
|
3639
|
+
signer: permanentWallet.address,
|
|
3640
|
+
data: signV2(permanentWallet.address, permanentWallet.secretKey, {
|
|
3641
|
+
masterPid: blocklet.appPid,
|
|
3642
|
+
status,
|
|
3643
|
+
delegation,
|
|
3644
|
+
roles: roleList,
|
|
3645
|
+
}),
|
|
3646
|
+
});
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
logger.error('Failed to post audit res to member-site', { error, did, url: postUrl });
|
|
3649
|
+
throw error;
|
|
3650
|
+
}
|
|
3542
3651
|
const waitingList = federated.sites
|
|
3543
3652
|
.filter((item) => item.appId !== federated.config.appId)
|
|
3544
3653
|
.map((item) => {
|
package/lib/index.js
CHANGED
|
@@ -259,6 +259,7 @@ function ABTNode(options) {
|
|
|
259
259
|
configNavigations: blockletManager.configNavigations.bind(blockletManager),
|
|
260
260
|
configOAuth: blockletManager.configOAuth.bind(blockletManager),
|
|
261
261
|
joinFederatedLogin: blockletManager.joinFederatedLogin.bind(blockletManager),
|
|
262
|
+
quitFederatedLogin: blockletManager.quitFederatedLogin.bind(blockletManager),
|
|
262
263
|
auditFederatedLogin: blockletManager.auditFederatedLogin.bind(blockletManager),
|
|
263
264
|
configFederated: blockletManager.configFederated.bind(blockletManager),
|
|
264
265
|
setFederated: blockletManager.setFederated.bind(blockletManager),
|
package/lib/states/audit-log.js
CHANGED
|
@@ -138,16 +138,21 @@ const getLogContent = async (action, args, context, result, info, node) => {
|
|
|
138
138
|
return `set publicToStore to ${args.publicToStore ? 'true' : 'false'}`;
|
|
139
139
|
case 'configNavigations':
|
|
140
140
|
// eslint-disable-next-line prettier/prettier
|
|
141
|
-
return `updated following navigations:\n${args.navigations.map(
|
|
142
|
-
(x) => `- ${x.title}: ${x.link}\n`
|
|
143
|
-
)}`;
|
|
141
|
+
return `updated following navigations:\n${args.navigations.map((x) => `- ${x.title}: ${x.link}\n`)}`;
|
|
144
142
|
case 'configOAuth':
|
|
145
143
|
return `updated following OAuth for blocklet ${getBlockletInfo(result, info)}:\n${args.oauth}`;
|
|
146
|
-
// TODO: @zhanghan 配置审计日志
|
|
147
144
|
case 'joinFederatedLogin':
|
|
148
|
-
return `
|
|
145
|
+
return `blocklet ${getBlockletInfo(result, info)} join federated login to ${args.appUrl}`;
|
|
146
|
+
case 'quitFederatedLogin':
|
|
147
|
+
return `blocklet ${getBlockletInfo(result, info)} quit federated login`;
|
|
149
148
|
case 'auditFederatedLogin':
|
|
150
|
-
return `
|
|
149
|
+
return `blocklet ${getBlockletInfo(result, info)} audit federated login member ${args.memberPid} with status: ${
|
|
150
|
+
args.status
|
|
151
|
+
}`;
|
|
152
|
+
case 'configFederated':
|
|
153
|
+
return `blocklet ${getBlockletInfo(result, info)} config federated login settings with autoLogin: ${
|
|
154
|
+
args.autoLogin
|
|
155
|
+
}`;
|
|
151
156
|
case 'configNotification':
|
|
152
157
|
return `updated following notification setting: ${args.notification}`;
|
|
153
158
|
case 'updateComponentTitle':
|
|
@@ -315,6 +320,10 @@ const getLogCategory = (action) => {
|
|
|
315
320
|
case 'updateComponentTitle':
|
|
316
321
|
case 'updateComponentMountPoint':
|
|
317
322
|
case 'backupToSpaces':
|
|
323
|
+
case 'joinFederatedLogin':
|
|
324
|
+
case 'quitFederatedLogin':
|
|
325
|
+
case 'auditFederatedLogin':
|
|
326
|
+
case 'configFederated':
|
|
318
327
|
return 'blocklet';
|
|
319
328
|
|
|
320
329
|
// store,此处应该返回 server
|
package/lib/util/blocklet.js
CHANGED
|
@@ -1229,6 +1229,7 @@ const getBlocklet = async ({
|
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
1231
1231
|
// app settings
|
|
1232
|
+
// FIXME: @zhanghan 在 server 开发模式下,使用 `node /workspace/arcblock/blocklet-server/core/cli/tools/dev.js` 运行的 blocklet,blocklet.meta.did 和 blocklet.appPid 是不一致的
|
|
1232
1233
|
const settings = await states.blockletExtras.getSettings(blocklet.meta.did);
|
|
1233
1234
|
blocklet.trustedPassports = get(settings, 'trustedPassports') || [];
|
|
1234
1235
|
blocklet.trustedFactories = (get(settings, 'trustedFactories') || []).map((x) => {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.16.14-beta-
|
|
6
|
+
"version": "1.16.14-beta-dd4f6a50",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@abtnode/analytics": "1.16.14-beta-
|
|
23
|
-
"@abtnode/auth": "1.16.14-beta-
|
|
24
|
-
"@abtnode/certificate-manager": "1.16.14-beta-
|
|
25
|
-
"@abtnode/constant": "1.16.14-beta-
|
|
26
|
-
"@abtnode/cron": "1.16.14-beta-
|
|
27
|
-
"@abtnode/logger": "1.16.14-beta-
|
|
28
|
-
"@abtnode/models": "1.16.14-beta-
|
|
29
|
-
"@abtnode/queue": "1.16.14-beta-
|
|
30
|
-
"@abtnode/rbac": "1.16.14-beta-
|
|
31
|
-
"@abtnode/router-provider": "1.16.14-beta-
|
|
32
|
-
"@abtnode/static-server": "1.16.14-beta-
|
|
33
|
-
"@abtnode/timemachine": "1.16.14-beta-
|
|
34
|
-
"@abtnode/util": "1.16.14-beta-
|
|
22
|
+
"@abtnode/analytics": "1.16.14-beta-dd4f6a50",
|
|
23
|
+
"@abtnode/auth": "1.16.14-beta-dd4f6a50",
|
|
24
|
+
"@abtnode/certificate-manager": "1.16.14-beta-dd4f6a50",
|
|
25
|
+
"@abtnode/constant": "1.16.14-beta-dd4f6a50",
|
|
26
|
+
"@abtnode/cron": "1.16.14-beta-dd4f6a50",
|
|
27
|
+
"@abtnode/logger": "1.16.14-beta-dd4f6a50",
|
|
28
|
+
"@abtnode/models": "1.16.14-beta-dd4f6a50",
|
|
29
|
+
"@abtnode/queue": "1.16.14-beta-dd4f6a50",
|
|
30
|
+
"@abtnode/rbac": "1.16.14-beta-dd4f6a50",
|
|
31
|
+
"@abtnode/router-provider": "1.16.14-beta-dd4f6a50",
|
|
32
|
+
"@abtnode/static-server": "1.16.14-beta-dd4f6a50",
|
|
33
|
+
"@abtnode/timemachine": "1.16.14-beta-dd4f6a50",
|
|
34
|
+
"@abtnode/util": "1.16.14-beta-dd4f6a50",
|
|
35
35
|
"@arcblock/did": "1.18.87",
|
|
36
36
|
"@arcblock/did-auth": "1.18.87",
|
|
37
37
|
"@arcblock/did-ext": "^1.18.87",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"@arcblock/pm2-events": "^0.0.5",
|
|
43
43
|
"@arcblock/validator": "^1.18.87",
|
|
44
44
|
"@arcblock/vc": "1.18.87",
|
|
45
|
-
"@blocklet/constant": "1.16.14-beta-
|
|
46
|
-
"@blocklet/meta": "1.16.14-beta-
|
|
47
|
-
"@blocklet/resolver": "1.16.14-beta-
|
|
48
|
-
"@blocklet/sdk": "1.16.14-beta-
|
|
45
|
+
"@blocklet/constant": "1.16.14-beta-dd4f6a50",
|
|
46
|
+
"@blocklet/meta": "1.16.14-beta-dd4f6a50",
|
|
47
|
+
"@blocklet/resolver": "1.16.14-beta-dd4f6a50",
|
|
48
|
+
"@blocklet/sdk": "1.16.14-beta-dd4f6a50",
|
|
49
49
|
"@did-space/client": "^0.2.129",
|
|
50
50
|
"@fidm/x509": "^1.2.1",
|
|
51
51
|
"@ocap/mcrypto": "1.18.87",
|
|
@@ -99,5 +99,5 @@
|
|
|
99
99
|
"jest": "^27.5.1",
|
|
100
100
|
"unzipper": "^0.10.11"
|
|
101
101
|
},
|
|
102
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "fb3858f842b221f0a8c298307b393d55c555af45"
|
|
103
103
|
}
|