@abtnode/core 1.6.0 → 1.6.4
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/api/node.js +1 -1
- package/lib/blocklet/extras.js +26 -1
- package/lib/blocklet/manager/disk.js +1 -1
- package/lib/blocklet/registry.js +2 -2
- package/lib/index.js +8 -1
- package/lib/migrations/1.6.4-security.js +43 -0
- package/lib/router/index.js +1 -1
- package/lib/states/blocklet-extras.js +20 -15
- package/lib/states/node.js +7 -2
- package/lib/util/blocklet.js +1 -1
- package/lib/util/get-ip-dns-domain-for-blocklet.js +1 -1
- package/lib/util/requirement.js +2 -1
- package/package.json +18 -18
package/lib/api/node.js
CHANGED
|
@@ -114,7 +114,7 @@ class NodeAPI {
|
|
|
114
114
|
const info = await this.state.read();
|
|
115
115
|
const env = await this.state.getEnvironments();
|
|
116
116
|
info.environments = Object.keys(env).map((x) => ({ key: x, value: env[x] }));
|
|
117
|
-
info.uptime =
|
|
117
|
+
info.uptime = process.uptime() * 1000;
|
|
118
118
|
|
|
119
119
|
return info;
|
|
120
120
|
}
|
package/lib/blocklet/extras.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const uniqBy = require('lodash/uniqBy');
|
|
2
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
3
|
+
const security = require('@abtnode/util/lib/security');
|
|
2
4
|
const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/meta/lib/constants');
|
|
3
5
|
|
|
4
|
-
const mergeConfigs = (oldConfigs, newConfigs = []) => {
|
|
6
|
+
const mergeConfigs = ({ old: oldConfigs, cur: newConfigs = [], did = '', dek = '' }) => {
|
|
5
7
|
const metaConfigs = (oldConfigs || []).filter((x) => !x.custom);
|
|
6
8
|
const customConfigs = (oldConfigs || []).filter((x) => x.custom);
|
|
7
9
|
|
|
@@ -23,6 +25,14 @@ const mergeConfigs = (oldConfigs, newConfigs = []) => {
|
|
|
23
25
|
return acc;
|
|
24
26
|
}, {});
|
|
25
27
|
|
|
28
|
+
if (dek && did) {
|
|
29
|
+
newConfigs.forEach((x) => {
|
|
30
|
+
if (x.secure) {
|
|
31
|
+
x.value = security.encrypt(x.value, did, dek);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
// newConfig 为用户传的,也可以是从环境变量中去读的
|
|
27
37
|
const uniqConfigs = uniqBy(newConfigs, (x) => x.key || x.name);
|
|
28
38
|
|
|
@@ -105,6 +115,21 @@ const mergeConfigs = (oldConfigs, newConfigs = []) => {
|
|
|
105
115
|
return mergedConfig;
|
|
106
116
|
};
|
|
107
117
|
|
|
118
|
+
const parseConfigs = ({ data, did, dek }) => {
|
|
119
|
+
if (dek && did && Array.isArray(data)) {
|
|
120
|
+
return cloneDeep(data).map((x) => {
|
|
121
|
+
if (x.secure) {
|
|
122
|
+
x.value = security.decrypt(x.value, did, dek);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return x;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return data;
|
|
130
|
+
};
|
|
131
|
+
|
|
108
132
|
module.exports = {
|
|
109
133
|
mergeConfigs,
|
|
134
|
+
parseConfigs,
|
|
110
135
|
};
|
|
@@ -23,7 +23,7 @@ const { getVcFromPresentation } = require('@abtnode/util/lib/vc');
|
|
|
23
23
|
const { BLOCKLET_PURCHASE_NFT_TYPE } = require('@abtnode/constant');
|
|
24
24
|
|
|
25
25
|
const getBlockletEngine = require('@blocklet/meta/lib/engine');
|
|
26
|
-
const { isFreeBlocklet } = require('@blocklet/meta/lib/
|
|
26
|
+
const { isFreeBlocklet } = require('@blocklet/meta/lib/util');
|
|
27
27
|
const validateBlockletEntry = require('@blocklet/meta/lib/entry');
|
|
28
28
|
const { getRequiredMissingConfigs } = require('@blocklet/meta/lib/util');
|
|
29
29
|
|
package/lib/blocklet/registry.js
CHANGED
|
@@ -3,7 +3,7 @@ const { BlockletGroup } = require('@blocklet/meta/lib/constants');
|
|
|
3
3
|
const joinURL = require('url-join');
|
|
4
4
|
const get = require('lodash/get');
|
|
5
5
|
const pick = require('lodash/pick');
|
|
6
|
-
const { BLOCKLET_STORE_API_PREFIX } = require('@abtnode/constant');
|
|
6
|
+
const { BLOCKLET_STORE_API_PREFIX, BLOCKLET_STORE_META_PATH } = require('@abtnode/constant');
|
|
7
7
|
|
|
8
8
|
const { name } = require('../../package.json');
|
|
9
9
|
|
|
@@ -170,7 +170,7 @@ BlockletRegistry.validateRegistryURL = async (registry) => {
|
|
|
170
170
|
|
|
171
171
|
BlockletRegistry.getRegistryMeta = async (registry) => {
|
|
172
172
|
try {
|
|
173
|
-
const url = joinURL(registry,
|
|
173
|
+
const url = joinURL(registry, BLOCKLET_STORE_META_PATH, `?__t__=${Date.now()}`);
|
|
174
174
|
const { data } = await request.get(url);
|
|
175
175
|
|
|
176
176
|
if (!data) {
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const md5 = require('@abtnode/util/lib/md5');
|
|
3
4
|
const Cron = require('@abtnode/cron');
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ const {
|
|
|
9
10
|
fromBlockletSource,
|
|
10
11
|
toBlockletSource,
|
|
11
12
|
} = require('@blocklet/meta/lib/constants');
|
|
13
|
+
const { listProviders } = require('@abtnode/router-provider');
|
|
12
14
|
|
|
13
15
|
const RoutingSnapshot = require('./states/routing-snapshot');
|
|
14
16
|
const BlockletRegistry = require('./blocklet/registry');
|
|
@@ -53,6 +55,11 @@ function ABTNode(options) {
|
|
|
53
55
|
throw new Error('Can not initialize ABTNode without dataDir');
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
const ekFile = path.join(options.dataDir, '.sock');
|
|
59
|
+
if (fs.existsSync(ekFile)) {
|
|
60
|
+
options.dek = fs.readFileSync(ekFile);
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
if (typeof options.daemon === 'undefined') {
|
|
57
64
|
options.daemon = false;
|
|
58
65
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
7
|
+
const security = require('@abtnode/util/lib/security');
|
|
8
|
+
|
|
9
|
+
module.exports = async ({ states, configFile, dataDir }) => {
|
|
10
|
+
if (process.env.CI) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const file = path.join(dataDir, '.sock');
|
|
15
|
+
if (fs.existsSync(file)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(file, crypto.randomBytes(32), { encoding: 'binary', mode: '0600' });
|
|
21
|
+
|
|
22
|
+
const config = yaml.safeLoad(fs.readFileSync(configFile).toString(), { json: true });
|
|
23
|
+
config.node.sk = security.encrypt(config.node.sk, config.node.did, fs.readFileSync(file));
|
|
24
|
+
fs.writeFileSync(configFile, yaml.dump(config));
|
|
25
|
+
await states.node.updateNodeInfo({ sk: config.node.sk });
|
|
26
|
+
|
|
27
|
+
const items = await states.blockletExtras.find();
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
const newConfigs = cloneDeep(item.configs || []).map((c) => {
|
|
30
|
+
if (c.secure) {
|
|
31
|
+
c.value = security.encrypt(c.value, item.did, fs.readFileSync(file));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return c;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await states.blockletExtras.update({ did: item.did }, { $set: { configs: newConfigs } });
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(err);
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
};
|
package/lib/router/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const get = require('lodash/get');
|
|
2
|
+
const pick = require('lodash/pick');
|
|
2
3
|
const cloneDeep = require('lodash/cloneDeep');
|
|
3
4
|
const {
|
|
4
5
|
DOMAIN_FOR_DEFAULT_SITE,
|
|
@@ -9,7 +10,6 @@ const {
|
|
|
9
10
|
BLOCKLET_SITE_GROUP_SUFFIX,
|
|
10
11
|
} = require('@abtnode/constant');
|
|
11
12
|
const { BLOCKLET_UI_INTERFACES } = require('@blocklet/meta/lib/constants');
|
|
12
|
-
const { pick } = require('lodash');
|
|
13
13
|
const logger = require('@abtnode/logger')('@abtnode/core:router');
|
|
14
14
|
|
|
15
15
|
const expandSites = (sites = []) => {
|
|
@@ -6,7 +6,9 @@ const camelCase = require('lodash/camelCase');
|
|
|
6
6
|
|
|
7
7
|
const BaseState = require('./base');
|
|
8
8
|
|
|
9
|
-
const { mergeConfigs } = require('../blocklet/extras');
|
|
9
|
+
const { mergeConfigs, parseConfigs } = require('../blocklet/extras');
|
|
10
|
+
|
|
11
|
+
const noop = (k) => (v) => v[k];
|
|
10
12
|
|
|
11
13
|
class BlockletExtrasState extends BaseState {
|
|
12
14
|
constructor(baseDir, options = {}) {
|
|
@@ -17,12 +19,13 @@ class BlockletExtrasState extends BaseState {
|
|
|
17
19
|
{
|
|
18
20
|
name: 'configs',
|
|
19
21
|
beforeSet: mergeConfigs,
|
|
22
|
+
afterGet: parseConfigs,
|
|
20
23
|
},
|
|
21
24
|
|
|
22
25
|
// setting
|
|
23
26
|
{
|
|
24
27
|
name: 'settings',
|
|
25
|
-
beforeSet: (old, cur) => {
|
|
28
|
+
beforeSet: ({ old, cur }) => {
|
|
26
29
|
const merged = { ...old, ...cur };
|
|
27
30
|
Object.keys(merged).forEach((key) => {
|
|
28
31
|
if (merged[key] === undefined || merged[key] === null) {
|
|
@@ -67,22 +70,23 @@ class BlockletExtrasState extends BaseState {
|
|
|
67
70
|
|
|
68
71
|
generateGetFn(extra) {
|
|
69
72
|
return async (did) => {
|
|
70
|
-
const {
|
|
73
|
+
const { dek } = this.options;
|
|
74
|
+
const { name, afterGet = noop('data') } = extra;
|
|
71
75
|
const item = await this.asyncDB.findOne({ did });
|
|
72
|
-
return item ? item[name] : item;
|
|
76
|
+
return afterGet({ data: item ? item[name] : item, did, dek });
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
generateSetFn(extra) {
|
|
77
81
|
return async (did, data) => {
|
|
78
|
-
const {
|
|
79
|
-
const
|
|
82
|
+
const { dek } = this.options;
|
|
83
|
+
const { name, beforeSet = noop('cur') } = extra;
|
|
80
84
|
const item = await this.asyncDB.findOne({ did });
|
|
81
85
|
|
|
82
86
|
if (!item) {
|
|
83
87
|
const insertData = {
|
|
84
88
|
did,
|
|
85
|
-
[name]:
|
|
89
|
+
[name]: beforeSet({ old: undefined, cur: data, did, dek }),
|
|
86
90
|
};
|
|
87
91
|
|
|
88
92
|
const info = await this.asyncDB.insert(insertData);
|
|
@@ -93,7 +97,7 @@ class BlockletExtrasState extends BaseState {
|
|
|
93
97
|
const itemNameValue = item[name];
|
|
94
98
|
const updated = await this.update(item._id, {
|
|
95
99
|
$set: {
|
|
96
|
-
[name]:
|
|
100
|
+
[name]: beforeSet({ old: itemNameValue, cur: data, did, dek }),
|
|
97
101
|
},
|
|
98
102
|
});
|
|
99
103
|
return updated[name];
|
|
@@ -132,22 +136,23 @@ class BlockletExtrasState extends BaseState {
|
|
|
132
136
|
|
|
133
137
|
generateGetChildFn(extra) {
|
|
134
138
|
return async (did, childDid) => {
|
|
135
|
-
const {
|
|
139
|
+
const { dek } = this.options;
|
|
140
|
+
const { name, afterGet = noop('data') } = extra;
|
|
136
141
|
const item = await this.asyncDB.findOne({ did });
|
|
137
142
|
const children = (item || {}).children || [];
|
|
138
143
|
const subItem = (children || []).find((x) => x.did === childDid);
|
|
139
|
-
return subItem ? subItem[name] : null;
|
|
144
|
+
return afterGet({ data: subItem ? subItem[name] : null, did, dek });
|
|
140
145
|
};
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
generateSetChildFn(extra) {
|
|
144
149
|
return async (did, childDid, data) => {
|
|
145
|
-
const {
|
|
146
|
-
const
|
|
150
|
+
const { dek } = this.options;
|
|
151
|
+
const { name, beforeSet = noop('cur') } = extra;
|
|
147
152
|
const item = await this.asyncDB.findOne({ did });
|
|
148
153
|
|
|
149
154
|
if (!item) {
|
|
150
|
-
const newData =
|
|
155
|
+
const newData = beforeSet({ old: undefined, cur: data, did, dek });
|
|
151
156
|
const insertData = {
|
|
152
157
|
did,
|
|
153
158
|
children: [
|
|
@@ -168,7 +173,7 @@ class BlockletExtrasState extends BaseState {
|
|
|
168
173
|
const subItem = (children || []).find((x) => x.did === childDid);
|
|
169
174
|
|
|
170
175
|
if (!subItem) {
|
|
171
|
-
const newData =
|
|
176
|
+
const newData = beforeSet({ old: undefined, cur: data, did, dek });
|
|
172
177
|
await this.update(item._id, {
|
|
173
178
|
$addToSet: {
|
|
174
179
|
children: {
|
|
@@ -182,7 +187,7 @@ class BlockletExtrasState extends BaseState {
|
|
|
182
187
|
return newData;
|
|
183
188
|
}
|
|
184
189
|
|
|
185
|
-
const newData =
|
|
190
|
+
const newData = beforeSet({ old: subItem[name], cur: data, did, dek });
|
|
186
191
|
|
|
187
192
|
children.forEach((x) => {
|
|
188
193
|
if (x.did === childDid) {
|
package/lib/states/node.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const omit = require('lodash/omit');
|
|
3
3
|
const isEqual = require('lodash/isEqual');
|
|
4
4
|
const isEmpty = require('lodash/isEmpty');
|
|
5
|
+
const security = require('@abtnode/util/lib/security');
|
|
5
6
|
const { isFromPublicKey } = require('@arcblock/did');
|
|
6
7
|
const logger = require('@abtnode/logger')('@abtnode/core:node');
|
|
7
8
|
const { ROUTER_PROVIDER_NONE, NODE_MODES, DISK_ALERT_THRESHOLD_PERCENT } = require('@abtnode/constant');
|
|
@@ -58,6 +59,7 @@ class NodeState extends BaseState {
|
|
|
58
59
|
*/
|
|
59
60
|
read() {
|
|
60
61
|
return new Promise((resolve, reject) => {
|
|
62
|
+
const { nodeDid, dek } = this.options;
|
|
61
63
|
this.db.findOne({ did: this.options.nodeDid }, (err, record) => {
|
|
62
64
|
if (err) {
|
|
63
65
|
// eslint-disable-next-line no-console
|
|
@@ -66,6 +68,10 @@ class NodeState extends BaseState {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
if (record) {
|
|
71
|
+
if (dek) {
|
|
72
|
+
record.sk = security.decrypt(record.sk, record.did, dek);
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
return resolve(record);
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -74,7 +80,6 @@ class NodeState extends BaseState {
|
|
|
74
80
|
description,
|
|
75
81
|
nodeSk,
|
|
76
82
|
nodePk,
|
|
77
|
-
nodeDid,
|
|
78
83
|
nodeOwner,
|
|
79
84
|
port,
|
|
80
85
|
version,
|
|
@@ -100,7 +105,7 @@ class NodeState extends BaseState {
|
|
|
100
105
|
name,
|
|
101
106
|
description,
|
|
102
107
|
pk: nodePk,
|
|
103
|
-
sk: nodeSk,
|
|
108
|
+
sk: dek ? security.encrypt(nodeSk, nodeDid, dek) : nodeSk,
|
|
104
109
|
did: nodeDid,
|
|
105
110
|
initialized,
|
|
106
111
|
version,
|
package/lib/util/blocklet.js
CHANGED
|
@@ -798,7 +798,7 @@ const getRuntimeInfo = async (appId) => {
|
|
|
798
798
|
const proc = await getProcessInfo(appId);
|
|
799
799
|
return {
|
|
800
800
|
pid: proc.pid,
|
|
801
|
-
uptime: proc.pm2_env ? Number(proc.pm2_env.pm_uptime) : 0,
|
|
801
|
+
uptime: proc.pm2_env ? +new Date() - Number(proc.pm2_env.pm_uptime) : 0,
|
|
802
802
|
memoryUsage: proc.monit.memory,
|
|
803
803
|
cpuUsage: proc.monit.cpu,
|
|
804
804
|
status: proc.pm2_env ? proc.pm2_env.status : null,
|
|
@@ -3,7 +3,7 @@ const { DEFAULT_IP_DNS_DOMAIN_SUFFIX } = require('@abtnode/constant');
|
|
|
3
3
|
|
|
4
4
|
const SLOT_FOR_IP_DNS_SITE = '888-888-888-888';
|
|
5
5
|
|
|
6
|
-
const formatName = (name) => slugify(name.replace(/^[@./-]/, '').replace(/[@./]/g, '-'));
|
|
6
|
+
const formatName = (name) => slugify(name.replace(/^[@./-]/, '').replace(/[@./_]/g, '-'));
|
|
7
7
|
|
|
8
8
|
const hiddenInterfaceNames = ['public', 'publicUrl'];
|
|
9
9
|
|
package/lib/util/requirement.js
CHANGED
|
@@ -10,7 +10,8 @@ const isSatisfied = (requirements, throwOnUnsatisfied = true) => {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const { platform: actualOs, arch: actualCpu } = process;
|
|
13
|
-
const { os: expectedOs, cpu: expectedCpu, abtnode
|
|
13
|
+
const { os: expectedOs, cpu: expectedCpu, abtnode, server } = Object.assign(defaults, requirements);
|
|
14
|
+
const expectedVersion = server || abtnode;
|
|
14
15
|
|
|
15
16
|
const isOsSatisfied =
|
|
16
17
|
expectedOs === '*' || expectedOs === actualOs || (Array.isArray(expectedOs) && expectedOs.includes(actualOs));
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.6.
|
|
6
|
+
"version": "1.6.4",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,26 +19,26 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@abtnode/constant": "1.6.
|
|
23
|
-
"@abtnode/cron": "1.6.
|
|
24
|
-
"@abtnode/logger": "1.6.
|
|
25
|
-
"@abtnode/queue": "1.6.
|
|
26
|
-
"@abtnode/rbac": "1.6.
|
|
27
|
-
"@abtnode/router-provider": "1.6.
|
|
28
|
-
"@abtnode/static-server": "1.6.
|
|
29
|
-
"@abtnode/timemachine": "1.6.
|
|
30
|
-
"@abtnode/util": "1.6.
|
|
31
|
-
"@arcblock/did": "^1.13.
|
|
32
|
-
"@arcblock/event-hub": "1.13.
|
|
22
|
+
"@abtnode/constant": "1.6.4",
|
|
23
|
+
"@abtnode/cron": "1.6.4",
|
|
24
|
+
"@abtnode/logger": "1.6.4",
|
|
25
|
+
"@abtnode/queue": "1.6.4",
|
|
26
|
+
"@abtnode/rbac": "1.6.4",
|
|
27
|
+
"@abtnode/router-provider": "1.6.4",
|
|
28
|
+
"@abtnode/static-server": "1.6.4",
|
|
29
|
+
"@abtnode/timemachine": "1.6.4",
|
|
30
|
+
"@abtnode/util": "1.6.4",
|
|
31
|
+
"@arcblock/did": "^1.13.77",
|
|
32
|
+
"@arcblock/event-hub": "1.13.77",
|
|
33
33
|
"@arcblock/pm2-events": "^0.0.5",
|
|
34
|
-
"@arcblock/vc": "^1.13.
|
|
35
|
-
"@blocklet/meta": "1.6.
|
|
34
|
+
"@arcblock/vc": "^1.13.77",
|
|
35
|
+
"@blocklet/meta": "1.6.4",
|
|
36
36
|
"@fidm/x509": "^1.2.1",
|
|
37
37
|
"@nedb/core": "^1.2.2",
|
|
38
38
|
"@nedb/multi": "^1.2.2",
|
|
39
|
-
"@ocap/mcrypto": "^1.13.
|
|
40
|
-
"@ocap/util": "^1.13.
|
|
41
|
-
"@ocap/wallet": "^1.13.
|
|
39
|
+
"@ocap/mcrypto": "^1.13.77",
|
|
40
|
+
"@ocap/util": "^1.13.77",
|
|
41
|
+
"@ocap/wallet": "^1.13.77",
|
|
42
42
|
"@slack/webhook": "^5.0.3",
|
|
43
43
|
"axios": "^0.21.4",
|
|
44
44
|
"axon": "^2.0.3",
|
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"express": "^4.17.1",
|
|
74
74
|
"jest": "^27.3.1"
|
|
75
75
|
},
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "1c144cb9fb9a9952bc92f25cabbdb47a378cbd24"
|
|
77
77
|
}
|