@abtnode/core 1.16.7 → 1.16.8-beta-0c0c5eb2
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 +300 -56
- package/lib/blocklet/storage/backup/spaces.js +25 -9
- package/lib/event.js +4 -10
- package/lib/index.js +32 -2
- package/lib/locales/en.js +9 -0
- package/lib/locales/index.js +7 -0
- package/lib/locales/zh.js +8 -0
- package/lib/migrations/1.16.8-component-title.js +66 -0
- package/lib/monitor/blocklet-runtime-monitor.js +1 -1
- package/lib/router/helper.js +15 -1
- package/lib/states/audit-log.js +7 -2
- package/lib/states/backup.js +177 -0
- package/lib/states/blocklet-extras.js +6 -0
- package/lib/states/index.js +3 -0
- package/lib/states/user.js +1 -2
- package/lib/util/blocklet.js +12 -1
- package/lib/util/queue.js +19 -0
- package/lib/util/spaces.js +39 -0
- package/lib/validators/backup.js +24 -0
- package/lib/validators/space-gateway.js +30 -0
- package/package.json +18 -17
|
@@ -128,6 +128,8 @@ const UpgradeComponents = require('./helper/upgrade-components');
|
|
|
128
128
|
const BlockletDownloader = require('../downloader/blocklet-downloader');
|
|
129
129
|
const RollbackCache = require('./helper/rollback-cache');
|
|
130
130
|
const { migrateApplicationToStructV2 } = require('./helper/migrate-application-to-struct-v2');
|
|
131
|
+
const { getBackupFilesUrlFromEndpoint, getDIDSpaceBackupEndpoint } = require('../../util/spaces');
|
|
132
|
+
const { validateAddSpaceGateway, validateUpdateSpaceGateway } = require('../../validators/space-gateway');
|
|
131
133
|
|
|
132
134
|
const { formatEnvironments, shouldUpdateBlockletStatus, getBlockletMeta, validateOwner } = util;
|
|
133
135
|
|
|
@@ -195,9 +197,19 @@ const MONITOR_HISTORY_LENGTH = 86400 / MONITOR_RECORD_INTERVAL_SEC;
|
|
|
195
197
|
|
|
196
198
|
class BlockletManager extends BaseBlockletManager {
|
|
197
199
|
/**
|
|
198
|
-
*
|
|
200
|
+
* Creates an instance of BlockletManager.
|
|
201
|
+
* @param {{
|
|
202
|
+
* dataDirs: ReturnType<typeof import('../../util/index').getDataDirs>,
|
|
203
|
+
* startQueue: ReturnType<typeof import('../../util/queue')>,
|
|
204
|
+
* installQueue: ReturnType<typeof import('../../util/queue')>,
|
|
205
|
+
* backupQueue: ReturnType<typeof import('../../util/queue')>,
|
|
206
|
+
* restoreQueue: ReturnType<typeof import('../../util/queue')>,
|
|
207
|
+
* daemon: boolean
|
|
208
|
+
* teamManager: import('../../team/manager.js')
|
|
209
|
+
* }} { dataDirs, startQueue, installQueue, backupQueue, restoreQueue, daemon = false, teamManager }
|
|
210
|
+
* @memberof BlockletManager
|
|
199
211
|
*/
|
|
200
|
-
constructor({ dataDirs, startQueue, installQueue, backupQueue, daemon = false, teamManager }) {
|
|
212
|
+
constructor({ dataDirs, startQueue, installQueue, backupQueue, restoreQueue, daemon = false, teamManager }) {
|
|
201
213
|
super();
|
|
202
214
|
|
|
203
215
|
this.dataDirs = dataDirs;
|
|
@@ -205,6 +217,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
205
217
|
this.startQueue = startQueue;
|
|
206
218
|
this.installQueue = installQueue;
|
|
207
219
|
this.backupQueue = backupQueue;
|
|
220
|
+
this.restoreQueue = restoreQueue;
|
|
208
221
|
this.teamManager = teamManager;
|
|
209
222
|
|
|
210
223
|
// cached installed blocklets for performance
|
|
@@ -607,7 +620,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
607
620
|
}
|
|
608
621
|
|
|
609
622
|
if (to === 'spaces') {
|
|
610
|
-
return this._backupToSpaces({ blocklet
|
|
623
|
+
return this._backupToSpaces({ blocklet, context });
|
|
611
624
|
}
|
|
612
625
|
|
|
613
626
|
if (to === 'disk') {
|
|
@@ -1097,7 +1110,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1097
1110
|
}
|
|
1098
1111
|
|
|
1099
1112
|
// Reload nginx to make sure did-space can embed content from this app
|
|
1100
|
-
if (newConfigs.find((x) => x.key === BLOCKLET_CONFIGURABLE_KEY.
|
|
1113
|
+
if (newConfigs.find((x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT)?.value) {
|
|
1101
1114
|
this.emit(BlockletEvents.spaceConnected, blocklet);
|
|
1102
1115
|
}
|
|
1103
1116
|
|
|
@@ -1222,6 +1235,7 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1222
1235
|
return newState;
|
|
1223
1236
|
}
|
|
1224
1237
|
|
|
1238
|
+
// TODO: this method can be removed if title is not changed anymore
|
|
1225
1239
|
async updateComponentTitle({ did, rootDid: inputRootDid, title }) {
|
|
1226
1240
|
await titleSchema.validateAsync(title);
|
|
1227
1241
|
|
|
@@ -1367,6 +1381,16 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1367
1381
|
}
|
|
1368
1382
|
|
|
1369
1383
|
try {
|
|
1384
|
+
if (
|
|
1385
|
+
blocklet.meta?.group === BlockletGroup.gateway &&
|
|
1386
|
+
!blocklet.children?.length &&
|
|
1387
|
+
isInProgress(blocklet.status)
|
|
1388
|
+
) {
|
|
1389
|
+
const res = await states.blocklet.setBlockletStatus(did, BlockletStatus.stopped);
|
|
1390
|
+
this.emit(BlockletEvents.statusChange, res);
|
|
1391
|
+
return res;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1370
1394
|
const status = await getBlockletStatusFromProcess(blocklet);
|
|
1371
1395
|
if (blocklet.status !== status) {
|
|
1372
1396
|
const res = await states.blocklet.setBlockletStatus(did, status);
|
|
@@ -1406,6 +1430,15 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1406
1430
|
});
|
|
1407
1431
|
}
|
|
1408
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* @description
|
|
1435
|
+
* @param {{
|
|
1436
|
+
* entity: string;
|
|
1437
|
+
* action: string;
|
|
1438
|
+
* id: string;
|
|
1439
|
+
* }} job
|
|
1440
|
+
* @memberof BlockletManager
|
|
1441
|
+
*/
|
|
1409
1442
|
async onJob(job) {
|
|
1410
1443
|
if (job.entity === 'blocklet') {
|
|
1411
1444
|
if (job.action === 'download') {
|
|
@@ -1418,6 +1451,14 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1418
1451
|
if (job.action === 'check_if_started') {
|
|
1419
1452
|
await this._onCheckIfStarted(job);
|
|
1420
1453
|
}
|
|
1454
|
+
|
|
1455
|
+
if (job.action === 'backupToSpaces') {
|
|
1456
|
+
await this._onBackupToSpaces(job);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
if (job.action === 'restoreFromSpaces') {
|
|
1460
|
+
await this._onRestoreFromSpaces(job);
|
|
1461
|
+
}
|
|
1421
1462
|
}
|
|
1422
1463
|
}
|
|
1423
1464
|
|
|
@@ -1461,6 +1502,100 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1461
1502
|
];
|
|
1462
1503
|
}
|
|
1463
1504
|
|
|
1505
|
+
/**
|
|
1506
|
+
* @description
|
|
1507
|
+
* @param {{
|
|
1508
|
+
* did: import('@abtnode/client').RequestAddBlockletSpaceGatewayInput,
|
|
1509
|
+
* spaceGateway: import('@abtnode/client').SpaceGateway
|
|
1510
|
+
* }} { did, spaceGateway }
|
|
1511
|
+
* @return {Promise<void>}
|
|
1512
|
+
* @memberof BlockletManager
|
|
1513
|
+
*/
|
|
1514
|
+
async addBlockletSpaceGateway({ did, spaceGateway }) {
|
|
1515
|
+
const spaceGateways = await this.getBlockletSpaceGateways({ did });
|
|
1516
|
+
|
|
1517
|
+
const { error, value } = validateAddSpaceGateway.validate(spaceGateway, {
|
|
1518
|
+
stripUnknown: true,
|
|
1519
|
+
allowUnknown: true,
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
if (error) {
|
|
1523
|
+
throw error;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
spaceGateways.push(value);
|
|
1527
|
+
|
|
1528
|
+
await states.blockletExtras.setSettings(did, { spaceGateways });
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* @description
|
|
1533
|
+
* @param {import('@abtnode/client').RequestAddBlockletSpaceGatewayInput} { did, spaceGateway }
|
|
1534
|
+
* @return {Promise<void>}
|
|
1535
|
+
* @memberof BlockletManager
|
|
1536
|
+
*/
|
|
1537
|
+
async deleteBlockletSpaceGateway({ did, url }) {
|
|
1538
|
+
const spaceGateways = await this.getBlockletSpaceGateways({ did });
|
|
1539
|
+
|
|
1540
|
+
const latestSpaceGateways = spaceGateways.filter((s) => s?.url !== url);
|
|
1541
|
+
|
|
1542
|
+
await states.blockletExtras.setSettings(did, { spaceGateways: latestSpaceGateways });
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* @description
|
|
1547
|
+
* @param {{
|
|
1548
|
+
* did: string,
|
|
1549
|
+
* where: import('@abtnode/client').SpaceGateway
|
|
1550
|
+
* spaceGateway: import('@abtnode/client').SpaceGateway
|
|
1551
|
+
* }} { did, spaceGateway }
|
|
1552
|
+
* @return {Promise<void>}
|
|
1553
|
+
* @memberof BlockletManager
|
|
1554
|
+
*/
|
|
1555
|
+
async updateBlockletSpaceGateway({ did, where, spaceGateway }) {
|
|
1556
|
+
const { error, value } = validateUpdateSpaceGateway.validate(spaceGateway, {
|
|
1557
|
+
stripUnknown: true,
|
|
1558
|
+
allowUnknown: true,
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
if (error) {
|
|
1562
|
+
throw error;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const spaceGateways = await this.getBlockletSpaceGateways({ did });
|
|
1566
|
+
|
|
1567
|
+
for (const s of spaceGateways) {
|
|
1568
|
+
if (s.url === where?.url) {
|
|
1569
|
+
Object.assign(s, value);
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
await states.blockletExtras.setSettings(did, { spaceGateways });
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* @description
|
|
1579
|
+
* @param {{ did: string }} { did }
|
|
1580
|
+
* @return {Promise<Array<import('@abtnode/client').SpaceGateway>>}
|
|
1581
|
+
* @memberof BlockletManager
|
|
1582
|
+
*/
|
|
1583
|
+
async getBlockletSpaceGateways({ did }) {
|
|
1584
|
+
const spaceGateways = await states.blockletExtras.getSettings(did, 'spaceGateways', []);
|
|
1585
|
+
|
|
1586
|
+
return spaceGateways;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* @description
|
|
1591
|
+
* @param {{ did: string }} { did }
|
|
1592
|
+
* @return {Promise<Array<import('@abtnode/client').Backup>>}
|
|
1593
|
+
* @memberof BlockletManager
|
|
1594
|
+
*/
|
|
1595
|
+
async getBlockletBackups({ did }) {
|
|
1596
|
+
return states.backup.getBlockletBackups({ did });
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1464
1599
|
// ============================================================================================
|
|
1465
1600
|
// Private API that are used by self of helper function
|
|
1466
1601
|
// ============================================================================================
|
|
@@ -1721,6 +1856,127 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
1721
1856
|
}
|
|
1722
1857
|
}
|
|
1723
1858
|
|
|
1859
|
+
/**
|
|
1860
|
+
* FIXME: @wangshijun create audit log for this
|
|
1861
|
+
* @param {{
|
|
1862
|
+
* blocklet: import('@abtnode/client').BlockletState,
|
|
1863
|
+
* context: {
|
|
1864
|
+
* referrer: string;
|
|
1865
|
+
* user: {
|
|
1866
|
+
* did: string;
|
|
1867
|
+
* }
|
|
1868
|
+
* }
|
|
1869
|
+
* }} { blocklet, context }
|
|
1870
|
+
* @memberof BlockletManager
|
|
1871
|
+
*/
|
|
1872
|
+
async _onBackupToSpaces({ blocklet, context, backup }) {
|
|
1873
|
+
const {
|
|
1874
|
+
referrer,
|
|
1875
|
+
user: { did: userDid },
|
|
1876
|
+
} = context;
|
|
1877
|
+
const {
|
|
1878
|
+
appDid,
|
|
1879
|
+
meta: { did: appPid },
|
|
1880
|
+
} = blocklet;
|
|
1881
|
+
|
|
1882
|
+
try {
|
|
1883
|
+
const spacesBackup = new SpacesBackup({ appDid, appPid, event: this, userDid, referrer, locale: 'en' });
|
|
1884
|
+
this.emit(BlockletEvents.backupProgress, {
|
|
1885
|
+
appDid,
|
|
1886
|
+
meta: { did: appPid },
|
|
1887
|
+
message: 'Start backup...',
|
|
1888
|
+
progress: 10,
|
|
1889
|
+
completed: false,
|
|
1890
|
+
});
|
|
1891
|
+
await spacesBackup.backup();
|
|
1892
|
+
|
|
1893
|
+
await states.backup.success(backup._id, {
|
|
1894
|
+
targetUrl: getBackupFilesUrlFromEndpoint(getDIDSpaceBackupEndpoint(blocklet?.environments)),
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
// 备份成功了
|
|
1898
|
+
this.emit(BlockletEvents.backupProgress, { appDid, meta: { did: appPid }, completed: true, progress: 100 });
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
await states.backup.fail(backup._id, {
|
|
1901
|
+
message: error?.message,
|
|
1902
|
+
});
|
|
1903
|
+
this.emit(BlockletEvents.backupProgress, {
|
|
1904
|
+
appDid,
|
|
1905
|
+
meta: { did: appPid },
|
|
1906
|
+
completed: true,
|
|
1907
|
+
progress: -1,
|
|
1908
|
+
message: error?.message,
|
|
1909
|
+
});
|
|
1910
|
+
throw error;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* FIXME: @linchen support cancel
|
|
1916
|
+
* FIXME: @wangshijun create audit log for this
|
|
1917
|
+
* @param {{
|
|
1918
|
+
* input: import('@abtnode/client').RequestRestoreBlockletInput,
|
|
1919
|
+
* context: Record<string, string>,
|
|
1920
|
+
* }} {input, context}
|
|
1921
|
+
* @memberof BlockletManager
|
|
1922
|
+
*/
|
|
1923
|
+
// eslint-disable-next-line no-unused-vars
|
|
1924
|
+
async _onRestoreFromSpaces({ input, context }) {
|
|
1925
|
+
if (input.delay) {
|
|
1926
|
+
await sleep(input.delay);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
const appPid = input.appDid;
|
|
1930
|
+
|
|
1931
|
+
this.emit(BlockletEvents.restoreProgress, {
|
|
1932
|
+
appDid: input.appDid,
|
|
1933
|
+
meta: { did: appPid },
|
|
1934
|
+
status: RESTORE_PROGRESS_STATUS.start,
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
const userDid = context.user.did;
|
|
1938
|
+
|
|
1939
|
+
const spacesRestore = new SpacesRestore({ ...input, appPid, event: this, userDid, referrer: context.referrer });
|
|
1940
|
+
const params = await spacesRestore.restore();
|
|
1941
|
+
|
|
1942
|
+
const removeRestoreDir = () => {
|
|
1943
|
+
if (fs.existsSync(spacesRestore.restoreDir)) {
|
|
1944
|
+
fs.remove(spacesRestore.restoreDir).catch((err) => {
|
|
1945
|
+
logger.error('failed to remove restore dir', { error: err, dir: spacesRestore.restoreDir });
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
this.emit(BlockletEvents.restoreProgress, {
|
|
1951
|
+
appDid: input.appDid,
|
|
1952
|
+
meta: { did: appPid },
|
|
1953
|
+
status: RESTORE_PROGRESS_STATUS.installing,
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1956
|
+
try {
|
|
1957
|
+
await installApplicationFromBackup({
|
|
1958
|
+
url: `file://${spacesRestore.restoreDir}`,
|
|
1959
|
+
moveDir: true,
|
|
1960
|
+
...merge(...params),
|
|
1961
|
+
manager: this,
|
|
1962
|
+
states,
|
|
1963
|
+
controller: input.controller,
|
|
1964
|
+
context: { ...context, startImmediately: true },
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
removeRestoreDir();
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
removeRestoreDir();
|
|
1970
|
+
throw error;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
this.emit(BlockletEvents.restoreProgress, {
|
|
1974
|
+
appDid: input.appDid,
|
|
1975
|
+
meta: { did: appPid },
|
|
1976
|
+
status: RESTORE_PROGRESS_STATUS.completed,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1724
1980
|
async _updateBlockletEnvironment(did) {
|
|
1725
1981
|
const blockletWithEnv = await this.getBlocklet(did);
|
|
1726
1982
|
const blocklet = await states.blocklet.getBlocklet(did);
|
|
@@ -2641,28 +2897,45 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2641
2897
|
|
|
2642
2898
|
/**
|
|
2643
2899
|
* FIXME: @wangshijun create audit log for this
|
|
2644
|
-
* @param {
|
|
2900
|
+
* @param {{
|
|
2901
|
+
* blocklet: import('@abtnode/client').BlockletState,
|
|
2902
|
+
* context: {
|
|
2903
|
+
* referrer: string;
|
|
2904
|
+
* user: {
|
|
2905
|
+
* did: string;
|
|
2906
|
+
* }
|
|
2907
|
+
* }
|
|
2908
|
+
* }} { blocklet, context }
|
|
2645
2909
|
* @memberof BlockletManager
|
|
2646
2910
|
*/
|
|
2647
2911
|
// eslint-disable-next-line no-unused-vars
|
|
2648
|
-
async _backupToSpaces({ blocklet
|
|
2649
|
-
const
|
|
2650
|
-
|
|
2912
|
+
async _backupToSpaces({ blocklet, context }) {
|
|
2913
|
+
const {
|
|
2914
|
+
user: { did: userDid },
|
|
2915
|
+
} = context;
|
|
2651
2916
|
const {
|
|
2652
2917
|
appDid,
|
|
2653
2918
|
meta: { did: appPid },
|
|
2654
2919
|
} = blocklet;
|
|
2655
2920
|
|
|
2656
|
-
const
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
progress: 10,
|
|
2662
|
-
completed: false,
|
|
2921
|
+
const backup = await states.backup.start({
|
|
2922
|
+
appPid,
|
|
2923
|
+
userDid,
|
|
2924
|
+
strategy: 1,
|
|
2925
|
+
sourceUrl: path.join(this.dataDirs.tmp, 'backup', appDid),
|
|
2663
2926
|
});
|
|
2664
|
-
|
|
2665
|
-
this.
|
|
2927
|
+
|
|
2928
|
+
this.backupQueue.push(
|
|
2929
|
+
{
|
|
2930
|
+
entity: 'blocklet',
|
|
2931
|
+
action: 'backupToSpaces',
|
|
2932
|
+
id: appDid,
|
|
2933
|
+
blocklet,
|
|
2934
|
+
context,
|
|
2935
|
+
backup,
|
|
2936
|
+
},
|
|
2937
|
+
appDid
|
|
2938
|
+
);
|
|
2666
2939
|
}
|
|
2667
2940
|
|
|
2668
2941
|
/**
|
|
@@ -2690,45 +2963,16 @@ class BlockletManager extends BaseBlockletManager {
|
|
|
2690
2963
|
|
|
2691
2964
|
const appPid = input.appDid;
|
|
2692
2965
|
|
|
2693
|
-
this.
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
const removeRestoreDir = () => {
|
|
2705
|
-
if (fs.existsSync(spacesRestore.restoreDir)) {
|
|
2706
|
-
fs.remove(spacesRestore.restoreDir).catch((err) => {
|
|
2707
|
-
logger.error('failed to remove restore dir', { error: err, dir: spacesRestore.restoreDir });
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
};
|
|
2711
|
-
|
|
2712
|
-
this.emit(BlockletEvents.restoreProgress, { appDid: input.appDid, status: RESTORE_PROGRESS_STATUS.installing });
|
|
2713
|
-
|
|
2714
|
-
try {
|
|
2715
|
-
await installApplicationFromBackup({
|
|
2716
|
-
url: `file://${spacesRestore.restoreDir}`,
|
|
2717
|
-
moveDir: true,
|
|
2718
|
-
...merge(...params),
|
|
2719
|
-
manager: this,
|
|
2720
|
-
states,
|
|
2721
|
-
controller: input.controller,
|
|
2722
|
-
context: { ...context, startImmediately: true },
|
|
2723
|
-
});
|
|
2724
|
-
|
|
2725
|
-
removeRestoreDir();
|
|
2726
|
-
} catch (error) {
|
|
2727
|
-
removeRestoreDir();
|
|
2728
|
-
throw error;
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
this.emit(BlockletEvents.restoreProgress, { appDid: input.appDid, status: RESTORE_PROGRESS_STATUS.completed });
|
|
2966
|
+
this.restoreQueue.push(
|
|
2967
|
+
{
|
|
2968
|
+
entity: 'blocklet',
|
|
2969
|
+
action: 'restoreFromSpaces',
|
|
2970
|
+
id: appPid,
|
|
2971
|
+
input,
|
|
2972
|
+
context,
|
|
2973
|
+
},
|
|
2974
|
+
appPid
|
|
2975
|
+
);
|
|
2732
2976
|
}
|
|
2733
2977
|
|
|
2734
2978
|
async _restoreFromDisk(input) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* event: import('events').EventEmitter,
|
|
6
6
|
* userDid: string,
|
|
7
7
|
* referrer: string,
|
|
8
|
+
* locale: 'zh' | 'en',
|
|
8
9
|
* }} SpaceBackupInput
|
|
9
10
|
*
|
|
10
11
|
* @typedef {{
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
const { isValid } = require('@arcblock/did');
|
|
19
20
|
const { BLOCKLET_CONFIGURABLE_KEY, BlockletEvents } = require('@blocklet/constant');
|
|
20
21
|
const { SpaceClient, BackupBlockletCommand } = require('@did-space/client');
|
|
21
|
-
const { ensureDirSync } = require('fs-extra');
|
|
22
|
+
const { ensureDirSync, existsSync, remove } = require('fs-extra');
|
|
22
23
|
const { isEmpty } = require('lodash');
|
|
23
24
|
const { join, basename } = require('path');
|
|
24
25
|
const { getAppName, getAppDescription } = require('@blocklet/meta/lib/util');
|
|
@@ -33,6 +34,7 @@ const { BlockletExtrasBackup } = require('./blocklet-extras');
|
|
|
33
34
|
const { BlockletsBackup } = require('./blocklets');
|
|
34
35
|
const { DataBackup } = require('./data');
|
|
35
36
|
const { RoutingRuleBackup } = require('./routing-rule');
|
|
37
|
+
const { translate } = require('../../../locales');
|
|
36
38
|
|
|
37
39
|
class SpacesBackup extends BaseBackup {
|
|
38
40
|
/**
|
|
@@ -70,7 +72,7 @@ class SpacesBackup extends BaseBackup {
|
|
|
70
72
|
* @type {string}
|
|
71
73
|
* @memberof SpacesBackup
|
|
72
74
|
*/
|
|
73
|
-
|
|
75
|
+
spaceBackupEndpoint;
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
78
|
*
|
|
@@ -123,6 +125,7 @@ class SpacesBackup extends BaseBackup {
|
|
|
123
125
|
await this.initialize();
|
|
124
126
|
await this.export();
|
|
125
127
|
await this.syncToSpaces();
|
|
128
|
+
await this.destroy();
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
async initialize() {
|
|
@@ -135,10 +138,10 @@ class SpacesBackup extends BaseBackup {
|
|
|
135
138
|
this.backupDir = join(this.serverDir, 'tmp/backup', this.blocklet.appDid);
|
|
136
139
|
ensureDirSync(this.backupDir);
|
|
137
140
|
|
|
138
|
-
this.
|
|
139
|
-
(e) => e.key === BLOCKLET_CONFIGURABLE_KEY.
|
|
141
|
+
this.spaceBackupEndpoint = this.blocklet.environments.find(
|
|
142
|
+
(e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT
|
|
140
143
|
)?.value;
|
|
141
|
-
if (isEmpty(this.
|
|
144
|
+
if (isEmpty(this.spaceBackupEndpoint)) {
|
|
142
145
|
throw new Error('spaceEndpoint cannot be empty');
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -173,12 +176,12 @@ class SpacesBackup extends BaseBackup {
|
|
|
173
176
|
const serverDid = node.did;
|
|
174
177
|
|
|
175
178
|
const spaceClient = new SpaceClient({
|
|
176
|
-
endpoint: this.
|
|
179
|
+
endpoint: this.spaceBackupEndpoint,
|
|
177
180
|
wallet: this.securityContext.signer,
|
|
178
181
|
delegation: this.securityContext.delegation,
|
|
179
182
|
});
|
|
180
183
|
|
|
181
|
-
const { errorCount, message } = await spaceClient.send(
|
|
184
|
+
const { errorCount, message, statusCode } = await spaceClient.send(
|
|
182
185
|
new BackupBlockletCommand({
|
|
183
186
|
appDid: this.blocklet.appDid,
|
|
184
187
|
appName: getAppName(this.blocklet),
|
|
@@ -204,7 +207,7 @@ class SpacesBackup extends BaseBackup {
|
|
|
204
207
|
meta: { did: this.input.appDid },
|
|
205
208
|
message: `Uploading file ${basename(data.key)} (${data.completed}/${data.total})`,
|
|
206
209
|
// 0.8 是因为上传文件到 spaces 占进度的 80%,+ 20 是因为需要累加之前的进度
|
|
207
|
-
progress: +Math.
|
|
210
|
+
progress: +Math.floor(percent * 0.8).toFixed(2) + 20,
|
|
208
211
|
completed: false,
|
|
209
212
|
});
|
|
210
213
|
},
|
|
@@ -212,7 +215,20 @@ class SpacesBackup extends BaseBackup {
|
|
|
212
215
|
);
|
|
213
216
|
|
|
214
217
|
if (errorCount !== 0) {
|
|
215
|
-
|
|
218
|
+
const { locale } = this.input;
|
|
219
|
+
// @FIXME: get locale @jianchao
|
|
220
|
+
if (statusCode === 403) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`${translate(locale, 'backup.space.error.title')}: ${translate(locale, 'backup.space.error.forbidden')}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`${translate(locale, 'backup.space.error.title')}: ${message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async destroy() {
|
|
230
|
+
if (existsSync(this.backupDir)) {
|
|
231
|
+
await remove(this.backupDir);
|
|
216
232
|
}
|
|
217
233
|
}
|
|
218
234
|
}
|
package/lib/event.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const get = require('lodash/get');
|
|
2
2
|
const cloneDeep = require('lodash/cloneDeep');
|
|
3
3
|
const { EventEmitter } = require('events');
|
|
4
|
-
const { wipeSensitiveData
|
|
4
|
+
const { wipeSensitiveData } = require('@blocklet/meta/lib/util');
|
|
5
5
|
const logger = require('@abtnode/logger')('@abtnode/core:event');
|
|
6
6
|
const { BLOCKLET_MODES, BlockletStatus, BlockletSource, BlockletEvents } = require('@blocklet/constant');
|
|
7
7
|
const { EVENTS } = require('@abtnode/constant');
|
|
@@ -262,15 +262,9 @@ module.exports = ({
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
if (
|
|
265
|
-
[
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
BlockletEvents.stopped,
|
|
269
|
-
BlockletEvents.reloaded,
|
|
270
|
-
BlockletEvents.statusChange,
|
|
271
|
-
].includes(eventName) &&
|
|
272
|
-
blocklet.status &&
|
|
273
|
-
!isBeforeInstalled(blocklet.status)
|
|
265
|
+
[BlockletEvents.started, BlockletEvents.startFailed, BlockletEvents.stopped, BlockletEvents.reloaded].includes(
|
|
266
|
+
eventName
|
|
267
|
+
)
|
|
274
268
|
) {
|
|
275
269
|
try {
|
|
276
270
|
await blockletManager.runtimeMonitor.monit(blocklet.meta.did);
|
package/lib/index.js
CHANGED
|
@@ -118,9 +118,29 @@ function ABTNode(options) {
|
|
|
118
118
|
},
|
|
119
119
|
options: {
|
|
120
120
|
concurrency,
|
|
121
|
-
|
|
121
|
+
// 备份自带重试机制
|
|
122
|
+
maxRetries: 0,
|
|
122
123
|
retryDelay: 10000, // retry after 10 seconds
|
|
123
|
-
maxTimeout: 60 * 1000 *
|
|
124
|
+
maxTimeout: 60 * 1000 * 60, // throw timeout error after 60 minutes
|
|
125
|
+
id: (job) => (job ? md5(`${job.entity}-${job.action}-${job.id}`) : ''),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const restoreQueue = createQueue({
|
|
130
|
+
daemon: options.daemon,
|
|
131
|
+
name: 'restore_queue',
|
|
132
|
+
dataDir: dataDirs.core,
|
|
133
|
+
onJob: async (job) => {
|
|
134
|
+
if (typeof blockletManager.onJob === 'function') {
|
|
135
|
+
await blockletManager.onJob(job);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
options: {
|
|
139
|
+
concurrency,
|
|
140
|
+
// 自带重试机制
|
|
141
|
+
maxRetries: 0,
|
|
142
|
+
retryDelay: 10000, // retry after 10 seconds
|
|
143
|
+
maxTimeout: 60 * 1000 * 60, // throw timeout error after 60 minutes
|
|
124
144
|
id: (job) => (job ? md5(`${job.entity}-${job.action}-${job.id}`) : ''),
|
|
125
145
|
},
|
|
126
146
|
});
|
|
@@ -151,6 +171,7 @@ function ABTNode(options) {
|
|
|
151
171
|
startQueue,
|
|
152
172
|
installQueue,
|
|
153
173
|
backupQueue,
|
|
174
|
+
restoreQueue,
|
|
154
175
|
daemon: options.daemon,
|
|
155
176
|
teamManager,
|
|
156
177
|
});
|
|
@@ -255,6 +276,15 @@ function ABTNode(options) {
|
|
|
255
276
|
updateBlockletOwner: blockletManager.updateOwner.bind(blockletManager),
|
|
256
277
|
getBlockletRuntimeHistory: blockletManager.getRuntimeHistory.bind(blockletManager),
|
|
257
278
|
|
|
279
|
+
// blocklet spaces gateways
|
|
280
|
+
addBlockletSpaceGateway: blockletManager.addBlockletSpaceGateway.bind(blockletManager),
|
|
281
|
+
deleteBlockletSpaceGateway: blockletManager.deleteBlockletSpaceGateway.bind(blockletManager),
|
|
282
|
+
updateBlockletSpaceGateway: blockletManager.updateBlockletSpaceGateway.bind(blockletManager),
|
|
283
|
+
getBlockletSpaceGateways: blockletManager.getBlockletSpaceGateways.bind(blockletManager),
|
|
284
|
+
|
|
285
|
+
// blocklet backup record
|
|
286
|
+
getBlockletBackups: blockletManager.getBlockletBackups.bind(blockletManager),
|
|
287
|
+
|
|
258
288
|
// Store
|
|
259
289
|
getBlockletMeta: StoreUtil.getBlockletMeta,
|
|
260
290
|
getStoreMeta: StoreUtil.getStoreMeta,
|
package/lib/locales/en.js
CHANGED
|
@@ -4,4 +4,13 @@ module.exports = flat({
|
|
|
4
4
|
registry: {
|
|
5
5
|
getListError: 'Get Blocklet list from registry "{registryUrl}" failed.',
|
|
6
6
|
},
|
|
7
|
+
backup: {
|
|
8
|
+
space: {
|
|
9
|
+
error: {
|
|
10
|
+
title: 'Backup to spaces encountered error',
|
|
11
|
+
forbidden:
|
|
12
|
+
'You do not have permission to perform the backup, try restoring the application license on DID Spaces or reconnect to DID Spaces and try again',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
7
16
|
});
|
package/lib/locales/index.js
CHANGED
|
@@ -10,6 +10,13 @@ const replace = (template, data) =>
|
|
|
10
10
|
template.replace(/{(\w*)}/g, (_, key) => (Object.prototype.hasOwnProperty.call(data, key) ? data[key] : ''));
|
|
11
11
|
|
|
12
12
|
module.exports = {
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {'zh' | 'en'} locale
|
|
16
|
+
* @param {string} key
|
|
17
|
+
* @param {Record<string, string>} data
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
13
20
|
translate: (locale, key, data) => {
|
|
14
21
|
if (tranlations.has(locale)) {
|
|
15
22
|
return replace(tranlations.get(locale)[key], data);
|
package/lib/locales/zh.js
CHANGED
|
@@ -4,4 +4,12 @@ module.exports = flat({
|
|
|
4
4
|
registry: {
|
|
5
5
|
getListError: '从 "{registryUrl}" 源获取 Blocklet 列表失败.',
|
|
6
6
|
},
|
|
7
|
+
backup: {
|
|
8
|
+
space: {
|
|
9
|
+
error: {
|
|
10
|
+
title: '备份到 Spaces 发生错误',
|
|
11
|
+
forbidden: '你还没有权限执行备份操作,请尝试在 DID Spaces 上恢复应用授权或者重新连接 DID Spaces 后重试',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
7
15
|
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
/* eslint-disable no-continue */
|
|
3
|
+
|
|
4
|
+
const blocklets = {
|
|
5
|
+
z8ia4e5vAeDsQEE2P26bQqz9oWR1Lxg9qUMaV: 'Static Demo',
|
|
6
|
+
z8iZyVVn6XsvcuiYhtdw3GoasMbtqR9BjvJz3: 'Blockchain Explorer',
|
|
7
|
+
z8iZqkCjLP6TZpR12tT3jESWxB8SGzNsx8nZa: 'NFT Store',
|
|
8
|
+
z8ia1WEiBZ7hxURf6LwH21Wpg99vophFwSJdu: 'Discuss Kit',
|
|
9
|
+
z8ia5AUWNBoc5Jw6Zf2ru97W1y6PZVFiFa7h9: 'Coming Soon Page',
|
|
10
|
+
z8iZiDFg3vkkrPwsiba1TLXy3H9XHzFERsP8o: 'Pages Kit',
|
|
11
|
+
z8iZscGk6ohCejHEiX16C7apdFC7JrPYD1J4Z: 'Virtual Gift Card',
|
|
12
|
+
z8ia2birZzhjbXqKnxPUUivmqErdsf3724tr6: 'NFT Maker',
|
|
13
|
+
z8ia1ieY5KhEC4LMRETzS5nUwD7PvAND8qkfX: 'NFT Blender',
|
|
14
|
+
z8iZqeUACK955YaBWqEd8aKg3tTki1GpvE2Wu: 'ArcBridge Node',
|
|
15
|
+
z8ia29UsENBg6tLZUKi2HABj38Cw1LmHZocbQ: 'Blocklet Store',
|
|
16
|
+
z8ia2KGe3icfgRcVc9C1qCbWTBbpP2TrfPu7T: 'FS Chain Manager',
|
|
17
|
+
z8ia3xzq2tMq8CRHfaXj1BTYJyYnEcHbqP8cJ: 'AI Kit',
|
|
18
|
+
z8iZvmERrWxqReWe1HZmkAaZvFeRpkXutfKDk: 'NFT Marketplace',
|
|
19
|
+
z8iZpnScvjjeeyYZQoHSdXm4GQTqcfTTGkyPP: 'DID Wallet',
|
|
20
|
+
z8iZrihfHTTBCBpDqCzrjFer5jop383b5hdPh: 'DID Spaces Enterprise',
|
|
21
|
+
z8ia1mAXo8ZE7ytGF36L5uBf9kD2kenhqFGp9: 'Image Bin',
|
|
22
|
+
z8iZu6GDcVFaSsT7LjrBJC9uAfM6HKyQaCD9U: 'Tower Blocks',
|
|
23
|
+
z8iZyhourKXqn8JKHbFcQDqWoAMsR6ZEi5nCW: 'Mine Sweeper',
|
|
24
|
+
z8ia48jeqzdNhr9smse1tQCmt72G5PnSZaTax: 'MultiSig Vault',
|
|
25
|
+
z8iZngXotuUXxsm6imc7naUUy1G5ycVs7A34H: 'Uptime Kuma',
|
|
26
|
+
z8iZvMrKPa7qy2nxfrKremuSm8bE9Wb9Tu2NA: 'AI Assistant',
|
|
27
|
+
z8ia2kJi2hdqASNBZzRiWQaZ8vshaxgQS67EW: 'DID Spaces Personal',
|
|
28
|
+
z8iZpog7mcgcgBZzTiXJCWESvmnRrQmnd3XBB: 'AI Studio',
|
|
29
|
+
z8iZxVUfZZBPpLhVov5YqsaorNX9F2vKAKeMc: 'Excalidraw',
|
|
30
|
+
z8iZoLRKRXHzqdJ2vFZEi4H5UXT9ADsurxZRK: 'Tweet Token',
|
|
31
|
+
z8ia5gwZog5Ut4TfUJP4k82fXKQN8iWZp2bfG: 'Token Prize Pool',
|
|
32
|
+
z8iZorY6mvb5tZrxXTqhBmwu89xjEEazrgT3t: 'Meilisearch',
|
|
33
|
+
z8iZy4P83i6AgnNdNUexsh2kBcsDHoqcwPavn: 'DID Pay',
|
|
34
|
+
z8iZqTiD6tFwEub6t685e3dj18Ekbo8xvqBSV: 'Vote',
|
|
35
|
+
z8iZkFBbrVQxZHvcWWB3Sa2TrfGmSeFz9MSU7: 'Server Launcher',
|
|
36
|
+
z8iZhW61syFGfgMGDm7ttbDATUf4zbNrzxfJG: 'Blocklet Launcher',
|
|
37
|
+
z8iZqxnmW2i3AbgmjuFki1J6KE8e5i5zBWB9k: 'Nostr Verifier',
|
|
38
|
+
z8iZwyBfqwNcGbLCiUnFAQLEzT8sJd2TSjbM2: 'Static Demo',
|
|
39
|
+
z8iZva6oERHPw7qveUwTBKcY8DqUUtcXheBX8: 'Form Builder',
|
|
40
|
+
z8iZrdP3XNxaqzcHqTRewE3BdJiCfeMfNLzTc: 'AD Kit',
|
|
41
|
+
z8ia2XJkmoZDwRBYzrvLqeZAHWz38Ptrz51xf: 'Tweet Assistant',
|
|
42
|
+
z8ia2YJVK83HuwqykTVVe61mtNWEWeR6kVERi: 'aistro',
|
|
43
|
+
z8ia5nxBkFetpK1BzaumvDStQiKyAuHdymnoh: 'Wait Genie',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
module.exports = async ({ states, printInfo }) => {
|
|
47
|
+
printInfo('Try to update component title...');
|
|
48
|
+
|
|
49
|
+
const apps = await states.blocklet.find({});
|
|
50
|
+
|
|
51
|
+
for (const app of apps || []) {
|
|
52
|
+
let shouldUpdate = false;
|
|
53
|
+
for (const component of app.children || []) {
|
|
54
|
+
const title = blocklets[component?.meta?.bundleDid];
|
|
55
|
+
if (component?.meta && title && title !== component.meta.title) {
|
|
56
|
+
component.meta.title = title;
|
|
57
|
+
shouldUpdate = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (shouldUpdate) {
|
|
62
|
+
await states.blocklet.update({ _id: app._id }, { $set: { children: app.children } });
|
|
63
|
+
printInfo(`Blocklet in blocklet.db updated: ${app.meta?.title}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -91,7 +91,7 @@ class BlockletRuntimeMonitor extends EventEmitter {
|
|
|
91
91
|
if (status !== BlockletStatus.running) {
|
|
92
92
|
if (this.data[blockletDid]) {
|
|
93
93
|
Object.keys(this.data[blockletDid]).forEach((key) => {
|
|
94
|
-
|
|
94
|
+
this.data[blockletDid][key].runtimeInfo = {};
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
return;
|
package/lib/router/helper.js
CHANGED
|
@@ -117,6 +117,14 @@ const attachRuntimeDomainAliases = async ({ sites = [], context = {}, node }) =>
|
|
|
117
117
|
});
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @description
|
|
122
|
+
* @param {{
|
|
123
|
+
* corsAllowedOrigins: Array<string>;
|
|
124
|
+
* }} site
|
|
125
|
+
* @param {string} rawUrl
|
|
126
|
+
* @return {void}
|
|
127
|
+
*/
|
|
120
128
|
const addCorsToSite = (site, rawUrl) => {
|
|
121
129
|
if (!site || !rawUrl) {
|
|
122
130
|
return;
|
|
@@ -374,12 +382,18 @@ const ensureCorsForWebWallet = async (sites) => {
|
|
|
374
382
|
return sites;
|
|
375
383
|
};
|
|
376
384
|
|
|
385
|
+
/**
|
|
386
|
+
* @description
|
|
387
|
+
* @param {Array<any>} [sites=[]]
|
|
388
|
+
* @param {Array<import('@abtnode/client').BlockletState>} blocklets
|
|
389
|
+
* @return {Promise<any>}
|
|
390
|
+
*/
|
|
377
391
|
const ensureCorsForDidSpace = async (sites = [], blocklets) => {
|
|
378
392
|
return sites.map((site) => {
|
|
379
393
|
const blocklet = blocklets.find((x) => x.meta.did === site.blockletDid);
|
|
380
394
|
if (blocklet) {
|
|
381
395
|
const endpoint = blocklet.environments.find(
|
|
382
|
-
(x) => x.key === BLOCKLET_CONFIGURABLE_KEY.
|
|
396
|
+
(x) => x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT
|
|
383
397
|
);
|
|
384
398
|
if (endpoint && isUrl(endpoint.value)) {
|
|
385
399
|
addCorsToSite(site, endpoint.value);
|
package/lib/states/audit-log.js
CHANGED
|
@@ -272,6 +272,11 @@ const getLogContent = async (action, args, context, result, info, node) => {
|
|
|
272
272
|
}
|
|
273
273
|
};
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* @description 获取日志的种类
|
|
277
|
+
* @param {string} action
|
|
278
|
+
* @return {Promise<'blocklet' | 'team' | 'security' | 'integrations' | 'server' | 'certificates' | 'gateway' | ''>}
|
|
279
|
+
*/
|
|
275
280
|
const getLogCategory = (action) => {
|
|
276
281
|
switch (action) {
|
|
277
282
|
// blocklets
|
|
@@ -294,7 +299,7 @@ const getLogCategory = (action) => {
|
|
|
294
299
|
case 'updateComponentMountPoint':
|
|
295
300
|
return 'blocklet';
|
|
296
301
|
|
|
297
|
-
// store
|
|
302
|
+
// store,此处应该返回 server
|
|
298
303
|
case 'addBlockletStore':
|
|
299
304
|
case 'deleteBlockletStore':
|
|
300
305
|
case 'selectBlockletStore':
|
|
@@ -449,7 +454,7 @@ class AuditLogState extends BaseState {
|
|
|
449
454
|
const data = await this.insert({
|
|
450
455
|
scope: getScope(args) || info.did, // server or blocklet did
|
|
451
456
|
action,
|
|
452
|
-
category: await getLogCategory(action
|
|
457
|
+
category: await getLogCategory(action),
|
|
453
458
|
content: (await getLogContent(action, args, context, result, info, node)).trim(),
|
|
454
459
|
actor: pick(user.actual || user, ['did', 'fullName', 'role']),
|
|
455
460
|
extra: args,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef{{
|
|
3
|
+
appPid: string; // 此处建议使用 appPid
|
|
4
|
+
userDid: string;
|
|
5
|
+
strategy?: 0 | 1;
|
|
6
|
+
|
|
7
|
+
sourceUrl: string;
|
|
8
|
+
|
|
9
|
+
target?: "Spaces" | 'Local';
|
|
10
|
+
targetUrl?: string;
|
|
11
|
+
|
|
12
|
+
createdAt: string;
|
|
13
|
+
updatedAt?: string;
|
|
14
|
+
status?: 0 | 1;
|
|
15
|
+
message?: string;
|
|
16
|
+
* }} Backup
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { Joi } = require('@arcblock/validator');
|
|
20
|
+
const BaseState = require('./base');
|
|
21
|
+
const { validateBackupSuccess, validateBackupFail, validateBackupStart } = require('../validators/backup.js');
|
|
22
|
+
|
|
23
|
+
const validateBackup = Joi.object({
|
|
24
|
+
appPid: Joi.DID(),
|
|
25
|
+
userDid: Joi.DID(),
|
|
26
|
+
// 备份策略: 0 表示自动,1 表示手动,建议使用常量
|
|
27
|
+
strategy: Joi.number().valid(0, 1).default(0).optional(),
|
|
28
|
+
|
|
29
|
+
// 形如, /User/allen/blocklet-server/data/discuss-kit
|
|
30
|
+
sourceUrl: Joi.string().required(),
|
|
31
|
+
|
|
32
|
+
// 备份文件存储在哪?本地还是 spaces 的那个位置?
|
|
33
|
+
// 如果存储在 Spaces, 形如: https://bbqaw5mgxc6fnihrwqcejcxvukkdgkk4anwxwk5msvm.did.abtnet.io/app/space/zNKhe8jwgNZX2z7ZUfwNddNECxSe3wyg7VtS
|
|
34
|
+
// 如果存储在 Local,形如: /User/allen/discuss-kit
|
|
35
|
+
target: Joi.string().valid('Spaces', 'Local').optional().default('Spaces'),
|
|
36
|
+
targetUrl: Joi.string().optional().allow('').default(''),
|
|
37
|
+
|
|
38
|
+
createdAt: Joi.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.default(() => new Date().toISOString()),
|
|
41
|
+
updatedAt: Joi.string().optional().default('').allow(''),
|
|
42
|
+
|
|
43
|
+
// 0 表示成功了,建议使用常量表示,默认是 1 表示错误的
|
|
44
|
+
status: Joi.number().optional().allow(null),
|
|
45
|
+
// 发生错误的时候可以用来存储错误下信息
|
|
46
|
+
message: Joi.string().optional().default('').allow(''),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @description
|
|
51
|
+
* @class BackupState
|
|
52
|
+
*/
|
|
53
|
+
class BackupState extends BaseState {
|
|
54
|
+
constructor(baseDir, config = {}) {
|
|
55
|
+
super(baseDir, {
|
|
56
|
+
filename: 'backup.db',
|
|
57
|
+
...config,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @description
|
|
63
|
+
* @param {Pick<Backup, 'appPid' | 'userDid' | 'strategy' | 'sourceUrl' | 'target'>} backup
|
|
64
|
+
* @return {Promise<Backup & {_id: string}>}
|
|
65
|
+
* @memberof BackupState
|
|
66
|
+
*/
|
|
67
|
+
async start(backup) {
|
|
68
|
+
const { error, value } = validateBackupStart.validate(backup, {
|
|
69
|
+
stripUnknown: true,
|
|
70
|
+
allowUnknown: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (error) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.create({
|
|
78
|
+
...value,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @description
|
|
84
|
+
* @param {string} id
|
|
85
|
+
* @param {Pick<Backup, 'targetUrl'>} successBackupInfo
|
|
86
|
+
* @return {PromiseLike<import('@nedb/core').UpdateResult<Backup>>}
|
|
87
|
+
* @memberof BackupState
|
|
88
|
+
*/
|
|
89
|
+
async success(id, successBackupInfo) {
|
|
90
|
+
const { error, value } = validateBackupSuccess.validate(successBackupInfo, {
|
|
91
|
+
stripUnknown: true,
|
|
92
|
+
allowUnknown: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (error) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.update(
|
|
100
|
+
{ _id: id },
|
|
101
|
+
{
|
|
102
|
+
$set: {
|
|
103
|
+
...value,
|
|
104
|
+
status: 0,
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @description
|
|
113
|
+
* @param {string} id
|
|
114
|
+
* @param {Pick<Backup, 'message'>} errorBackupInfo
|
|
115
|
+
* @return {PromiseLike<import('@nedb/core').UpdateResult<Backup>>}
|
|
116
|
+
* @memberof BackupState
|
|
117
|
+
*/
|
|
118
|
+
async fail(id, errorBackupInfo) {
|
|
119
|
+
const { error, value } = validateBackupFail.validate(errorBackupInfo, {
|
|
120
|
+
stripUnknown: true,
|
|
121
|
+
allowUnknown: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (error) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this.update(
|
|
129
|
+
{ _id: id },
|
|
130
|
+
{
|
|
131
|
+
$set: {
|
|
132
|
+
...value,
|
|
133
|
+
status: 1,
|
|
134
|
+
updatedAt: new Date().toISOString(),
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description
|
|
142
|
+
* @param {Backup} backup
|
|
143
|
+
* @return {Promise<Backup & {_id: string}>}
|
|
144
|
+
* @memberof BackupState
|
|
145
|
+
*/
|
|
146
|
+
async create(backup) {
|
|
147
|
+
const { error, value } = validateBackup.validate(backup, {
|
|
148
|
+
allowUnknown: true,
|
|
149
|
+
stripUnknown: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (error) {
|
|
153
|
+
throw new Error(error.message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return this.insert(value);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @description
|
|
161
|
+
* @param {{ did: string }} { did }
|
|
162
|
+
* @return {Promise<Array<import('@abtnode/client').Backup>>}
|
|
163
|
+
*/
|
|
164
|
+
async getBlockletBackups({ did }) {
|
|
165
|
+
const backups = await this.cursor({
|
|
166
|
+
appPid: did,
|
|
167
|
+
})
|
|
168
|
+
.sort({
|
|
169
|
+
updatedAt: -1,
|
|
170
|
+
})
|
|
171
|
+
.exec();
|
|
172
|
+
|
|
173
|
+
return backups ?? [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = BackupState;
|
|
@@ -91,6 +91,12 @@ class BlockletExtrasState extends BaseState {
|
|
|
91
91
|
this.generateExtraFns();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @description
|
|
96
|
+
* @param {string} did
|
|
97
|
+
* @return {Promise<number>}
|
|
98
|
+
* @memberof BlockletExtrasState
|
|
99
|
+
*/
|
|
94
100
|
delete(did) {
|
|
95
101
|
return this.remove({ did });
|
|
96
102
|
}
|
package/lib/states/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const SessionState = require('./session');
|
|
|
11
11
|
const ExtrasState = require('./blocklet-extras');
|
|
12
12
|
const CacheState = require('./cache');
|
|
13
13
|
const AuditLogState = require('./audit-log');
|
|
14
|
+
const BackupState = require('./backup');
|
|
14
15
|
|
|
15
16
|
const init = (dataDirs, config) => {
|
|
16
17
|
const notificationState = new NotificationState(dataDirs.core, config);
|
|
@@ -25,6 +26,7 @@ const init = (dataDirs, config) => {
|
|
|
25
26
|
const extrasState = new ExtrasState(dataDirs.core, config);
|
|
26
27
|
const cacheState = new CacheState(dataDirs.core, config);
|
|
27
28
|
const auditLogState = new AuditLogState(dataDirs.core, config);
|
|
29
|
+
const backupState = new BackupState(dataDirs.core, config);
|
|
28
30
|
|
|
29
31
|
return {
|
|
30
32
|
node: nodeState,
|
|
@@ -39,6 +41,7 @@ const init = (dataDirs, config) => {
|
|
|
39
41
|
blockletExtras: extrasState,
|
|
40
42
|
cache: cacheState,
|
|
41
43
|
auditLog: auditLogState,
|
|
44
|
+
backup: backupState,
|
|
42
45
|
};
|
|
43
46
|
};
|
|
44
47
|
|
package/lib/states/user.js
CHANGED
|
@@ -7,7 +7,6 @@ const { isValid } = require('@arcblock/did');
|
|
|
7
7
|
const { PASSPORT_STATUS } = require('@abtnode/constant');
|
|
8
8
|
const { upsertToPassports } = require('@abtnode/auth/lib/passport');
|
|
9
9
|
const { fromAppDid } = require('@arcblock/did-ext');
|
|
10
|
-
const { types } = require('@arcblock/did');
|
|
11
10
|
|
|
12
11
|
const BaseState = require('./base');
|
|
13
12
|
const { validateOwner } = require('../util');
|
|
@@ -400,7 +399,7 @@ class User extends BaseState {
|
|
|
400
399
|
sourceProvider = 'wallet';
|
|
401
400
|
connectedAccounts.forEach((account) => {
|
|
402
401
|
if (account.id && blockletSk) {
|
|
403
|
-
const accountWallet = fromAppDid(account.id, blockletSk
|
|
402
|
+
const accountWallet = fromAppDid(account.id, blockletSk);
|
|
404
403
|
account.did = accountWallet.address;
|
|
405
404
|
account.pk = accountWallet.publicKey;
|
|
406
405
|
account.firstLoginAt = account.firstLoginAt || new Date().toISOString();
|
package/lib/util/blocklet.js
CHANGED
|
@@ -543,7 +543,8 @@ const startBlockletProcess = async (
|
|
|
543
543
|
namespace: 'blocklets',
|
|
544
544
|
name: processId,
|
|
545
545
|
cwd: appCwd,
|
|
546
|
-
|
|
546
|
+
// FIXME @linchen [] does not work, so use () here
|
|
547
|
+
log_date_format: '(YYYY-MM-DD HH:mm:ss)',
|
|
547
548
|
output: path.join(logsDir, 'output.log'),
|
|
548
549
|
error: path.join(logsDir, 'error.log'),
|
|
549
550
|
wait_ready: process.env.NODE_ENV !== 'test',
|
|
@@ -1699,6 +1700,16 @@ const validateAppConfig = async (config, states) => {
|
|
|
1699
1700
|
throw new Error(`${x.key}(${x.value}) is not a valid http address`);
|
|
1700
1701
|
}
|
|
1701
1702
|
}
|
|
1703
|
+
|
|
1704
|
+
if (x.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT) {
|
|
1705
|
+
if (isEmpty(x.value)) {
|
|
1706
|
+
throw new Error(`${x.key} can not be empty`);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (!isUrl(x.value)) {
|
|
1710
|
+
throw new Error(`${x.key}(${x.value}) is not a valid http address`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1702
1713
|
};
|
|
1703
1714
|
|
|
1704
1715
|
const checkDuplicateAppSk = async ({ sk, did, states }) => {
|
package/lib/util/queue.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const createQueue = require('@abtnode/queue');
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
*
|
|
6
|
+
* @typedef {
|
|
7
|
+
* EventEmitter & {
|
|
8
|
+
* store: JobStore;
|
|
9
|
+
* push: (...args: {
|
|
10
|
+
* job: any;
|
|
11
|
+
* jobId: string;
|
|
12
|
+
* persist: boolean;
|
|
13
|
+
* delay: number;
|
|
14
|
+
* }) => EventEmitter;
|
|
15
|
+
* get: (id: string) => Promise<any>;
|
|
16
|
+
* cancel: (id: string) => Promise<...>;
|
|
17
|
+
* options: {}
|
|
18
|
+
* }
|
|
19
|
+
* } Queue
|
|
20
|
+
*/
|
|
21
|
+
|
|
4
22
|
/**
|
|
5
23
|
*
|
|
6
24
|
* @param {Object} Options
|
|
@@ -14,6 +32,7 @@ const createQueue = require('@abtnode/queue');
|
|
|
14
32
|
* @param {number} Options.options.maxRetries [param=1] number of max retries, default 1
|
|
15
33
|
* @param {number} Options.options.maxTimeout [param=86400000] max timeout, in ms, default 86400000ms(1d)
|
|
16
34
|
* @param {number} Options.options.retryDelay [param=0] retry delay, in ms, default 0ms
|
|
35
|
+
* @returns {Queue}
|
|
17
36
|
*/
|
|
18
37
|
module.exports = ({ name, dataDir, onJob, daemon = false, options = {} }) => {
|
|
19
38
|
if (daemon) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { BLOCKLET_CONFIGURABLE_KEY } = require('@blocklet/constant');
|
|
2
|
+
const isEmpty = require('lodash/isEmpty');
|
|
3
|
+
const joinUrl = require('url-join');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @description
|
|
7
|
+
* @param {import('@abtnode/client').ConfigEntry[]} configs
|
|
8
|
+
* @return {string | null}
|
|
9
|
+
*/
|
|
10
|
+
const getDIDSpaceBackupEndpoint = (configs) => {
|
|
11
|
+
return (
|
|
12
|
+
configs?.find((config) => config.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_BACKUP_ENDPOINT)?.value || null
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @FIXME 希望之后放在 sdk 中 @jianchao
|
|
18
|
+
* @description
|
|
19
|
+
* @param {string} endpoint
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function getBackupFilesUrlFromEndpoint(endpoint) {
|
|
23
|
+
if (isEmpty(endpoint)) {
|
|
24
|
+
throw new Error(`Endpoint(${endpoint}) cannot be empty`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const prefix = endpoint.replace(/\/api\/space\/.+/, '');
|
|
28
|
+
|
|
29
|
+
const strArray = endpoint.replace(/\/$/, '').split('/');
|
|
30
|
+
const spaceDid = strArray.at(-4);
|
|
31
|
+
const appDid = strArray.at(-2);
|
|
32
|
+
|
|
33
|
+
return joinUrl(prefix, 'space', spaceDid, 'apps', appDid, 'explorer', `?key=/apps/${appDid}/.did-objects/${appDid}/`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
getDIDSpaceBackupEndpoint,
|
|
38
|
+
getBackupFilesUrlFromEndpoint,
|
|
39
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { Joi } = require('@arcblock/validator');
|
|
2
|
+
|
|
3
|
+
const validateBackupStart = Joi.object({
|
|
4
|
+
appPid: Joi.DID().required(),
|
|
5
|
+
userDid: Joi.DID().required(),
|
|
6
|
+
strategy: Joi.number().valid(0, 1).optional().default(0),
|
|
7
|
+
|
|
8
|
+
sourceUrl: Joi.string().required(),
|
|
9
|
+
target: Joi.string().valid('Spaces', 'Local').optional().default('Spaces'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const validateBackupSuccess = Joi.object({
|
|
13
|
+
targetUrl: Joi.string().required(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const validateBackupFail = Joi.object({
|
|
17
|
+
message: Joi.string().required(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
validateBackupStart,
|
|
22
|
+
validateBackupSuccess,
|
|
23
|
+
validateBackupFail,
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { Joi } = require('@arcblock/validator');
|
|
2
|
+
|
|
3
|
+
const validateAddSpaceGateway = Joi.object({
|
|
4
|
+
url: Joi.string()
|
|
5
|
+
.uri({ scheme: ['http', 'https'] })
|
|
6
|
+
.required(),
|
|
7
|
+
name: Joi.string().required(),
|
|
8
|
+
protected: Joi.boolean().optional().allow(null),
|
|
9
|
+
endpoint: Joi.string()
|
|
10
|
+
.uri({ scheme: ['http', 'https'] })
|
|
11
|
+
.optional()
|
|
12
|
+
.allow(''),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const validateUpdateSpaceGateway = Joi.object({
|
|
16
|
+
url: Joi.string()
|
|
17
|
+
.uri({ scheme: ['http', 'https'] })
|
|
18
|
+
.required(),
|
|
19
|
+
name: Joi.string().optional().allow(''),
|
|
20
|
+
protected: Joi.boolean().optional().allow(null),
|
|
21
|
+
endpoint: Joi.string()
|
|
22
|
+
.uri({ scheme: ['http', 'https'] })
|
|
23
|
+
.optional()
|
|
24
|
+
.allow(''),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
validateAddSpaceGateway,
|
|
29
|
+
validateUpdateSpaceGateway,
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.16.
|
|
6
|
+
"version": "1.16.8-beta-0c0c5eb2",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,18 +19,18 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@abtnode/auth": "1.16.
|
|
23
|
-
"@abtnode/certificate-manager": "1.16.
|
|
24
|
-
"@abtnode/constant": "1.16.
|
|
25
|
-
"@abtnode/cron": "1.16.
|
|
26
|
-
"@abtnode/db": "1.16.
|
|
27
|
-
"@abtnode/logger": "1.16.
|
|
28
|
-
"@abtnode/queue": "1.16.
|
|
29
|
-
"@abtnode/rbac": "1.16.
|
|
30
|
-
"@abtnode/router-provider": "1.16.
|
|
31
|
-
"@abtnode/static-server": "1.16.
|
|
32
|
-
"@abtnode/timemachine": "1.16.
|
|
33
|
-
"@abtnode/util": "1.16.
|
|
22
|
+
"@abtnode/auth": "1.16.8-beta-0c0c5eb2",
|
|
23
|
+
"@abtnode/certificate-manager": "1.16.8-beta-0c0c5eb2",
|
|
24
|
+
"@abtnode/constant": "1.16.8-beta-0c0c5eb2",
|
|
25
|
+
"@abtnode/cron": "1.16.8-beta-0c0c5eb2",
|
|
26
|
+
"@abtnode/db": "1.16.8-beta-0c0c5eb2",
|
|
27
|
+
"@abtnode/logger": "1.16.8-beta-0c0c5eb2",
|
|
28
|
+
"@abtnode/queue": "1.16.8-beta-0c0c5eb2",
|
|
29
|
+
"@abtnode/rbac": "1.16.8-beta-0c0c5eb2",
|
|
30
|
+
"@abtnode/router-provider": "1.16.8-beta-0c0c5eb2",
|
|
31
|
+
"@abtnode/static-server": "1.16.8-beta-0c0c5eb2",
|
|
32
|
+
"@abtnode/timemachine": "1.16.8-beta-0c0c5eb2",
|
|
33
|
+
"@abtnode/util": "1.16.8-beta-0c0c5eb2",
|
|
34
34
|
"@arcblock/did": "1.18.78",
|
|
35
35
|
"@arcblock/did-auth": "1.18.78",
|
|
36
36
|
"@arcblock/did-ext": "^1.18.78",
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
"@arcblock/event-hub": "1.18.78",
|
|
40
40
|
"@arcblock/jwt": "^1.18.78",
|
|
41
41
|
"@arcblock/pm2-events": "^0.0.5",
|
|
42
|
+
"@arcblock/validator": "^1.18.77",
|
|
42
43
|
"@arcblock/vc": "1.18.78",
|
|
43
|
-
"@blocklet/constant": "1.16.
|
|
44
|
-
"@blocklet/meta": "1.16.
|
|
45
|
-
"@blocklet/sdk": "1.16.
|
|
44
|
+
"@blocklet/constant": "1.16.8-beta-0c0c5eb2",
|
|
45
|
+
"@blocklet/meta": "1.16.8-beta-0c0c5eb2",
|
|
46
|
+
"@blocklet/sdk": "1.16.8-beta-0c0c5eb2",
|
|
46
47
|
"@did-space/client": "^0.2.90",
|
|
47
48
|
"@fidm/x509": "^1.2.1",
|
|
48
49
|
"@ocap/mcrypto": "1.18.78",
|
|
@@ -93,5 +94,5 @@
|
|
|
93
94
|
"express": "^4.18.2",
|
|
94
95
|
"jest": "^27.5.1"
|
|
95
96
|
},
|
|
96
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "0d8ec3154caf670770284dbdabc61a5f4e8b205f"
|
|
97
98
|
}
|