@abtnode/core 1.16.42 → 1.16.43-beta-20250419-231352-c78ac93d

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/team.js CHANGED
@@ -2022,7 +2022,10 @@ class TeamAPI extends EventEmitter {
2022
2022
  * @returns
2023
2023
  */
2024
2024
 
2025
- async upsertUserSession({ teamDid, appPid, userDid, visitorId, ua, lastLoginIp, passportId, status, extra }) {
2025
+ async upsertUserSession(
2026
+ { teamDid, appPid, userDid, visitorId, ua, lastLoginIp, passportId, status, extra, locale, origin },
2027
+ { skipNotification = false } = {}
2028
+ ) {
2026
2029
  if (!userDid) {
2027
2030
  throw new Error('userDid is required');
2028
2031
  }
@@ -2039,6 +2042,17 @@ class TeamAPI extends EventEmitter {
2039
2042
  extra,
2040
2043
  });
2041
2044
  logger.info('insert userSession successfully', { userDid, ua, lastLoginIp, appPid, passportId, extra });
2045
+ // HACK: 某些地方,首次新增 userSession 时不会,携带 ua 信息,这个时候不能发通知
2046
+ if (ua) {
2047
+ this.emit(BlockletEvents.addUserSession, {
2048
+ userSession: data,
2049
+ teamDid,
2050
+ userDid,
2051
+ locale,
2052
+ skipNotification,
2053
+ origin,
2054
+ });
2055
+ }
2042
2056
  } else {
2043
2057
  const exist = await state.findOne({ userDid, visitorId, appPid });
2044
2058
  if (exist) {
@@ -2052,6 +2066,25 @@ class TeamAPI extends EventEmitter {
2052
2066
  extra: mergeExtra,
2053
2067
  });
2054
2068
  logger.info('update userSession successfully', { id: exist.id, ua, lastLoginIp, passportId, status, extra });
2069
+ if (Date.now() - new Date(exist.createdAt).getTime() < 1000 * 10) {
2070
+ // HACK: 此处是为了矫正首次创建 userSession 没有 ua 的情况,会通过 /api/did/session 接口来更新 ua 信息,这个时候才能当成新增 userSession 来发送通知
2071
+ this.emit(BlockletEvents.addUserSession, {
2072
+ userSession: data,
2073
+ teamDid,
2074
+ userDid,
2075
+ locale,
2076
+ skipNotification,
2077
+ origin,
2078
+ });
2079
+ } else {
2080
+ this.emit(BlockletEvents.updateUserSession, {
2081
+ userSession: data,
2082
+ teamDid,
2083
+ userDid,
2084
+ skipNotification,
2085
+ origin,
2086
+ });
2087
+ }
2055
2088
  } else {
2056
2089
  data = await state.insert({
2057
2090
  visitorId,
@@ -2071,6 +2104,16 @@ class TeamAPI extends EventEmitter {
2071
2104
  passportId,
2072
2105
  extra,
2073
2106
  });
2107
+ if (ua) {
2108
+ this.emit(BlockletEvents.addUserSession, {
2109
+ userSession: data,
2110
+ teamDid,
2111
+ userDid,
2112
+ locale,
2113
+ skipNotification,
2114
+ origin,
2115
+ });
2116
+ }
2074
2117
  }
2075
2118
  }
2076
2119
 
@@ -2185,11 +2228,7 @@ class TeamAPI extends EventEmitter {
2185
2228
  // HACK: 使用 state.remove 并不能按预期工作,先改为使用 state.model.destroy 来实现
2186
2229
  const result = await state.model.destroy({ where });
2187
2230
  const { permanentWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
2188
- const federated = defaults(cloneDeep(blocklet.settings.federated || {}), {
2189
- config: {},
2190
- sites: [],
2191
- });
2192
- const masterSite = federated.sites[0];
2231
+ const masterSite = getFederatedMaster(blocklet);
2193
2232
  if (masterSite && masterSite.isMaster !== false && masterSite.appPid !== teamDid) {
2194
2233
  callFederated({
2195
2234
  action: 'sync',
@@ -123,7 +123,7 @@ const {
123
123
  } = require('@abtnode/auth/lib/util/federated');
124
124
  const toBlockletDid = require('@blocklet/meta/lib/did');
125
125
 
126
- const { groupBy } = require('lodash');
126
+ const groupBy = require('lodash/groupBy');
127
127
  const launcher = require('../../util/launcher');
128
128
  const util = require('../../util');
129
129
  const {
@@ -229,10 +229,11 @@ const { dockerExec } = require('../../util/docker/docker-exec');
229
229
  const { installExternalDependencies } = require('../../util/install-external-dependencies');
230
230
  const { dockerExecChown } = require('../../util/docker/docker-exec-chown');
231
231
  const checkDockerRunHistory = require('../../util/docker/check-docker-run-history');
232
- const { abtNodeStartBackupDelaySeconds } = require('../../util/env');
232
+ const { shouldJobBackoff } = require('../../util/env');
233
233
  const ensureBlockletRunning = require('./ensure-blocklet-running');
234
234
 
235
235
  const { transformNotification } = require('../../util/notification');
236
+ const { generateUserUpdateData } = require('../../util/user');
236
237
 
237
238
  const { formatEnvironments, getBlockletMeta, validateOwner, isCLI } = util;
238
239
 
@@ -433,6 +434,7 @@ class DiskBlockletManager extends BaseBlockletManager {
433
434
  stop: async (params) => {
434
435
  await this.stop(params);
435
436
  },
437
+ createAuditLog: (params) => this.createAuditLog(params),
436
438
  notification: (did, title, description, severity) => {
437
439
  try {
438
440
  this._createNotification(did, {
@@ -448,6 +450,7 @@ class DiskBlockletManager extends BaseBlockletManager {
448
450
  logger.error('create notification failed', { err });
449
451
  }
450
452
  },
453
+ checkSystemHighLoad: (...args) => this.nodeAPI.runtimeMonitor.checkSystemHighLoad(...args),
451
454
  });
452
455
  }
453
456
  }
@@ -515,6 +518,11 @@ class DiskBlockletManager extends BaseBlockletManager {
515
518
  });
516
519
  }
517
520
 
521
+ // This property will be injected after the node instance is instantiated.
522
+ createAuditLog = (params) => {
523
+ console.warn('createAuditLog not initialized', params);
524
+ };
525
+
518
526
  // ============================================================================================
519
527
  // Public API for Installing/Upgrading Application or Components
520
528
  // ============================================================================================
@@ -3160,6 +3168,11 @@ class DiskBlockletManager extends BaseBlockletManager {
3160
3168
  strategy: BACKUPS.STRATEGY.AUTO,
3161
3169
  },
3162
3170
  }) {
3171
+ if (shouldJobBackoff()) {
3172
+ logger.warn('Backup to spaces is not available when blocklet server is starting.');
3173
+ return;
3174
+ }
3175
+
3163
3176
  const blocklet = await states.blocklet.getBlocklet(did);
3164
3177
  const {
3165
3178
  appDid,
@@ -3206,10 +3219,9 @@ class DiskBlockletManager extends BaseBlockletManager {
3206
3219
  return message;
3207
3220
  }
3208
3221
 
3209
- const uptime = process.uptime();
3210
- if (backupState?.strategy === BACKUPS.STRATEGY.AUTO && uptime < abtNodeStartBackupDelaySeconds) {
3222
+ if (backupState?.strategy === BACKUPS.STRATEGY.AUTO && shouldJobBackoff()) {
3211
3223
  const message = 'Automatic backup is not available when blocklet server is starting.';
3212
- logger.error(message, { appPid, uptime, abtNodeStartBackupDelaySeconds });
3224
+ logger.error(message, { appPid });
3213
3225
 
3214
3226
  return message;
3215
3227
  }
@@ -3379,6 +3391,17 @@ class DiskBlockletManager extends BaseBlockletManager {
3379
3391
  }
3380
3392
 
3381
3393
  async _onCheckForComponentUpdate({ did }) {
3394
+ if (shouldJobBackoff()) {
3395
+ logger.warn('Check for component update is not available when blocklet server is starting.');
3396
+ return;
3397
+ }
3398
+
3399
+ const blocklet = await this.getBlocklet(did);
3400
+ if (blocklet.status === BlockletStatus.stopped) {
3401
+ logger.warn('Check for component update is not available when blocklet is stopped.');
3402
+ return;
3403
+ }
3404
+
3382
3405
  const list = await UpgradeComponents.check({ did, states });
3383
3406
  if (!list || !list.updateList?.length) {
3384
3407
  return;
@@ -3394,7 +3417,7 @@ class DiskBlockletManager extends BaseBlockletManager {
3394
3417
  if (checkUpdateMd5 === oldMd5) {
3395
3418
  return;
3396
3419
  }
3397
- const blocklet = await this.getBlocklet(did);
3420
+
3398
3421
  const firstComponent = updateList[0];
3399
3422
  const blockletTitle = getDisplayName(blocklet);
3400
3423
 
@@ -5358,9 +5381,21 @@ class FederatedBlockletManager extends DiskBlockletManager {
5358
5381
  return newState;
5359
5382
  }
5360
5383
 
5384
+ /**
5385
+ * 调用来源有两种
5386
+ * 1. js-sdk: 调用时,会处理好 user.phone 与 user.metadata.phone 的同步,已经 user.metadata.location 与 user.address.city 的同步
5387
+ * 2. node-sdk: 需要处理 node-sdk 的调用时的数据同步
5388
+ * @param {*} param0
5389
+ * @returns
5390
+ */
5361
5391
  async updateUserInfoAndSync({ teamDid, user }) {
5362
5392
  try {
5363
- const updated = await this.teamAPI.updateUser({ teamDid, user });
5393
+ const existingUser = await this.teamAPI.getUser({ teamDid, user: { did: user.did } });
5394
+ if (!existingUser) {
5395
+ throw new Error('User not found');
5396
+ }
5397
+ const updateData = generateUserUpdateData(user, existingUser);
5398
+ const updated = await this.teamAPI.updateUser({ teamDid, user: updateData });
5364
5399
  const { sourceAppPid } = updated;
5365
5400
 
5366
5401
  const blocklet = await this.getBlocklet(teamDid);
@@ -4,34 +4,36 @@ const { BlockletStatus } = require('@blocklet/constant');
4
4
  const sleep = require('@abtnode/util/lib/sleep');
5
5
  const { getDisplayName } = require('@blocklet/meta/lib/util');
6
6
 
7
- const inProgressStatuses = [BlockletStatus.stopping, BlockletStatus.restarting, BlockletStatus.waiting];
8
-
9
7
  const states = require('../../states');
10
8
  const { isBlockletPortHealthy, shouldCheckHealthy } = require('../../util/blocklet');
9
+ const { shouldJobBackoff } = require('../../util/env');
10
+
11
+ const inProgressStatuses = [BlockletStatus.stopping, BlockletStatus.restarting, BlockletStatus.waiting];
11
12
 
12
13
  class EnsureBlockletRunning {
13
14
  initialized = false;
14
15
 
15
16
  // 每次任务的最小间隔时间
16
- checkInterval = process.env.ABT_NODE_ENSURE_RUNNING_CHECK_INTERVAL
17
- ? Number(process.env.ABT_NODE_ENSURE_RUNNING_CHECK_INTERVAL)
18
- : 60 * 1000;
17
+ checkInterval = +process.env.ABT_NODE_ENSURE_RUNNING_CHECK_INTERVAL || 60 * 1000;
19
18
 
20
19
  // 首次任务的延迟时间
21
- deferredTime = process.env.ABT_NODE_ENSURE_RUNNING_DEFERRED_TIME
22
- ? Number(process.env.ABT_NODE_ENSURE_RUNNING_DEFERRED_TIME)
23
- : 30 * 1000;
20
+ deferredTime = +process.env.ABT_NODE_ENSURE_RUNNING_DEFERRED_TIME || 30 * 1000;
24
21
 
25
22
  // 每个重启任务(did + componentDids)的重启间隔为 2 min,如果 2 min 内重启过,就不会再重启
26
23
  restartInterval = 1000 * 60 * 2;
27
24
 
28
- // 如果长时间在某个 doing 状态,则认为该 blocklet 有问题,会进行重启
29
- maxLongtimeDoing = 1000 * 60 * 5;
25
+ everyBlockletCheckInterval = 2000;
30
26
 
31
- everyBlockletCheckInterval = 1000;
27
+ everyBlockletDoingInterval = 5000;
32
28
 
33
29
  fakeRunningToWaitingOnce = false;
34
30
 
31
+ highLoadCpu = +process.env.ABT_NODE_ENSURE_RUNNING_HIGH_LOAD_CPU || 0.7;
32
+
33
+ highLoadMemory = +process.env.ABT_NODE_ENSURE_RUNNING_HIGH_LOAD_MEMORY || 0.8;
34
+
35
+ highLoadDisk = +process.env.ABT_NODE_ENSURE_RUNNING_HIGH_LOAD_DISK || 0.8;
36
+
35
37
  runningBlocklets = {};
36
38
 
37
39
  runningRootBlocklets = {};
@@ -49,17 +51,26 @@ class EnsureBlockletRunning {
49
51
  // Ease to mock
50
52
  isBlockletPortHealthy = isBlockletPortHealthy;
51
53
 
52
- isBlockletPortHealthyWithRetries = async (blocklet) => {
54
+ // Ease to mock
55
+ shouldJobBackoff = shouldJobBackoff;
56
+
57
+ isBlockletPortHealthyWithRetries = async (blocklet, isDoing = false) => {
58
+ let error;
53
59
  for (let attempt = 0; attempt < 10; attempt++) {
54
- // eslint-disable-next-line no-await-in-loop
55
- const healthy = await this.isBlockletPortHealthy(blocklet, {
56
- minConsecutiveTime: 3000,
57
- timeout: 6000,
58
- });
59
- if (healthy) return true;
60
- // eslint-disable-next-line no-await-in-loop
61
- await sleep(this.everyBlockletCheckInterval);
60
+ try {
61
+ // eslint-disable-next-line no-await-in-loop
62
+ await this.isBlockletPortHealthy(blocklet, {
63
+ minConsecutiveTime: 3000,
64
+ timeout: 6000,
65
+ });
66
+ return true;
67
+ } catch (e) {
68
+ error = e;
69
+ // eslint-disable-next-line no-await-in-loop
70
+ await sleep(isDoing ? this.everyBlockletDoingInterval : this.everyBlockletCheckInterval);
71
+ }
62
72
  }
73
+ logger.error('blocklet port is not healthy', error);
63
74
  return false;
64
75
  };
65
76
 
@@ -67,7 +78,7 @@ class EnsureBlockletRunning {
67
78
  this.states = states;
68
79
  }
69
80
 
70
- initialize = ({ restart, stop, notification }) => {
81
+ initialize = ({ restart, stop, notification, checkSystemHighLoad, createAuditLog }) => {
71
82
  if (this.initialized) {
72
83
  return;
73
84
  }
@@ -75,13 +86,13 @@ class EnsureBlockletRunning {
75
86
  this.restart = restart;
76
87
  this.stop = stop;
77
88
  this.notification = notification;
78
-
89
+ this.createAuditLog = createAuditLog;
90
+ this.checkSystemHighLoad = checkSystemHighLoad;
79
91
  const task = async () => {
80
92
  if (this.stopped) {
81
93
  return;
82
94
  }
83
95
  await this.checkAndFix();
84
-
85
96
  await sleep(this.checkInterval);
86
97
  task();
87
98
  };
@@ -104,6 +115,21 @@ class EnsureBlockletRunning {
104
115
  if (process.env.ABT_NODE_RESTART_RUNNING_COMPONENT === '1') {
105
116
  return;
106
117
  }
118
+
119
+ if (!this.shouldJobBackoff()) {
120
+ return;
121
+ }
122
+ const systemHighLoad = this.checkSystemHighLoad({
123
+ maxCpus: this.highLoadCpu,
124
+ maxMem: this.highLoadMemory,
125
+ maxDisk: this.highLoadDisk,
126
+ });
127
+
128
+ logger.info('Check ensure blocklet running', systemHighLoad);
129
+ if (systemHighLoad.isHighLoad) {
130
+ logger.warn('Skip once ensure blocklet running because system high load', systemHighLoad);
131
+ return;
132
+ }
107
133
  this.runningBlocklets = {};
108
134
  this.fakeRunningBlocklets = {};
109
135
  this.fakeRunningBlockletsTimes = {};
@@ -186,24 +212,23 @@ class EnsureBlockletRunning {
186
212
  }
187
213
  return;
188
214
  }
189
- const health = await this.isBlockletPortHealthyWithRetries(blocklet);
215
+
216
+ const health = await this.isBlockletPortHealthyWithRetries(
217
+ blocklet,
218
+ inProgressStatuses.includes(blocklet.status)
219
+ );
190
220
 
191
221
  if (health) {
192
- // 如果 blocklet 健康,但处于 doing 状态,则设置为 running 状态
193
- if (blocklet.status !== BlockletStatus.running && inProgressStatuses.includes(blocklet.status)) {
194
- await this.states.blocklet.setBlockletStatus(did, BlockletStatus.running, {
195
- componentDids: [blocklet.meta.did],
196
- });
197
- }
198
- } else {
199
- if (!this.fakeRunningBlocklets[did]) {
200
- this.fakeRunningBlocklets[did] = [];
201
- }
202
- if (this.fakeRunningBlocklets[did].find((b) => b.meta.did === blocklet.meta.did)) {
203
- return;
204
- }
205
- this.fakeRunningBlocklets[did].push(blocklet);
222
+ return;
223
+ }
224
+
225
+ if (!this.fakeRunningBlocklets[did]) {
226
+ this.fakeRunningBlocklets[did] = [];
206
227
  }
228
+ if (this.fakeRunningBlocklets[did].find((b) => b.meta.did === blocklet.meta.did)) {
229
+ return;
230
+ }
231
+ this.fakeRunningBlocklets[did].push(blocklet);
207
232
  };
208
233
  }),
209
234
  { concurrency: 10 }
@@ -255,15 +280,34 @@ class EnsureBlockletRunning {
255
280
  }
256
281
  this.restartingBlocklets[key] = Date.now();
257
282
 
258
- this.notification(
259
- did,
260
- 'Blocklet health check failed',
261
- `Blocklet ${this.getDisplayNameByRootDid(did)} with components ${componentDids.map((v) => this.getDisplayName(blocklets.find((b) => b.meta.did === v))).join(', ')} health check failed, restarting...`,
262
- 'warning'
263
- );
283
+ const blockletDisplayName = this.getDisplayNameByRootDid(did);
284
+ const restartTitle = 'Blocklet health check failed';
285
+ const restartDescription = `Blocklet ${blockletDisplayName} with components ${componentDids.map((v) => this.getDisplayName(blocklets.find((b) => b.meta.did === v))).join(', ')} health check failed, restarting...`;
286
+ this.notification(did, restartTitle, restartDescription, 'warning');
287
+
264
288
  try {
265
289
  logger.info('restart blocklet:', did, componentDids);
266
290
  await this.restart({ did, componentDids, checkHealthImmediately: true });
291
+ this.createAuditLog({
292
+ action: 'ensureBlockletRunning',
293
+ args: {
294
+ teamDid: did,
295
+ componentDids,
296
+ },
297
+ context: {
298
+ user: {
299
+ did,
300
+ role: 'daemon',
301
+ blockletDid: did,
302
+ fullName: blockletDisplayName,
303
+ elevated: false,
304
+ },
305
+ },
306
+ result: {
307
+ title: restartTitle,
308
+ description: restartDescription,
309
+ },
310
+ });
267
311
  delete this.restartingBlocklets[key];
268
312
  } catch (e) {
269
313
  logger.error('restart blocklet failed', did, componentDids, e);
@@ -274,16 +318,34 @@ class EnsureBlockletRunning {
274
318
 
275
319
  // 如果重启失败次数超过 3 次,则设置为 stopped
276
320
  if (this.errorStartBlocklets[key] >= 3) {
277
- this.notification(
278
- did,
279
- 'Restart blocklet failed when health check failed',
280
- `Restart blocklet ${this.getDisplayNameByRootDid(did)} with components ${componentDids.map((v) => this.getDisplayName(blocklets.find((b) => b.meta.did === v))).join(', ')} failed`,
281
- 'error'
282
- );
321
+ const title = 'Restart blocklet failed when health check failed';
322
+ const description = `Restart blocklet ${blockletDisplayName} with components ${componentDids.map((v) => this.getDisplayName(blocklets.find((b) => b.meta.did === v))).join(', ')} failed`;
323
+ this.notification(did, title, description, 'error');
283
324
  delete this.errorStartBlocklets[key];
284
325
  logger.error('restart many times blocklet failed, set stopped', did, componentDids, e);
285
326
  try {
286
327
  await this.stop({ did, componentDids });
328
+ this.createAuditLog({
329
+ action: 'ensureBlockletRunning',
330
+ args: {
331
+ blockletDisplayName,
332
+ teamDid: did,
333
+ componentDids,
334
+ },
335
+ context: {
336
+ user: {
337
+ did,
338
+ role: 'daemon',
339
+ blockletDid: did,
340
+ fullName: blockletDisplayName,
341
+ elevated: false,
342
+ },
343
+ },
344
+ result: {
345
+ title,
346
+ description,
347
+ },
348
+ });
287
349
  } catch (err) {
288
350
  logger.error('set blocklet stopped failed', did, componentDids, err);
289
351
  }
@@ -100,7 +100,6 @@ const RBAC_CONFIG = {
100
100
  name: SERVER_ROLES.CERTIFICATE,
101
101
  title: 'Certificate',
102
102
  description: 'Manage https certificates for blocklets on the Blocklet Server',
103
- passport: true,
104
103
  noHuman: true,
105
104
  },
106
105
  {
@@ -127,7 +126,6 @@ const RBAC_CONFIG = {
127
126
  name: SERVER_ROLES.EXTERNAL_BLOCKLETS_MANAGER,
128
127
  title: 'External Blocklets Manager',
129
128
  description: 'Manage external blocklets in the Blocklet Server',
130
- passport: true,
131
129
  noHuman: true,
132
130
  },
133
131
  ]),
@@ -38737,7 +38735,7 @@ module.exports = require("zlib");
38737
38735
  /***/ ((module) => {
38738
38736
 
38739
38737
  "use strict";
38740
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.16.41","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib","test":"node tools/jest.js","coverage":"npm run test -- --coverage"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.16.41","@abtnode/auth":"1.16.41","@abtnode/certificate-manager":"1.16.41","@abtnode/constant":"1.16.41","@abtnode/cron":"1.16.41","@abtnode/docker-utils":"1.16.41","@abtnode/logger":"1.16.41","@abtnode/models":"1.16.41","@abtnode/queue":"1.16.41","@abtnode/rbac":"1.16.41","@abtnode/router-provider":"1.16.41","@abtnode/static-server":"1.16.41","@abtnode/timemachine":"1.16.41","@abtnode/util":"1.16.41","@arcblock/did":"1.20.0","@arcblock/did-auth":"1.20.0","@arcblock/did-ext":"^1.20.0","@arcblock/did-motif":"^1.1.13","@arcblock/did-util":"1.20.0","@arcblock/event-hub":"1.20.0","@arcblock/jwt":"^1.20.0","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"^1.20.0","@arcblock/vc":"1.20.0","@blocklet/constant":"1.16.41","@blocklet/did-space-js":"^1.0.47","@blocklet/env":"1.16.41","@blocklet/meta":"1.16.41","@blocklet/resolver":"1.16.41","@blocklet/sdk":"1.16.41","@blocklet/store":"1.16.41","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.20.0","@ocap/util":"1.20.0","@ocap/wallet":"1.20.0","@slack/webhook":"^5.0.4","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","dayjs":"^1.11.13","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","lru-cache":"^11.0.2","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","rate-limiter-flexible":"^5.0.5","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"^2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^9.0.1","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"expand-tilde":"^2.0.2","express":"^4.18.2","jest":"^29.7.0","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
38738
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.16.42","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib","test":"node tools/jest.js","coverage":"npm run test -- --coverage"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.16.42","@abtnode/auth":"1.16.42","@abtnode/certificate-manager":"1.16.42","@abtnode/constant":"1.16.42","@abtnode/cron":"1.16.42","@abtnode/docker-utils":"1.16.42","@abtnode/logger":"1.16.42","@abtnode/models":"1.16.42","@abtnode/queue":"1.16.42","@abtnode/rbac":"1.16.42","@abtnode/router-provider":"1.16.42","@abtnode/static-server":"1.16.42","@abtnode/timemachine":"1.16.42","@abtnode/util":"1.16.42","@arcblock/did":"1.20.1","@arcblock/did-auth":"1.20.1","@arcblock/did-ext":"^1.20.1","@arcblock/did-motif":"^1.1.13","@arcblock/did-util":"1.20.1","@arcblock/event-hub":"1.20.1","@arcblock/jwt":"^1.20.1","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"^1.20.1","@arcblock/vc":"1.20.1","@blocklet/constant":"1.16.42","@blocklet/did-space-js":"^1.0.48","@blocklet/env":"1.16.42","@blocklet/meta":"1.16.42","@blocklet/resolver":"1.16.42","@blocklet/sdk":"1.16.42","@blocklet/store":"1.16.42","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.20.1","@ocap/util":"1.20.1","@ocap/wallet":"1.20.1","@slack/webhook":"^5.0.4","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","dayjs":"^1.11.13","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","lru-cache":"^11.0.2","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","rate-limiter-flexible":"^5.0.5","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"^2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^9.0.1","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"expand-tilde":"^2.0.2","express":"^4.18.2","jest":"^29.7.0","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
38741
38739
 
38742
38740
  /***/ }),
38743
38741
 
@@ -22,6 +22,7 @@ const {
22
22
  } = require('@abtnode/constant');
23
23
  const { joinURL } = require('ufo');
24
24
  const { encode } = require('@abtnode/util/lib/base32');
25
+
25
26
  const { NodeMonitSender } = require('../monitor/node-monit-sender');
26
27
  const { isCLI } = require('../util');
27
28
 
@@ -626,5 +627,81 @@ module.exports = ({
626
627
  eventHub.on(EVENT_BUS_EVENT, (data) => handleEventBusEvent(data));
627
628
  }
628
629
 
630
+ // 更新会话时,暂不做任何操作
631
+ // listen(teamAPI, BlockletEvents.updateUserSession, noop);
632
+ listen(teamAPI, BlockletEvents.addUserSession, (eventName, eventData) => {
633
+ const { teamDid, userDid, userSession, locale = 'en', skipNotification = false, origin } = eventData;
634
+ if (skipNotification) {
635
+ return;
636
+ }
637
+ const translations = {
638
+ en: {
639
+ title: 'You are logged in a new device',
640
+ body: `You are logged in a new device: ${userSession.ua}. \n\nIf this is not you, please click "View Your Account" button to view details`,
641
+ loginIp: 'Login IP',
642
+ loginAt: 'Login At',
643
+ viewYourAccount: 'View Your Account',
644
+ },
645
+ zh: {
646
+ title: '你在一个新的设备上登录了',
647
+ loginIp: '登录 IP',
648
+ loginAt: '登录时间',
649
+ body: `你在一个新的设备上登录了: ${userSession.ua}。\n\n如果这不是你本人在登录,请点击“查看您的账户”按钮查看详情`,
650
+ viewYourAccount: '查看您的账户',
651
+ },
652
+ };
653
+
654
+ const translation = translations[locale] || translations.en;
655
+ const localeMap = {
656
+ en: 'en-US',
657
+ zh: 'zh-CN',
658
+ };
659
+ const targetLocale = localeMap[locale] || localeMap.en;
660
+ const notification = {
661
+ title: translation.title,
662
+ body: translation.body,
663
+ severity: 'warning',
664
+ attachments: [
665
+ {
666
+ type: 'section',
667
+ fields: [
668
+ {
669
+ type: 'text',
670
+ data: { type: 'plain', color: '#9397A1', text: translation.loginIp },
671
+ },
672
+ {
673
+ type: 'text',
674
+ data: { type: 'plain', text: userSession.lastLoginIp },
675
+ },
676
+ {
677
+ type: 'text',
678
+ data: { type: 'plain', color: '#9397A1', text: translation.loginAt },
679
+ },
680
+ {
681
+ type: 'text',
682
+ data: { type: 'plain', text: userSession.updatedAt.toLocaleString(targetLocale) },
683
+ },
684
+ ],
685
+ },
686
+ ],
687
+ actions: [
688
+ {
689
+ name: translation.viewYourAccount,
690
+ link: `${origin}/.well-known/service/user/settings`,
691
+ },
692
+ ],
693
+ };
694
+ logger.info(`create notification on ${eventName}`, { teamDid, receiver: userDid, notification });
695
+ teamManager.createNotification({
696
+ teamDid,
697
+ receiver: userDid,
698
+ // NOTICE: createNotification 如果传递 notification 对象,就必须加上 pushOnly 选项,否则会调用失败
699
+ // 目前先改为使用 payload 的形式传递通知内容
700
+ // notification,
701
+ ...notification,
702
+ source: 'component',
703
+ });
704
+ });
705
+
629
706
  return events;
630
707
  };
package/lib/index.js CHANGED
@@ -755,6 +755,8 @@ function ABTNode(options) {
755
755
  getBlockletBaseInfo: teamAPI.getBlockletBaseInfo.bind(teamAPI),
756
756
  };
757
757
 
758
+ blockletManager.createAuditLog = (params) => states.auditLog.create(params, instance);
759
+
758
760
  const events = createEvents({
759
761
  blockletManager,
760
762
  ensureBlockletRouting,
@@ -193,6 +193,63 @@ class NodeRuntimeMonitor extends EventEmitter {
193
193
  });
194
194
  }
195
195
 
196
+ calculateUtilization() {
197
+ if (!this.data.realtime || !this.data.realtime.cpu || !this.data.realtime.mem || !this.data.realtime.disks) {
198
+ return {
199
+ cpus: [0],
200
+ disks: [0],
201
+ memory: 0,
202
+ };
203
+ }
204
+ try {
205
+ const { cpu, mem, disks } = this.data.realtime;
206
+ // 计算每个 CPU 的占用率:load字段为百分比,除以 100 得到 0 到 1 的值
207
+ const cpuUtilizations = cpu.cpus.filter((v) => v.load > 0).map((v) => v.load / 100);
208
+
209
+ // 计算内存占用率:used / total
210
+ const memoryUtilization = mem.total > 0 && mem.available > 0 ? (mem.total - mem.available) / mem.total : 0;
211
+
212
+ // 计算每个硬盘占用率:used / total
213
+ const diskUtilizations = disks.filter((v) => v.used > 0).map((v) => v.used / v.total);
214
+
215
+ // 返回包含所需值的对象
216
+ return {
217
+ cpus: cpuUtilizations,
218
+ disks: diskUtilizations,
219
+ memory: memoryUtilization,
220
+ };
221
+ } catch (error) {
222
+ this.logger.error('failed to calculate utilization', error);
223
+ return {
224
+ cpus: [0],
225
+ disks: [0],
226
+ memory: 0,
227
+ };
228
+ }
229
+ }
230
+
231
+ checkSystemHighLoad({ maxCpus, maxMem, maxDisk }) {
232
+ const { cpus, memory, disks } = this.calculateUtilization();
233
+ let highType = '';
234
+ if (cpus.some((v) => v > maxCpus)) {
235
+ highType = 'cpu';
236
+ } else if (memory > maxMem) {
237
+ highType = 'memory';
238
+ } else if (disks.some((v) => v > maxDisk)) {
239
+ highType = 'disk';
240
+ }
241
+ if (highType) {
242
+ this.logger.info('system high load', { cpus, memory, disks });
243
+ }
244
+ return {
245
+ isHighLoad: !!highType,
246
+ highType,
247
+ cpus,
248
+ memory,
249
+ disks,
250
+ };
251
+ }
252
+
196
253
  cleanup() {
197
254
  return this.state.remove({ date: { $lt: Date.now() - 1000 * 60 * 60 * 24 } });
198
255
  }
@@ -513,6 +513,8 @@ const getLogContent = async (action, args, context, result, info, node) => {
513
513
  return `Update Blocklet Webhook(${result.id})`;
514
514
  case 'deleteWebhookEndpoint':
515
515
  return `Delete Blocklet Webhook(${result.id})`;
516
+ case 'ensureBlockletRunning':
517
+ return `${result.title}:\n* ${result.description}`;
516
518
 
517
519
  default:
518
520
  return action;
@@ -557,6 +559,7 @@ const getLogCategory = (action) => {
557
559
  case 'auditFederated':
558
560
  case 'syncMasterAuthorization':
559
561
  case 'syncFederatedConfig':
562
+ case 'ensureBlockletRunning':
560
563
  return 'blocklet';
561
564
 
562
565
  // store,此处应该返回 server
@@ -732,11 +735,9 @@ class AuditLogState extends BaseState {
732
735
  const [info, uaInfo] = await Promise.all([node.states.node.read(), parse(ua)]);
733
736
 
734
737
  fixActor(user);
735
-
736
738
  const content = (await getLogContent(action, args, context, result, info, node)).trim();
737
739
  const actor = pick(user.actual || user, ['did', 'fullName', 'role']);
738
740
  actor.source = '';
739
-
740
741
  const teamDid =
741
742
  user.blockletDid || args.teamDid || (typeof args?.did === 'string' ? args.did : get(args, 'did.0'));
742
743
  const userDid = user.did || args.userDid || get(args, 'user.did') || args.ownerDid;
@@ -770,6 +771,7 @@ class AuditLogState extends BaseState {
770
771
  }
771
772
  } else {
772
773
  actor.avatar = '';
774
+ // actor.did = teamDid || info.did;
773
775
  }
774
776
 
775
777
  const data = await this.insert({
@@ -757,6 +757,13 @@ SELECT did,inviter,generation FROM UserTree`.trim();
757
757
  if (user.phone) {
758
758
  user.phone = String(user.phone).trim().toLowerCase().replace(/\s+/g, '').replace(/[()-]/g, '');
759
759
  }
760
+ if (user.metadata?.phone?.phoneNumber) {
761
+ user.metadata.phone.phoneNumber = String(user.metadata.phone.phoneNumber)
762
+ .trim()
763
+ .toLowerCase()
764
+ .replace(/\s+/g, '')
765
+ .replace(/[()-]/g, '');
766
+ }
760
767
  return user;
761
768
  }
762
769
 
@@ -1028,7 +1028,7 @@ const shouldCheckHealthy = (blocklet) => {
1028
1028
 
1029
1029
  const isBlockletPortHealthy = async (blocklet, { minConsecutiveTime = 3000, timeout = 10 * 1000 } = {}) => {
1030
1030
  if (!blocklet) {
1031
- return true;
1031
+ return;
1032
1032
  }
1033
1033
  const { environments } = blocklet;
1034
1034
  const webInterface = (blocklet.meta?.interfaces || []).find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
@@ -1042,22 +1042,16 @@ const isBlockletPortHealthy = async (blocklet, { minConsecutiveTime = 3000, time
1042
1042
  }
1043
1043
 
1044
1044
  if (!port) {
1045
- return true;
1045
+ return;
1046
1046
  }
1047
1047
 
1048
- try {
1049
- await ensureEndpointHealthy({
1050
- port,
1051
- protocol: webInterface ? 'http' : 'tcp',
1052
- minConsecutiveTime,
1053
- timeout,
1054
- doConsecutiveCheck: false,
1055
- });
1056
- return true;
1057
- } catch (error) {
1058
- logger.error('blocklet port is not healthy', error);
1059
- return false;
1060
- }
1048
+ await ensureEndpointHealthy({
1049
+ port,
1050
+ protocol: webInterface ? 'http' : 'tcp',
1051
+ minConsecutiveTime,
1052
+ timeout,
1053
+ doConsecutiveCheck: false,
1054
+ });
1061
1055
  };
1062
1056
 
1063
1057
  const expandTarball = async ({ source, dest, strip = 1 }) => {
package/lib/util/env.js CHANGED
@@ -1,7 +1,13 @@
1
- const abtNodeStartBackupDelaySeconds = process.env.ABT_NODE_START_BACKUP_DELAY_SECONDS
2
- ? +process.env.ABT_NODE_START_BACKUP_DELAY_SECONDS
3
- : 300;
1
+ const serverJobBackoffSeconds = process.env.ABT_NODE_JOB_BACKOFF_SECONDS
2
+ ? +process.env.ABT_NODE_JOB_BACKOFF_SECONDS
3
+ : 600;
4
+
5
+ const shouldJobBackoff = () => {
6
+ const uptime = process.uptime();
7
+ return uptime <= serverJobBackoffSeconds;
8
+ };
4
9
 
5
10
  module.exports = {
6
- abtNodeStartBackupDelaySeconds,
11
+ serverJobBackoffSeconds,
12
+ shouldJobBackoff,
7
13
  };
@@ -367,6 +367,8 @@ const setupAppOwner = async ({ node, sessionId, justCreate = false, context, pro
367
367
  clientName: context?.device?.clientName,
368
368
  },
369
369
  },
370
+ locale,
371
+ origin: context?.origin,
370
372
  })
371
373
  .catch((error) => {
372
374
  logger.error('upsertUserSession failed', { error, appDid, ownerDid, context });
@@ -0,0 +1,73 @@
1
+ const omitBy = require('lodash/omitBy');
2
+ const omit = require('lodash/omit');
3
+
4
+ /**
5
+ * 生成用户要更新的数据
6
+ * @param {*} user 要更新的用户对象
7
+ * @param {*} existingUser 已存在的用户对象
8
+ */
9
+ const generateUserUpdateData = (user, existingUser) => {
10
+ // 如果用户不存在,需要抛出错误
11
+ if (!existingUser) {
12
+ throw new Error('User not found');
13
+ }
14
+
15
+ const { emailVerified, phoneVerified, metadata = {} } = existingUser;
16
+
17
+ // 创建基础更新对象,只包含非空值
18
+ let updateData = {
19
+ did: user.did,
20
+ ...omitBy(user, (x) => !x),
21
+ metadata: { ...metadata, ...user.metadata },
22
+ };
23
+
24
+ // 处理地址
25
+ if (user.address) {
26
+ updateData.address = {
27
+ ...existingUser.address,
28
+ ...(user.address ?? {}),
29
+ };
30
+ updateData.metadata = {
31
+ ...updateData.metadata,
32
+ ...(user.metadata ?? {}),
33
+ location: user.address?.city || '',
34
+ };
35
+ }
36
+
37
+ // 处理电话信息
38
+ if (phoneVerified && updateData.metadata?.phone?.phoneNumber !== existingUser.phone) {
39
+ // 保持原号码相同
40
+ updateData.metadata.phone = {
41
+ country: '',
42
+ phoneNumber: existingUser.phone,
43
+ };
44
+ } else if (!phoneVerified && user.phone) {
45
+ // 从user.phone更新
46
+ updateData.metadata.phone = {
47
+ country: '',
48
+ phoneNumber: user.phone,
49
+ };
50
+ } else if (!phoneVerified && !user.phone && user.metadata?.phone) {
51
+ // 从user.metadata.phone更新
52
+ updateData.phone = user.metadata?.phone?.phoneNumber || '';
53
+ }
54
+
55
+ // 处理邮件
56
+ if (user.metadata?.email) {
57
+ updateData.metadata = omit(updateData.metadata, ['email']);
58
+ if (!emailVerified) {
59
+ updateData.email = user.metadata.email;
60
+ }
61
+ }
62
+
63
+ // 邮箱已验证,保持服务器上已验证的邮箱
64
+ if (emailVerified && updateData.email) {
65
+ updateData = omit(updateData, ['email']);
66
+ }
67
+ if (phoneVerified && updateData.phone) {
68
+ updateData = omit(updateData, ['phone']);
69
+ }
70
+ return updateData;
71
+ };
72
+
73
+ module.exports = { generateUserUpdateData };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.42",
6
+ "version": "1.16.43-beta-20250419-231352-c78ac93d",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,41 +19,41 @@
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.42",
23
- "@abtnode/auth": "1.16.42",
24
- "@abtnode/certificate-manager": "1.16.42",
25
- "@abtnode/constant": "1.16.42",
26
- "@abtnode/cron": "1.16.42",
27
- "@abtnode/docker-utils": "1.16.42",
28
- "@abtnode/logger": "1.16.42",
29
- "@abtnode/models": "1.16.42",
30
- "@abtnode/queue": "1.16.42",
31
- "@abtnode/rbac": "1.16.42",
32
- "@abtnode/router-provider": "1.16.42",
33
- "@abtnode/static-server": "1.16.42",
34
- "@abtnode/timemachine": "1.16.42",
35
- "@abtnode/util": "1.16.42",
36
- "@arcblock/did": "1.20.0",
37
- "@arcblock/did-auth": "1.20.0",
38
- "@arcblock/did-ext": "^1.20.0",
22
+ "@abtnode/analytics": "1.16.43-beta-20250419-231352-c78ac93d",
23
+ "@abtnode/auth": "1.16.43-beta-20250419-231352-c78ac93d",
24
+ "@abtnode/certificate-manager": "1.16.43-beta-20250419-231352-c78ac93d",
25
+ "@abtnode/constant": "1.16.43-beta-20250419-231352-c78ac93d",
26
+ "@abtnode/cron": "1.16.43-beta-20250419-231352-c78ac93d",
27
+ "@abtnode/docker-utils": "1.16.43-beta-20250419-231352-c78ac93d",
28
+ "@abtnode/logger": "1.16.43-beta-20250419-231352-c78ac93d",
29
+ "@abtnode/models": "1.16.43-beta-20250419-231352-c78ac93d",
30
+ "@abtnode/queue": "1.16.43-beta-20250419-231352-c78ac93d",
31
+ "@abtnode/rbac": "1.16.43-beta-20250419-231352-c78ac93d",
32
+ "@abtnode/router-provider": "1.16.43-beta-20250419-231352-c78ac93d",
33
+ "@abtnode/static-server": "1.16.43-beta-20250419-231352-c78ac93d",
34
+ "@abtnode/timemachine": "1.16.43-beta-20250419-231352-c78ac93d",
35
+ "@abtnode/util": "1.16.43-beta-20250419-231352-c78ac93d",
36
+ "@arcblock/did": "1.20.1",
37
+ "@arcblock/did-auth": "1.20.1",
38
+ "@arcblock/did-ext": "^1.20.1",
39
39
  "@arcblock/did-motif": "^1.1.13",
40
- "@arcblock/did-util": "1.20.0",
41
- "@arcblock/event-hub": "1.20.0",
42
- "@arcblock/jwt": "^1.20.0",
40
+ "@arcblock/did-util": "1.20.1",
41
+ "@arcblock/event-hub": "1.20.1",
42
+ "@arcblock/jwt": "^1.20.1",
43
43
  "@arcblock/pm2-events": "^0.0.5",
44
- "@arcblock/validator": "^1.20.0",
45
- "@arcblock/vc": "1.20.0",
46
- "@blocklet/constant": "1.16.42",
47
- "@blocklet/did-space-js": "^1.0.47",
48
- "@blocklet/env": "1.16.42",
49
- "@blocklet/meta": "1.16.42",
50
- "@blocklet/resolver": "1.16.42",
51
- "@blocklet/sdk": "1.16.42",
52
- "@blocklet/store": "1.16.42",
44
+ "@arcblock/validator": "^1.20.1",
45
+ "@arcblock/vc": "1.20.1",
46
+ "@blocklet/constant": "1.16.43-beta-20250419-231352-c78ac93d",
47
+ "@blocklet/did-space-js": "^1.0.48",
48
+ "@blocklet/env": "1.16.43-beta-20250419-231352-c78ac93d",
49
+ "@blocklet/meta": "1.16.43-beta-20250419-231352-c78ac93d",
50
+ "@blocklet/resolver": "1.16.43-beta-20250419-231352-c78ac93d",
51
+ "@blocklet/sdk": "1.16.43-beta-20250419-231352-c78ac93d",
52
+ "@blocklet/store": "1.16.43-beta-20250419-231352-c78ac93d",
53
53
  "@fidm/x509": "^1.2.1",
54
- "@ocap/mcrypto": "1.20.0",
55
- "@ocap/util": "1.20.0",
56
- "@ocap/wallet": "1.20.0",
54
+ "@ocap/mcrypto": "1.20.1",
55
+ "@ocap/util": "1.20.1",
56
+ "@ocap/wallet": "1.20.1",
57
57
  "@slack/webhook": "^5.0.4",
58
58
  "archiver": "^7.0.1",
59
59
  "axios": "^1.7.9",
@@ -111,5 +111,5 @@
111
111
  "jest": "^29.7.0",
112
112
  "unzipper": "^0.10.11"
113
113
  },
114
- "gitHead": "04e4adf8f2c1d6289850c66159c0a68ea946d5e6"
114
+ "gitHead": "207acad34e8ccf318cd7539c1ac717cee7951b53"
115
115
  }