querysub 0.156.0 → 0.158.0
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/package.json +2 -2
- package/src/-b-authorities/cloudflareHelpers.ts +5 -3
- package/src/-b-authorities/dnsAuthority.ts +1 -3
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -2
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +33 -49
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +6 -5
- package/src/0-path-value-core/pathValueCore.ts +8 -6
- package/src/2-proxy/PathValueProxyWatcher.ts +11 -1
- package/src/2-proxy/archiveMoveHarness.ts +4 -6
- package/src/2-proxy/garbageCollection.ts +9 -0
- package/src/2-proxy/schema2.ts +9 -0
- package/src/3-path-functions/PathFunctionRunner.ts +1 -1
- package/src/3-path-functions/syncSchema.ts +14 -3
- package/src/4-deploy/edgeNodes.ts +2 -2
- package/src/4-dom/qreact.tsx +1 -1
- package/src/4-querysub/Querysub.ts +25 -7
- package/src/5-diagnostics/Table.tsx +6 -1
- package/src/archiveapps/archiveMergeEntry.tsx +1 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +0 -1
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +0 -1
- package/src/diagnostics/managementPages.tsx +16 -6
- package/src/diagnostics/periodic.ts +21 -3
- package/src/server.ts +1 -1
- package/src/user-implementation/LoginPage.tsx +18 -2
- package/src/user-implementation/UserPage.tsx +2 -2
- package/src/user-implementation/addSuperUser.ts +1 -2
- package/src/user-implementation/loginEmail.tsx +7 -7
- package/src/user-implementation/userData.ts +60 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.158.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
25
25
|
"pako": "^2.1.0",
|
|
26
26
|
"preact": "^10.11.3",
|
|
27
|
-
"socket-function": "^0.
|
|
27
|
+
"socket-function": "^0.90.0",
|
|
28
28
|
"terser": "^5.31.0",
|
|
29
29
|
"typesafecss": "^0.6.3",
|
|
30
30
|
"yaml": "^2.5.0",
|
|
@@ -3,15 +3,17 @@ import { getArchives } from "../-a-archives/archives";
|
|
|
3
3
|
import { lazy } from "socket-function/src/caching";
|
|
4
4
|
import { getStorageDir, getSubFolder } from "../fs";
|
|
5
5
|
import fs from "fs";
|
|
6
|
-
|
|
6
|
+
import { isNodeTrue } from "socket-function/src/misc";
|
|
7
|
+
|
|
8
|
+
export const keys = lazy(() => getArchives("keys"));
|
|
7
9
|
|
|
8
10
|
export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: string }> => {
|
|
9
|
-
let credsJSON = await keys.get("cloudflare.json");
|
|
11
|
+
let credsJSON = await keys().get("cloudflare.json");
|
|
10
12
|
if (!credsJSON) {
|
|
11
13
|
let localPath = getStorageDir() + "cloudflare.json";
|
|
12
14
|
if (fs.existsSync(localPath)) {
|
|
13
15
|
credsJSON = fs.readFileSync(localPath);
|
|
14
|
-
await keys.set("cloudflare.json", credsJSON);
|
|
16
|
+
await keys().set("cloudflare.json", credsJSON);
|
|
15
17
|
} else {
|
|
16
18
|
throw new Error(`b2:/keys/cloudflare.json is missing. It should contain { "key": "your-key", "email": "your-email" }`);
|
|
17
19
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import { cache, lazy } from "socket-function/src/caching";
|
|
4
|
-
import { isNode } from "socket-function/src/misc";
|
|
4
|
+
import { isNode, isNodeTrue } from "socket-function/src/misc";
|
|
5
5
|
import { httpsRequest } from "../https";
|
|
6
6
|
import { getStorageDir } from "../fs";
|
|
7
7
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
@@ -16,8 +16,6 @@ const DNS_TTLSeconds = {
|
|
|
16
16
|
"A": 60,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export const keys = getArchives("keys");
|
|
20
|
-
|
|
21
19
|
export const hasDNSWritePermissions = lazy(async () => {
|
|
22
20
|
if (!isNode()) return false;
|
|
23
21
|
if (isClient()) return false;
|
|
@@ -243,13 +243,12 @@ export async function triggerNodeChange() {
|
|
|
243
243
|
}));
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
let seenNodesForEvilDNS = new Set<string>();
|
|
247
246
|
async function syncArchives() {
|
|
248
247
|
if (isServer()) {
|
|
249
248
|
// Make sure we are present
|
|
250
249
|
await writeHeartbeat();
|
|
251
250
|
let nodeIds = await archives().find("");
|
|
252
|
-
console.log(green(`Syncing archives`), { nodeIds });
|
|
251
|
+
console.log(green(`Syncing node ids from archives`), { nodeIds });
|
|
253
252
|
setNodeIds(nodeIds);
|
|
254
253
|
} else {
|
|
255
254
|
if (isNoNetwork() || !isNode()) {
|
|
@@ -18,7 +18,6 @@ import { logNodeStateStats, logNodeStats } from "../../-0-hooks/hooks";
|
|
|
18
18
|
/** Clean up old files after a while */
|
|
19
19
|
const DEAD_CREATE_THRESHOLD = timeInHour * 12;
|
|
20
20
|
const ARCHIVE_PROPAGATION_TIME = 5000;
|
|
21
|
-
const LOG = false;
|
|
22
21
|
// NOTE: There isn't a reason for transaction to not apply. If applying throws, we throw,
|
|
23
22
|
// and can throw infinite times, but if it doesn't throw, it SHOULD be applied. We retry
|
|
24
23
|
// a few times though, just in case the storage system was having some issues. After enough
|
|
@@ -309,9 +308,7 @@ class TransactionLocker {
|
|
|
309
308
|
}
|
|
310
309
|
public async createConfirm(key: string) {
|
|
311
310
|
let path = this.getConfirmKey(key);
|
|
312
|
-
|
|
313
|
-
console.log(`Creating confirmation for ${key}`);
|
|
314
|
-
}
|
|
311
|
+
diskLog(`Creating confirmation for ${key}`);
|
|
315
312
|
await this.storage.setValue(path, Buffer.from(""));
|
|
316
313
|
return path;
|
|
317
314
|
}
|
|
@@ -363,9 +360,10 @@ class TransactionLocker {
|
|
|
363
360
|
delete: ellipsize(deletes.map(a => debugFileInfo(a.key)).join(","), 50),
|
|
364
361
|
});
|
|
365
362
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
363
|
+
diskLog(`Writing transaction`, {
|
|
364
|
+
name,
|
|
365
|
+
ops: transaction.ops.length,
|
|
366
|
+
});
|
|
369
367
|
|
|
370
368
|
let key = `transaction_${name}.transaction`;
|
|
371
369
|
let strippedTransaction: Transaction = { ops: transaction.ops.map(a => ({ ...a, value: undefined })) };
|
|
@@ -490,10 +488,8 @@ class TransactionLocker {
|
|
|
490
488
|
while (true) {
|
|
491
489
|
let result = await tryToRead();
|
|
492
490
|
if (result) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
console.log(`Read data state in ${formatTime(timeToRead)}`);
|
|
496
|
-
}
|
|
491
|
+
let timeToRead = Date.now() - startTime;
|
|
492
|
+
diskLog(`Read data state in ${formatTime(timeToRead)}`);
|
|
497
493
|
return result;
|
|
498
494
|
}
|
|
499
495
|
}
|
|
@@ -532,20 +528,20 @@ class TransactionLocker {
|
|
|
532
528
|
let rawLookup = new Set(Array.from(rawDataFiles).map(a => a.file));
|
|
533
529
|
// If any creates are not confirmed, it must not have been applied
|
|
534
530
|
if (transaction.ops.some(a => a.type === "create" && rawLookup.has(a.key) && !confirmedKeys.has(a.key))) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
}
|
|
531
|
+
diskLog(`Transaction not applied (has pending confirmations of creates)`, {
|
|
532
|
+
keys: transaction.ops
|
|
533
|
+
.filter(a => a.type === "create" && rawLookup.has(a.key) && !confirmedKeys.has(a.key))
|
|
534
|
+
.map(a => a.key)
|
|
535
|
+
});
|
|
542
536
|
return false;
|
|
543
537
|
}
|
|
544
538
|
// If any deletes still exist, it must not have been applied
|
|
545
539
|
if (transaction.ops.some(a => a.type === "delete" && confirmedKeys.has(a.key))) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
540
|
+
diskLog(`Transaction not applied (has pending deletes)`, {
|
|
541
|
+
keys: transaction.ops
|
|
542
|
+
.filter(a => a.type === "delete" && confirmedKeys.has(a.key))
|
|
543
|
+
.map(a => a.key)
|
|
544
|
+
});
|
|
549
545
|
return false;
|
|
550
546
|
}
|
|
551
547
|
return true;
|
|
@@ -554,9 +550,9 @@ class TransactionLocker {
|
|
|
554
550
|
let createCount = transaction.ops.filter(a => a.type === "create").length;
|
|
555
551
|
let deleteCount = transaction.ops.filter(a => a.type === "delete").length;
|
|
556
552
|
let lockedFiles = transaction.lockedFilesMustEqual?.length;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
553
|
+
diskLog(`Applying transaction with ${createCount} creates and ${deleteCount} deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
|
|
554
|
+
transactions: transaction.ops.map(x => JSON.stringify(x)),
|
|
555
|
+
});
|
|
560
556
|
logNodeStats(`archives|TΔ Apply`, formatNumber, 1);
|
|
561
557
|
let opsRemaining = transaction.ops.slice();
|
|
562
558
|
// NOTE: Order doesn't matter here. If anything is reading the values
|
|
@@ -580,9 +576,9 @@ class TransactionLocker {
|
|
|
580
576
|
};
|
|
581
577
|
await Promise.all(list(CONCURRENT_WRITE_COUNT).map(runThread));
|
|
582
578
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
579
|
+
diskLog(`Applied transaction with ${createCount} creates and ${deleteCount} deletes. ${lockedFiles !== undefined && `Lock state depends on ${lockedFiles} files` || ""}`, {
|
|
580
|
+
transactions: transaction.ops.map(x => JSON.stringify(x)),
|
|
581
|
+
});
|
|
586
582
|
}
|
|
587
583
|
|
|
588
584
|
/** Only returns data files (no transaction files, or confirmations).
|
|
@@ -646,9 +642,7 @@ class TransactionLocker {
|
|
|
646
642
|
let threshold = activeT.createTime + this.storage.propagationTime;
|
|
647
643
|
if (Date.now() < threshold) {
|
|
648
644
|
let waitTime = threshold - Date.now();
|
|
649
|
-
|
|
650
|
-
console.log(`Waiting ${formatTime(waitTime)} for transaction ${activeT.seqNum} to settle.`);
|
|
651
|
-
}
|
|
645
|
+
diskLog(`Waiting ${formatTime(waitTime)} for transaction ${activeT.seqNum} to settle.`);
|
|
652
646
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
653
647
|
return this.getFilesBase();
|
|
654
648
|
}
|
|
@@ -688,9 +682,7 @@ class TransactionLocker {
|
|
|
688
682
|
// triggered (without this check), and deletes all of our files...
|
|
689
683
|
let unconfirmedOldFiles2 = veryOldFiles.filter(a => !doubleCheckLookup.has(a) && doubleCheckDataFiles.has(a.file));
|
|
690
684
|
console.warn(red(`Deleted ${unconfirmedOldFiles2.length} very old unconfirmed files`));
|
|
691
|
-
|
|
692
|
-
logNodeStats(`archives|TΔ Delete Old Rejected File`, formatNumber, unconfirmedOldFiles2.length);
|
|
693
|
-
}
|
|
685
|
+
diskLog(`archives|TΔ Delete Old Rejected File`, formatNumber, unconfirmedOldFiles2.length);
|
|
694
686
|
// At the point the file was very old when we started reading, not part of the active transaction.
|
|
695
687
|
for (let file of unconfirmedOldFiles2) {
|
|
696
688
|
await this.deleteDataFile(file.file, `old unconfirmed file (${getOwnNodeId()}, ${process.argv[1]})`);
|
|
@@ -706,10 +698,8 @@ class TransactionLocker {
|
|
|
706
698
|
let oldEnoughConfirms = dataState.rawDataFiles.filter(x => x.file.endsWith(".confirm") && x.createTime < oldThreshold);
|
|
707
699
|
let deprecatedFiles = oldEnoughConfirms.filter(a => !usedConfirmations.has(a.file));
|
|
708
700
|
if (deprecatedFiles.length > 0) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
logNodeStats(`archives|TΔ Delete Deprecated Confirm`, formatNumber, deprecatedFiles.length);
|
|
712
|
-
}
|
|
701
|
+
console.warn(red(`Deleted ${deprecatedFiles.length} / ${oldEnoughConfirms.length} confirmations, for not having corresponding data files`));
|
|
702
|
+
logNodeStats(`archives|TΔ Delete Deprecated Confirm`, formatNumber, deprecatedFiles.length);
|
|
713
703
|
for (let file of deprecatedFiles) {
|
|
714
704
|
await this.storage.deleteKey(file.file);
|
|
715
705
|
}
|
|
@@ -744,12 +734,10 @@ class TransactionLocker {
|
|
|
744
734
|
|
|
745
735
|
/** If any deleted files were deleted by other transactions, then we will be rejected. */
|
|
746
736
|
public async addTransaction(transaction: Transaction): Promise<"accepted" | "rejected"> {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
console.log(blue(`Starting transaction with ${creates} creates and ${dels} deletes, ${formatNumber(createBytes)}B`));
|
|
752
|
-
}
|
|
737
|
+
let dels = transaction.ops.filter(a => a.type === "delete").length;
|
|
738
|
+
let creates = transaction.ops.filter(a => a.type === "create").length;
|
|
739
|
+
let createBytes = transaction.ops.map(a => a.type === "create" && a.value?.length || 0).reduce((a, b) => a + b, 0);
|
|
740
|
+
diskLog(`Starting transaction with ${creates} creates and ${dels} deletes, ${formatNumber(createBytes)}B`);
|
|
753
741
|
transaction = { ...transaction, ops: transaction.ops.slice() };
|
|
754
742
|
function normalizePath(path: string) {
|
|
755
743
|
// Replace duplicate slashes with a single slash
|
|
@@ -775,9 +763,7 @@ class TransactionLocker {
|
|
|
775
763
|
let beforeData = await this.getFilesBase();
|
|
776
764
|
if (!this.isTransactionValid(transaction, beforeData.dataFiles, beforeData.rawDataFiles)) {
|
|
777
765
|
logNodeStats(`archives|TΔ Rejected`, formatNumber, 1);
|
|
778
|
-
|
|
779
|
-
console.log(red(`Finished transaction with rejection, ${transaction.ops.length} ops`));
|
|
780
|
-
}
|
|
766
|
+
diskLog(`Finished transaction with rejection, ${transaction.ops.length} ops`);
|
|
781
767
|
return "rejected";
|
|
782
768
|
}
|
|
783
769
|
|
|
@@ -786,9 +772,7 @@ class TransactionLocker {
|
|
|
786
772
|
let afterData = await this.getFilesBase();
|
|
787
773
|
if (this.wasTransactionApplied(transaction, afterData.dataFiles, afterData.rawDataFiles)) {
|
|
788
774
|
logNodeStats(`archives|TΔ Accepted`, formatNumber, 1);
|
|
789
|
-
|
|
790
|
-
console.log(green(`Finished transaction with ${transaction.ops.length} ops`));
|
|
791
|
-
}
|
|
775
|
+
diskLog(`Finished transaction with ${transaction.ops.length} ops`);
|
|
792
776
|
return "accepted";
|
|
793
777
|
}
|
|
794
778
|
}
|
|
@@ -6,8 +6,9 @@ import { ignoreErrors, logErrors, timeoutToUndefinedSilent } from "../../errors"
|
|
|
6
6
|
import { green, magenta } from "socket-function/src/formatting/logColors";
|
|
7
7
|
import { devDebugbreak } from "../../config";
|
|
8
8
|
import { sort } from "socket-function/src/misc";
|
|
9
|
+
import { lazy } from "socket-function/src/caching";
|
|
9
10
|
|
|
10
|
-
const snapshots = getArchives("snapshots");
|
|
11
|
+
const snapshots = lazy(() => getArchives("snapshots"));
|
|
11
12
|
|
|
12
13
|
export type ArchiveSnapshotOverview = {
|
|
13
14
|
// If live, it isn't a real file that can be loaded
|
|
@@ -54,7 +55,7 @@ export async function saveSnapshot(config: {
|
|
|
54
55
|
}) {
|
|
55
56
|
let { files } = config;
|
|
56
57
|
let overview = getSnapshotOverview(files);
|
|
57
|
-
await snapshots.set(overview.file, Buffer.from(files.join("\n")));
|
|
58
|
+
await snapshots().set(overview.file, Buffer.from(files.join("\n")));
|
|
58
59
|
}
|
|
59
60
|
function overviewToFileName(overview: Omit<ArchiveSnapshotOverview, "file">): string {
|
|
60
61
|
return Object.entries({
|
|
@@ -84,7 +85,7 @@ function fileNameToOverview(fileName: string): ArchiveSnapshotOverview {
|
|
|
84
85
|
|
|
85
86
|
// pathValueArchives.decodeDataPath
|
|
86
87
|
export async function getSnapshotList(): Promise<ArchiveSnapshotOverview[]> {
|
|
87
|
-
let snapshotFiles = await snapshots.find("");
|
|
88
|
+
let snapshotFiles = await snapshots().find("");
|
|
88
89
|
|
|
89
90
|
let overview: ArchiveSnapshotOverview[] = [];
|
|
90
91
|
let locker = await pathValueArchives.getArchiveLocker();
|
|
@@ -128,7 +129,7 @@ async function getSnapshotBase(snapshotFile: string | "live") {
|
|
|
128
129
|
files: allFiles.map(x => x.file),
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
|
-
let data = await snapshots.get(snapshotFile);
|
|
132
|
+
let data = await snapshots().get(snapshotFile);
|
|
132
133
|
if (!data) {
|
|
133
134
|
throw new Error(`Snapshot not found: ${snapshotFile}`);
|
|
134
135
|
}
|
|
@@ -158,7 +159,7 @@ export async function loadSnapshot(config: {
|
|
|
158
159
|
console.log(magenta(`Loading snapshot: ${overview.file}`));
|
|
159
160
|
let locker = await pathValueArchives.getArchiveLocker();
|
|
160
161
|
|
|
161
|
-
let snapshotData = await snapshots.get(overview.file);
|
|
162
|
+
let snapshotData = await snapshots().get(overview.file);
|
|
162
163
|
if (!snapshotData) {
|
|
163
164
|
throw new Error(`Snapshot not found: ${overview.file}`);
|
|
164
165
|
}
|
|
@@ -1768,13 +1768,15 @@ class WriteValidStorage {
|
|
|
1768
1768
|
let authorityValue = authorityStorage.getValueExactIgnoreInvalid(write.path, write.time);
|
|
1769
1769
|
if (authorityValue) {
|
|
1770
1770
|
authorityValue.valid = write.isValid;
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1771
|
+
// NOTE: I think it is fine now for us to set the valid state early? Hopefully...
|
|
1772
|
+
// because it happens A LOT.
|
|
1773
|
+
// if (!isCoreQuiet) {
|
|
1774
|
+
// console.log(`Setting valid state of ${debugPathValuePath(authorityValue)} to ${write.isValid}`);
|
|
1775
|
+
// }
|
|
1774
1776
|
} else {
|
|
1775
|
-
if (isNode()) {
|
|
1776
|
-
|
|
1777
|
-
}
|
|
1777
|
+
// if (isNode()) {
|
|
1778
|
+
// console.error(`Setting valid state of ${write.path}@${debugTime(write.time)} to ${write.isValid}, but the ValuePath was not found. If the ValuePath is found later, it might not have the valid state set correctly.`);
|
|
1779
|
+
// }
|
|
1778
1780
|
}
|
|
1779
1781
|
}
|
|
1780
1782
|
|
|
@@ -162,6 +162,9 @@ interface WatcherOptions<Result> {
|
|
|
162
162
|
// Fairly self explanatory. If there are nested createWatchers, instead of forking, we just
|
|
163
163
|
// run them immediately.
|
|
164
164
|
inlineNestedWatchers?: boolean;
|
|
165
|
+
|
|
166
|
+
// Temporary indicates after becoming synchronizes it will immediately dispose itself
|
|
167
|
+
temporary?: boolean;
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
let harvestableReadyLoopCount = 0;
|
|
@@ -286,6 +289,8 @@ export type SyncWatcher = {
|
|
|
286
289
|
tag: SyncWatcherTag;
|
|
287
290
|
|
|
288
291
|
hackHistory: { message: string; time: number }[];
|
|
292
|
+
|
|
293
|
+
createTime: number;
|
|
289
294
|
}
|
|
290
295
|
function addToHistory(watcher: SyncWatcher, message: string) {
|
|
291
296
|
watcher.hackHistory.push({ message, time: Date.now() });
|
|
@@ -989,6 +994,7 @@ export class PathValueProxyWatcher {
|
|
|
989
994
|
tag: new SyncWatcherTag(),
|
|
990
995
|
|
|
991
996
|
hackHistory: [],
|
|
997
|
+
createTime: Date.now(),
|
|
992
998
|
};
|
|
993
999
|
const SHOULD_TRACE = this.SHOULD_TRACE(watcher);
|
|
994
1000
|
const proxy = this.proxy;
|
|
@@ -1205,11 +1211,13 @@ export class PathValueProxyWatcher {
|
|
|
1205
1211
|
debugger;
|
|
1206
1212
|
// NOTE: Using forceEqualWrites will also fix this a lot of the time, such as when
|
|
1207
1213
|
// a write contains random numbers or dates.
|
|
1208
|
-
let errorMessage = `Too many attempts to sync with different values. If you are reading in a loop, make sure to read all the values, instead of aborting the loop if a value is not synced. ALSO, make sure you don't access paths with Math.random() or Date.now(). This will prevent the sync loop from ever stabilizing.`;
|
|
1214
|
+
let errorMessage = `Too many attempts (${watcher.countSinceLastFullSync}) to sync with different values. If you are reading in a loop, make sure to read all the values, instead of aborting the loop if a value is not synced. ALSO, make sure you don't access paths with Math.random() or Date.now(). This will prevent the sync loop from ever stabilizing.`;
|
|
1209
1215
|
if (specialPromiseUnsynced) {
|
|
1210
1216
|
errorMessage += ` A promise is being accessed, so it is possible triggerOnPromiseFinish is being used on a new promise every loop, which cannot work (you MUST cache MaybePromise and replace the value with a non-promise, otherwise it will never be available synchronously!)`;
|
|
1211
1217
|
}
|
|
1218
|
+
errorMessage += ` (${watcher.debugName}})`;
|
|
1212
1219
|
console.error(red(errorMessage));
|
|
1220
|
+
logUnsynced();
|
|
1213
1221
|
result = { error: new Error(errorMessage).stack || "" };
|
|
1214
1222
|
// Force the watches to be equal, so we stop looping
|
|
1215
1223
|
watcher.pendingWatches = watcher.lastWatches;
|
|
@@ -1648,6 +1656,7 @@ export class PathValueProxyWatcher {
|
|
|
1648
1656
|
let anyWrites = false;
|
|
1649
1657
|
this.createWatcher({
|
|
1650
1658
|
...options,
|
|
1659
|
+
temporary: true,
|
|
1651
1660
|
onResultUpdated: (result, writes, watcher) => {
|
|
1652
1661
|
if (writes?.length) {
|
|
1653
1662
|
anyWrites = true;
|
|
@@ -1700,6 +1709,7 @@ export class PathValueProxyWatcher {
|
|
|
1700
1709
|
...options,
|
|
1701
1710
|
canWrite: true,
|
|
1702
1711
|
dryRun: true,
|
|
1712
|
+
temporary: true,
|
|
1703
1713
|
onResultUpdated: (result, values, watcher) => {
|
|
1704
1714
|
watcher.dispose();
|
|
1705
1715
|
if ("error" in result) {
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// yarn typenode src\framework-beta\archiveMoveBase\archiveMoveHarness.ts --nonetwork
|
|
4
|
-
|
|
5
1
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
6
2
|
import { blue, green, red } from "socket-function/src/formatting/logColors";
|
|
7
3
|
import { measureBlock, measureWrap } from "socket-function/src/profiling/measure";
|
|
@@ -22,6 +18,7 @@ import debugbreak from "debugbreak";
|
|
|
22
18
|
let lastFileWrites = new Map<string, string[]>();
|
|
23
19
|
|
|
24
20
|
export async function runArchiveMover(config: {
|
|
21
|
+
debugName: string;
|
|
25
22
|
// Defaults to getOurAuthorities
|
|
26
23
|
// NOTE: Runs once per authority, as we want to keep data as sharded as possible
|
|
27
24
|
authorities?: AuthorityPath[];
|
|
@@ -307,10 +304,11 @@ export async function runArchiveMover(config: {
|
|
|
307
304
|
|| lastResult === "aborted" && green("noop")
|
|
308
305
|
|| red("rejected")
|
|
309
306
|
);
|
|
310
|
-
console.log(`ArchiveMover ${config.runMover.name} on ${authorityDir} in ${formatTime(time)}. Result = ${resultFormat}.`);
|
|
307
|
+
console.log(`ArchiveMover ${config.debugName ?? config.runMover.name} on ${authorityDir} in ${formatTime(time)}. Result = ${resultFormat}.`);
|
|
311
308
|
if (lastResult === "accepted" && !abortedDueToInsufficientReduction) {
|
|
312
309
|
console.log(
|
|
313
|
-
`
|
|
310
|
+
`ArchiveMover free committed ${config.debugName ?? config.runMover.name} on ${authorityDir}`
|
|
311
|
+
+ ` { files: ${formatNumber(inputFiles)}, values: ${formatNumber(inputValueCount)}, bytes: ${formatNumber(inputBytes)} }`
|
|
314
312
|
+ "\n=>\n"
|
|
315
313
|
+ ` { files: ${formatNumber(outputFiles)}, values: ${formatNumber(outputValueCount)}, bytes: ${formatNumber(outputBytes)} }`
|
|
316
314
|
,
|
|
@@ -542,6 +542,7 @@ export async function runAliveCheckerIteration(config?: {
|
|
|
542
542
|
|
|
543
543
|
let lockWrites: PathValue[] = [];
|
|
544
544
|
await runArchiveMover({
|
|
545
|
+
debugName: type,
|
|
545
546
|
outputType: type,
|
|
546
547
|
readLiveData: example.unsafeLiveReads,
|
|
547
548
|
force: config?.force,
|
|
@@ -719,7 +720,15 @@ export async function runAliveCheckerIteration(config?: {
|
|
|
719
720
|
}
|
|
720
721
|
|
|
721
722
|
let allFrees = new Set(freeLists.flat().flat().map(x => x.value));
|
|
723
|
+
let allValues = values;
|
|
722
724
|
values = values.filter(x => !allFrees.has(x));
|
|
725
|
+
if (allValues.length !== values.length) {
|
|
726
|
+
console.log("Freeding values due to garbage collection", {
|
|
727
|
+
before: allValues.length,
|
|
728
|
+
after: values.length,
|
|
729
|
+
freed: allValues.length - values.length,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
723
732
|
|
|
724
733
|
// NOTE: We don't notify any servers about this. We will eventually have to write
|
|
725
734
|
// audit code to periodically check against the disk to get updates.
|
package/src/2-proxy/schema2.ts
CHANGED
|
@@ -199,6 +199,10 @@ export class Schema2Fncs {
|
|
|
199
199
|
}
|
|
200
200
|
return { defaultValue: def.defaultValue, };
|
|
201
201
|
}
|
|
202
|
+
// OR, if it is an optional object, then the default IS undefined
|
|
203
|
+
if (def?.isMaybeUndefined) {
|
|
204
|
+
return { defaultValue: undefined };
|
|
205
|
+
}
|
|
202
206
|
return undefined;
|
|
203
207
|
}
|
|
204
208
|
public static isObject(schema: Schema2, path: string[]): boolean {
|
|
@@ -293,6 +297,7 @@ type InternalTypeDef = {
|
|
|
293
297
|
|
|
294
298
|
isObject?: boolean;
|
|
295
299
|
isObjectGC?: boolean;
|
|
300
|
+
isMaybeUndefined?: boolean;
|
|
296
301
|
|
|
297
302
|
isNotAtomic?: boolean;
|
|
298
303
|
|
|
@@ -340,6 +345,9 @@ function typeDefTypeToInternalType(
|
|
|
340
345
|
isObject = true;
|
|
341
346
|
isObjectGC = false;
|
|
342
347
|
}
|
|
348
|
+
if (typeDef.path.includes("optionalObject") || typeDef.path.includes("optionalObjectNoGC")) {
|
|
349
|
+
rootResult.isMaybeUndefined = true;
|
|
350
|
+
}
|
|
343
351
|
|
|
344
352
|
let undoRegions = typeDef.pathWithCalls.filter(x => x.key === "undoRegionObject");
|
|
345
353
|
for (let undoRegion of undoRegions) {
|
|
@@ -370,6 +378,7 @@ function typeDefTypeToInternalType(
|
|
|
370
378
|
nestedValues.isObjectGC = isObjectGC || isLookupGC;
|
|
371
379
|
nestedValues.gcDelay = Number(typeDef.pathWithCalls.find(x => x.key === "gcDelay")?.callParams?.[0]) || undefined;
|
|
372
380
|
|
|
381
|
+
|
|
373
382
|
if (isLookup) {
|
|
374
383
|
let undoRegions = typeDef.pathWithCalls.filter(x => x.key === "undoRegion");
|
|
375
384
|
for (let undoRegion of undoRegions) {
|
|
@@ -550,7 +550,7 @@ export class PathFunctionRunner {
|
|
|
550
550
|
}
|
|
551
551
|
}
|
|
552
552
|
if (runCount > PathFunctionRunner.MAX_WATCH_LOOPS) {
|
|
553
|
-
throw new Error(`MAX_WATCH_LOOPS exceeded for ${getDebugName(callPath, functionSpec, true)}
|
|
553
|
+
throw new Error(`MAX_WATCH_LOOPS exceeded for ${getDebugName(callPath, functionSpec, true)}. All accesses have to be consistent. So Querysub.time() instead of Date.now() and Querysub.nextId() instead of nextId() / Math.random(). If you need multiple random numbers, keep track of an index, and pass it to Querysub.nextId() for the nth random number.`);
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
// We NEED to depend on the function spec, so spec updates can be atomic (which is needed to deploy an update
|
|
@@ -140,14 +140,25 @@ export type PermissionsParameters = {
|
|
|
140
140
|
};
|
|
141
141
|
/** A false of false will deny read permissions, resulting in all reads being given value with a value
|
|
142
142
|
* of undefined, and a time of 0.
|
|
143
|
-
|
|
144
|
-
* be used to verify calls are allowed as well, even if the call doesn't read or write any data
|
|
145
|
-
* that is under a permissions check.
|
|
143
|
+
|
|
146
144
|
*/
|
|
147
145
|
export type PermissionsCallback = (config: PermissionsParameters) => PermissionsCheckResult;
|
|
148
146
|
export type PermissionsCheckResult = boolean | { allowed: boolean; skipParentChecks?: boolean; };
|
|
149
147
|
/*
|
|
150
148
|
NOTE: All ancestor permissions checks are applied as well.
|
|
149
|
+
- This means if the ancestor disallows, it blocks any descendant checks even if they are allowed.
|
|
150
|
+
|
|
151
|
+
NOTE: If no permissions match, then access is disallowed by default.
|
|
152
|
+
|
|
153
|
+
NOTE: Permissions checks at the root (permissions: { READ_PERMISSIONS(config) { } }) will
|
|
154
|
+
be used to verify calls are allowed as well, even if the call doesn't read or write any data
|
|
155
|
+
that is under a permissions check.
|
|
156
|
+
- THIS MEANS that if there is no permission check at the root, calls won't be allowed by any user
|
|
157
|
+
(as we default to disallow).
|
|
158
|
+
|
|
159
|
+
NOTE: Servers watches ignore permissions, as they directly communicate with the underlying PathValueServer,
|
|
160
|
+
skipping the ProxyServer.
|
|
161
|
+
- However function calls by servers are still run by FunctionRunner, which will apply permission checks.
|
|
151
162
|
|
|
152
163
|
IMPORTANT! Wildcards are tested via the "" path. This means if the "" key is provided access to a user, they can run
|
|
153
164
|
Object.keys()/Object.values() on the data. In order to not accidentally provide list access, never allow "" to be
|
|
@@ -7,7 +7,7 @@ import { getArchivesBackblazePublic } from "../-a-archives/archivesBackBlaze";
|
|
|
7
7
|
import { nestArchives } from "../-a-archives/archives";
|
|
8
8
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
9
|
import { runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
10
|
-
import { compare, compareArray, timeInMinute } from "socket-function/src/misc";
|
|
10
|
+
import { compare, compareArray, isNodeTrue, timeInMinute } from "socket-function/src/misc";
|
|
11
11
|
import { cacheLimited, lazy } from "socket-function/src/caching";
|
|
12
12
|
import { canHaveChildren } from "socket-function/src/types";
|
|
13
13
|
import { shutdown } from "../diagnostics/periodic";
|
|
@@ -29,7 +29,7 @@ import { startEdgeNotifier } from "./edgeClientWatcher";
|
|
|
29
29
|
const UPDATE_POLL_INTERVAL = timeInMinute;
|
|
30
30
|
const DEAD_NODE_COUNT_THRESHOLD = 15;
|
|
31
31
|
|
|
32
|
-
const edgeNodeStorage = nestArchives("edgenodes/", getArchivesBackblazePublic(getDomain()));
|
|
32
|
+
const edgeNodeStorage = isNodeTrue() && nestArchives("edgenodes/", getArchivesBackblazePublic(getDomain()));
|
|
33
33
|
const edgeNodeIndexFile = "edge-nodes-index.json";
|
|
34
34
|
|
|
35
35
|
const getEdgeNodeConfig = cacheLimited(10000, async (fileName: string): Promise<EdgeNodeConfig | undefined> => {
|
package/src/4-dom/qreact.tsx
CHANGED
|
@@ -2033,7 +2033,7 @@ function blurFixOnMouseDownHack(event: MouseEvent) {
|
|
|
2033
2033
|
|
|
2034
2034
|
// Looks like we are going to blur, so blur now
|
|
2035
2035
|
if (selected instanceof HTMLElement && !selected.hasAttribute("data-no-early-blur")) {
|
|
2036
|
-
console.info(`Simulating early blur to prevent
|
|
2036
|
+
console.info(`Simulating early blur to prevent blur from firing after mousedown. This solves a problem where mousedown changes the UI, and then the blur fires on the wrong element. You can use data-no-early-blur to opt-out of this feature`, selected);
|
|
2037
2037
|
selected.blur();
|
|
2038
2038
|
}
|
|
2039
2039
|
}
|
|
@@ -32,7 +32,7 @@ import { pathValueCommitter } from "../0-path-value-core/PathValueCommitter";
|
|
|
32
32
|
import debugbreak from "debugbreak";
|
|
33
33
|
import { extractPublicKey, verifyED25519 } from "../-a-auth/ed25519";
|
|
34
34
|
import { registerDynamicResource, registerResource } from "../diagnostics/trackResources";
|
|
35
|
-
import { registerPeriodic, shutdown } from "../diagnostics/periodic";
|
|
35
|
+
import { registerPeriodic, registerPreshutdownHandler, registerShutdownHandler, shutdown } from "../diagnostics/periodic";
|
|
36
36
|
import { sha256 } from "js-sha256";
|
|
37
37
|
import { green, red } from "socket-function/src/formatting/logColors";
|
|
38
38
|
import { minify_sync } from "terser";
|
|
@@ -43,7 +43,6 @@ import { measureBlock, measureFnc } from "socket-function/src/profiling/measure"
|
|
|
43
43
|
import { delay } from "socket-function/src/batching";
|
|
44
44
|
import { MaybePromise } from "socket-function/src/types";
|
|
45
45
|
import { devDebugbreak, getDomain, isDevDebugbreak, isDynamicallyLoading, isNoNetwork, isPublic } from "../config";
|
|
46
|
-
import { hookErrors } from "../diagnostics/errorLogs/hookErrors";
|
|
47
46
|
import { Schema2, Schema2T, t } from "../2-proxy/schema2";
|
|
48
47
|
import { CALL_PERMISSIONS_KEY } from "./permissionsShared";
|
|
49
48
|
import { isDynamicModule } from "../3-path-functions/pathFunctionLoader";
|
|
@@ -115,6 +114,7 @@ export class Querysub {
|
|
|
115
114
|
public static Fragment = qreact.Fragment;
|
|
116
115
|
public static t = t;
|
|
117
116
|
public static shutdown = () => shutdown();
|
|
117
|
+
public static onShutdown = (fnc: () => Promise<void>) => registerPreshutdownHandler(fnc);
|
|
118
118
|
/**
|
|
119
119
|
IMPORTANT! New schemas must be deployed by calling `yarn deploy`
|
|
120
120
|
|
|
@@ -192,7 +192,8 @@ export class Querysub {
|
|
|
192
192
|
public static getCallId = () => Querysub.getCallerMachineId() + "_" + Querysub.getCallTime();
|
|
193
193
|
// Returns a random value between 0 and 1. The value depends on the callId, and index (being identical for
|
|
194
194
|
// the same callid and index).
|
|
195
|
-
public static callRandom = (index
|
|
195
|
+
public static callRandom = (index = 0) => hashRandom(Querysub.getCallId(), index);
|
|
196
|
+
public static nextId = (index = 0) => Querysub.getCallId() + "_" + index;
|
|
196
197
|
|
|
197
198
|
public static configRootDiscoveryLocation = configRootDiscoveryLocation;
|
|
198
199
|
|
|
@@ -275,6 +276,11 @@ export class Querysub {
|
|
|
275
276
|
watchFunction: fnc
|
|
276
277
|
}));
|
|
277
278
|
}
|
|
279
|
+
public static commitDelayed = (fnc: () => unknown) => {
|
|
280
|
+
setImmediate(() => {
|
|
281
|
+
Querysub.serviceWriteDetached(fnc);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
278
284
|
|
|
279
285
|
public static exit = Querysub.gracefulTerminateProcess;
|
|
280
286
|
public static async gracefulTerminateProcess() {
|
|
@@ -286,6 +292,7 @@ export class Querysub {
|
|
|
286
292
|
|
|
287
293
|
public static syncedCommit = Querysub.serviceWrite;
|
|
288
294
|
public static commitSynced = Querysub.serviceWrite;
|
|
295
|
+
public static commitAsync = Querysub.serviceWrite;
|
|
289
296
|
public static async serviceWrite<T>(fnc: () => T) {
|
|
290
297
|
return await proxyWatcher.commitFunction({
|
|
291
298
|
canWrite: true,
|
|
@@ -384,7 +391,8 @@ export class Querysub {
|
|
|
384
391
|
public static getSelfMachineId = getOwnMachineId;
|
|
385
392
|
|
|
386
393
|
public static getOwnNodeId = getOwnNodeId;
|
|
387
|
-
public static getSelfNodeId =
|
|
394
|
+
public static getSelfNodeId = getOwnNodeId;
|
|
395
|
+
public static getOwnThreadId = getOwnNodeId;
|
|
388
396
|
|
|
389
397
|
/** Set ClientWatcher.DEBUG_SOURCES to true for to be populated */
|
|
390
398
|
public static getTriggerReason() {
|
|
@@ -899,7 +907,14 @@ function getSyncedTime() {
|
|
|
899
907
|
if (call) {
|
|
900
908
|
return call.runAtTime.time;
|
|
901
909
|
}
|
|
910
|
+
let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
|
|
911
|
+
if (watcher?.options.temporary) {
|
|
912
|
+
return watcher.createTime;
|
|
913
|
+
}
|
|
902
914
|
if (isNode()) {
|
|
915
|
+
if (watcher) {
|
|
916
|
+
throw new Error(`Trying to access time in a serverside non-temporary watcher. Clientside this is allowed, as infinite loops (render every frame) makes sense. Serverside this is not allowed. Did you try to run a call-type operatin in a watcher? If you manually created a watcher, you might want to set "temporary: true" if you will be immediately disposing it. You almost might want to fork your write logic with setImmediate to detach this from your watcher, so your write can access a single non-changing time.`);
|
|
917
|
+
}
|
|
903
918
|
return Date.now();
|
|
904
919
|
}
|
|
905
920
|
initTimeLoop();
|
|
@@ -953,9 +968,12 @@ setImmediate(() => {
|
|
|
953
968
|
});
|
|
954
969
|
});
|
|
955
970
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
});
|
|
971
|
+
if (isNode()) {
|
|
972
|
+
setImmediate(async () => {
|
|
973
|
+
const { hookErrors } = await import("../diagnostics/errorLogs/hookErrors");
|
|
974
|
+
hookErrors();
|
|
975
|
+
});
|
|
976
|
+
}
|
|
959
977
|
|
|
960
978
|
setImmediate(async () => {
|
|
961
979
|
// Import, so it registers addStatPeriodic
|
|
@@ -32,6 +32,7 @@ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT
|
|
|
32
32
|
// Line and character limits before we cut off the inner content
|
|
33
33
|
lineLimit?: number;
|
|
34
34
|
characterLimit?: number;
|
|
35
|
+
dark?: boolean;
|
|
35
36
|
}> {
|
|
36
37
|
state = {
|
|
37
38
|
limit: this.props.initialLimit || 100,
|
|
@@ -47,7 +48,11 @@ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT
|
|
|
47
48
|
|
|
48
49
|
return (
|
|
49
50
|
<table class={css.borderCollapse("collapse") + this.props.class}>
|
|
50
|
-
<tr class={
|
|
51
|
+
<tr class={
|
|
52
|
+
css.position("sticky").top(0)
|
|
53
|
+
+ (this.props.dark && css.hsla(0, 0, 10, 0.9))
|
|
54
|
+
+ (!this.props.dark && css.hsla(0, 0, 95, 0.9))
|
|
55
|
+
}>
|
|
51
56
|
<th class={css.whiteSpace("nowrap")}>⧉ {allRows.length}</th>
|
|
52
57
|
{Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]) =>
|
|
53
58
|
<th class={css.pad2(8, 4) + cellClass}>{column?.title || toSpaceCase(columnName)}</th>
|
|
@@ -19,7 +19,6 @@ import { atomic, doAtomicWrites } from "../../2-proxy/PathValueProxyWatcher";
|
|
|
19
19
|
import { isClient } from "../../config2";
|
|
20
20
|
import { ControllerPick, SocketRegistered } from "socket-function/SocketFunctionTypes";
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
const NOTIFY_HISTORY = timeInDay * 7;
|
|
24
23
|
// Only error / fatal are tracked, for now... we might also add warns?
|
|
25
24
|
const logIssueNotifyTypes: LogType[] = ["error", "fatal"];
|
|
@@ -14,7 +14,6 @@ import { ButtonSelector } from "../../library-components/ButtonSelector";
|
|
|
14
14
|
import { Icon } from "../../library-components/icons";
|
|
15
15
|
import { MeasuredDiv } from "../../library-components/MeasuredDiv";
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
type RangeSize = "lastYear" | "lastMonth" | "lastWeek" | "lastDay" | "lastHour";
|
|
19
18
|
export let logNowTime = Date.now();
|
|
20
19
|
const logFilterStart = new URLParam<number | RangeSize>("logFilterStartTime", "lastWeek");
|
|
@@ -18,7 +18,6 @@ import { getNextTime } from "../0-path-value-core/pathValueCore";
|
|
|
18
18
|
import { encodeArgs } from "../3-path-functions/PathFunctionHelpers";
|
|
19
19
|
import { CallSpec, getCurrentCall, getCurrentCallAllowUndefined, overrideCurrentCall } from "../3-path-functions/PathFunctionRunner";
|
|
20
20
|
import { createLazyComponent } from "../library-components/LazyComponent";
|
|
21
|
-
import { LogNotify } from "./errorLogs/LogNotify";
|
|
22
21
|
import { isTrusted } from "../-d-trust/NetworkTrust2";
|
|
23
22
|
import { devDebugbreak, getDomain } from "../config";
|
|
24
23
|
import { getCallWrites } from "../4-querysub/querysubPrediction";
|
|
@@ -27,7 +26,9 @@ import * as hooks from "../-0-hooks/hooks";
|
|
|
27
26
|
import { addComponentUI } from "../5-diagnostics/qreactDebug";
|
|
28
27
|
import { addComponentButton } from "../5-diagnostics/qreactDebug";
|
|
29
28
|
import { closeAllModals } from "../5-diagnostics/Modal";
|
|
29
|
+
import { delay } from "socket-function/src/batching";
|
|
30
30
|
|
|
31
|
+
const LogNotify = createLazyComponent(() => import("./errorLogs/LogNotify"))("LogNotify");
|
|
31
32
|
|
|
32
33
|
export const managementPageURL = new URLParam("managementpage", "");
|
|
33
34
|
export const showingManagementURL = new URLParam("showingmanagement", false);
|
|
@@ -134,14 +135,21 @@ export async function registerManagementPages2(config: {
|
|
|
134
135
|
__schema = config.schema;
|
|
135
136
|
|
|
136
137
|
setImmediate(async () => {
|
|
137
|
-
if (!isNode() &&
|
|
138
|
+
if (!isNode() &&
|
|
139
|
+
!await ManagementController.nodes[getBrowserUrlNode()].isManagementUser()
|
|
140
|
+
&& new URLSearchParams(window.location.search).get("dropto") !== "user"
|
|
141
|
+
) return;
|
|
142
|
+
|
|
143
|
+
// Wait, so the import system knows the modules are async imports
|
|
144
|
+
await delay(0);
|
|
145
|
+
|
|
146
|
+
const ComponentSyncStats = createLazyComponent(() => import("./misc-pages/ComponentSyncStats"))("ComponentSyncStats");
|
|
138
147
|
|
|
139
|
-
const { filtersURL } = await import("./misc-pages/archiveViewerShared");
|
|
140
|
-
const { watchValueType } = await import("./misc-pages/LocalWatchViewer");
|
|
141
|
-
const { ComponentSyncStats } = await import("./misc-pages/ComponentSyncStats");
|
|
142
148
|
addComponentButton({
|
|
143
149
|
title: "View in Path Navigator",
|
|
144
|
-
callback(component) {
|
|
150
|
+
async callback(component) {
|
|
151
|
+
const { filtersURL } = await import("./misc-pages/archiveViewerShared");
|
|
152
|
+
const { watchValueType } = await import("./misc-pages/LocalWatchViewer");
|
|
145
153
|
filtersURL.value[nextId()] = { column: "path", match: component.debugName, type: "and" };
|
|
146
154
|
managementPageURL.value = "LocalWatchViewer";
|
|
147
155
|
watchValueType.value = "component";
|
|
@@ -168,6 +176,8 @@ export async function registerManagementPages2(config: {
|
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
if (isServer()) {
|
|
179
|
+
// Wait, so the import system knows the modules are async imports
|
|
180
|
+
await delay(0);
|
|
171
181
|
for (let page of inputPages) {
|
|
172
182
|
// NOTE: If we split this into a module for component/controller, we need to make sure we
|
|
173
183
|
// import both serverside, so we can whitelist them for import clientside.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
1
|
+
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
2
2
|
import { isNode, timeInMinute } from "socket-function/src/misc";
|
|
3
|
-
import { logErrors } from "../errors";
|
|
3
|
+
import { logErrors, timeoutToError } from "../errors";
|
|
4
4
|
import debugbreak from "debugbreak";
|
|
5
5
|
import { nodeDiscoveryShutdown } from "../-f-node-discovery/NodeDiscovery";
|
|
6
6
|
import { authorityStorage } from "../0-path-value-core/pathValueCore";
|
|
@@ -15,6 +15,10 @@ let shutdownHandlers: (() => Promise<void>)[] = [];
|
|
|
15
15
|
export function registerShutdownHandler(fnc: () => Promise<void>) {
|
|
16
16
|
shutdownHandlers.push(fnc);
|
|
17
17
|
}
|
|
18
|
+
let preshutdownHandlers: (() => Promise<void>)[] = [];
|
|
19
|
+
export function registerPreshutdownHandler(fnc: () => Promise<void>) {
|
|
20
|
+
preshutdownHandlers.push(fnc);
|
|
21
|
+
}
|
|
18
22
|
|
|
19
23
|
function logAll() {
|
|
20
24
|
for (let fnc of periodicFncs) {
|
|
@@ -24,8 +28,20 @@ function logAll() {
|
|
|
24
28
|
|
|
25
29
|
logErrors(runInfinitePollCallAtStart(timeInMinute * 5, logAll));
|
|
26
30
|
|
|
31
|
+
let shuttingDown = false;
|
|
27
32
|
export async function shutdown() {
|
|
33
|
+
if (shuttingDown) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
shuttingDown = true;
|
|
28
37
|
const { authorityStorage } = await import("../0-path-value-core/pathValueCore");
|
|
38
|
+
for (let fnc of preshutdownHandlers) {
|
|
39
|
+
try {
|
|
40
|
+
await timeoutToError(timeInMinute, fnc(), () => new Error(`Preshutdown handler ${fnc.name} timed out`));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(`Error on preshutdown handler ${fnc.name}`, e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
29
45
|
try {
|
|
30
46
|
await authorityStorage.onShutdown();
|
|
31
47
|
await nodeDiscoveryShutdown();
|
|
@@ -34,11 +50,13 @@ export async function shutdown() {
|
|
|
34
50
|
}
|
|
35
51
|
for (let fnc of shutdownHandlers) {
|
|
36
52
|
try {
|
|
37
|
-
await fnc();
|
|
53
|
+
await timeoutToError(timeInMinute, fnc(), () => new Error(`Shutdown handler ${fnc.name} timed out`));
|
|
38
54
|
} catch (e) {
|
|
39
55
|
console.log(`Error on shutdown handler ${fnc.name}`, e);
|
|
40
56
|
}
|
|
41
57
|
}
|
|
58
|
+
// Wait to allow any logged errors to hopefully be written somewhere?
|
|
59
|
+
await delay(2000);
|
|
42
60
|
process.exit();
|
|
43
61
|
}
|
|
44
62
|
|
package/src/server.ts
CHANGED
|
@@ -14,7 +14,6 @@ import { AuthorityPath, pathValueAuthority2 } from "./0-path-value-core/NodePath
|
|
|
14
14
|
import { Querysub } from "./4-querysub/QuerysubController";
|
|
15
15
|
import { ClientWatcher } from "./1-path-client/pathValueClientWatcher";
|
|
16
16
|
import { delay } from "socket-function/src/batching";
|
|
17
|
-
import yargs from "yargs";
|
|
18
17
|
import { JSONLACKS } from "socket-function/src/JSONLACKS/JSONLACKS";
|
|
19
18
|
import { green } from "socket-function/src/formatting/logColors";
|
|
20
19
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
@@ -26,6 +25,7 @@ import { getOwnThreadId } from "./-f-node-discovery/NodeDiscovery";
|
|
|
26
25
|
import { getOurAuthorities } from "./config2";
|
|
27
26
|
import { devDebugbreak, isPublic } from "./config";
|
|
28
27
|
|
|
28
|
+
import yargs from "yargs";
|
|
29
29
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
30
30
|
.option("authority", { type: "string", desc: `Defines the base paths we are an authority on (the domain is prepended to them). Either a file path to a JSON(AuthorityPath[]), or a base64 representation of the JSON(AuthorityPath[]).` })
|
|
31
31
|
.option("verbose", { type: "boolean", desc: "Log all writes and reads" })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import preact from "preact"; import { qreact } from "../4-dom/qreact";
|
|
2
2
|
import { Button } from "../library-components/Button";
|
|
3
3
|
import { css } from "../4-dom/css";
|
|
4
|
-
import { loginTokenURL, user_data, user_functions } from "./userData";
|
|
4
|
+
import { getUserId, getUserIdAllowUndefined, getUserObj, loginTokenURL, user_data, user_functions } from "./userData";
|
|
5
5
|
import { Querysub } from "../4-querysub/Querysub";
|
|
6
6
|
import { isNode, list } from "socket-function/src/misc";
|
|
7
7
|
import { InputLabel } from "../library-components/InputLabel";
|
|
@@ -16,7 +16,7 @@ export class LoginPage extends qreact.Component {
|
|
|
16
16
|
render() {
|
|
17
17
|
const login = () => {
|
|
18
18
|
user_functions.sendLoginEmail({
|
|
19
|
-
|
|
19
|
+
email: this.state.email,
|
|
20
20
|
redirectURL: window.location.href,
|
|
21
21
|
loginTokenRandomness: Buffer.from(list(32).map(() => Math.random() * 256)).toString("base64"),
|
|
22
22
|
});
|
|
@@ -54,6 +54,22 @@ export class LoginPage extends qreact.Component {
|
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
56
|
}
|
|
57
|
+
{getUserIdAllowUndefined() &&
|
|
58
|
+
<div class={css.hbox(10).alignSelf("center").maxWidth("50vw")}>
|
|
59
|
+
<div class={css.fontSize(20).hslcolor(110, 75, 75)}>
|
|
60
|
+
You are logged in as {getUserObj().email}.
|
|
61
|
+
</div>
|
|
62
|
+
<button onClick={() => {
|
|
63
|
+
let call = user_functions.logoutCurrent();
|
|
64
|
+
void Querysub.onCommitPredictFinished(call).finally(async () => {
|
|
65
|
+
await Querysub.gracefulTerminateProcess();
|
|
66
|
+
location.reload();
|
|
67
|
+
});
|
|
68
|
+
}}>
|
|
69
|
+
Logout
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
}
|
|
57
73
|
</div>
|
|
58
74
|
);
|
|
59
75
|
}
|
|
@@ -268,7 +268,7 @@ export class UserPage extends qreact.Component {
|
|
|
268
268
|
</table>
|
|
269
269
|
</div>
|
|
270
270
|
<div>
|
|
271
|
-
<div class={css.fontSize(20)}>Latest
|
|
271
|
+
<div class={css.fontSize(20)}>Latest Access by Machine/IP</div>
|
|
272
272
|
<table>
|
|
273
273
|
<tr>
|
|
274
274
|
<th>Source</th>
|
|
@@ -293,7 +293,7 @@ export class UserPage extends qreact.Component {
|
|
|
293
293
|
<h2>Example Login Email</h2>
|
|
294
294
|
<div key="email" ref2={e => {
|
|
295
295
|
e.innerHTML = renderToString(generateLoginEmail({
|
|
296
|
-
|
|
296
|
+
email: "user@example.com",
|
|
297
297
|
loginToken: "loginToken",
|
|
298
298
|
ip: "127.0.0.1",
|
|
299
299
|
machineId: getOwnMachineId(),
|
|
@@ -14,12 +14,11 @@ async function main() {
|
|
|
14
14
|
const email = yargObj.email;
|
|
15
15
|
if (!email) throw new Error("No email provided." + howToCall);
|
|
16
16
|
if (!email.includes("@")) throw new Error(`Invalid email ${email}.` + howToCall);
|
|
17
|
-
const userId = email;
|
|
18
17
|
|
|
19
18
|
await Querysub.hostService("addSuperUser");
|
|
20
19
|
|
|
21
20
|
await Querysub.commitSynced(() => {
|
|
22
|
-
scriptCreateUser({
|
|
21
|
+
scriptCreateUser({ email });
|
|
23
22
|
});
|
|
24
23
|
|
|
25
24
|
await pathValueCommitter.waitForValuesToCommit();
|
|
@@ -4,7 +4,7 @@ import { list } from "socket-function/src/misc";
|
|
|
4
4
|
import { hslToRGB } from "socket-function/src/formatting/colors";
|
|
5
5
|
import { joinVNodes } from "../misc";
|
|
6
6
|
export function generateLoginEmail(config: {
|
|
7
|
-
|
|
7
|
+
email: string;
|
|
8
8
|
loginToken: string;
|
|
9
9
|
ip: string;
|
|
10
10
|
machineId: string;
|
|
@@ -12,10 +12,10 @@ export function generateLoginEmail(config: {
|
|
|
12
12
|
timeoutTime: number;
|
|
13
13
|
noHTMLWrapper?: boolean;
|
|
14
14
|
}): { subject: string; contents: preact.VNode } {
|
|
15
|
-
const {
|
|
15
|
+
const { loginToken, ip, machineId, redirectURL, email } = config;
|
|
16
16
|
let url = new URL(redirectURL);
|
|
17
17
|
url.searchParams.set(loginTokenURL.urlKey, loginToken);
|
|
18
|
-
const subject = `${
|
|
18
|
+
const subject = `${email} Access Request (${new Date().toLocaleString()})`;
|
|
19
19
|
function center(jsx: preact.ComponentChild) {
|
|
20
20
|
return (
|
|
21
21
|
<table style={{ width: "100%" }}>
|
|
@@ -79,7 +79,7 @@ export function generateLoginEmail(config: {
|
|
|
79
79
|
</table>
|
|
80
80
|
{line(<div style={{ color: "rgb(255, 255, 255)" }}>
|
|
81
81
|
{/* Mess up the email so it isn't turned into a link by gmail. */}
|
|
82
|
-
Greetings <b>{
|
|
82
|
+
Greetings <b>{email.replaceAll(".", ".")}</b>. You have requested access to <a href={redirectURL} style={{ color: "rgb(153, 204, 255)" }}>{new URL(redirectURL).hostname}</a>.
|
|
83
83
|
</div>)}
|
|
84
84
|
{line(<div style={{ color: "rgb(255, 255, 255)" }}>
|
|
85
85
|
If you did not make this request, please disregard this email. Your account is safe and no action is required.
|
|
@@ -114,11 +114,11 @@ export function generateLoginEmail(config: {
|
|
|
114
114
|
<td {...{ "nowrap": true }} style={{ paddingLeft: "40px", }}></td>
|
|
115
115
|
</tr></tbody></table>
|
|
116
116
|
<table><tbody><tr><td style={{ paddingTop: "20px" }}></td></tr></tbody></table>
|
|
117
|
-
{line(<div style={{ color: "rgb(140, 140, 140)", width: "100%" }}><b>Do NOT share or forward this email
|
|
117
|
+
{line(<div style={{ color: "rgb(140, 140, 140)", width: "100%" }}><b>Do NOT share or forward this email to anyone.</b></div>)}
|
|
118
118
|
<table><tbody><tr><td style={{ paddingTop: "10px" }}></td></tr></tbody></table>
|
|
119
119
|
{line(<div style={{ color: "rgb(140, 140, 140)", width: "100%" }}>
|
|
120
|
-
This link will only work
|
|
121
|
-
This link can only be used one time,
|
|
120
|
+
This link will only work in the current device and browser.
|
|
121
|
+
This link can only be used one time, and will expire at {new Date(config.timeoutTime).toLocaleString()}.
|
|
122
122
|
</div>)}
|
|
123
123
|
</>
|
|
124
124
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { atomic, atomicObjectWrite } from "../2-proxy/PathValueProxyWatcher";
|
|
2
2
|
import { Querysub } from "../4-querysub/QuerysubController";
|
|
3
3
|
import { red } from "socket-function/src/formatting/logColors";
|
|
4
|
-
import { isNode, timeInHour } from "socket-function/src/misc";
|
|
4
|
+
import { isNode, sha256Hash, timeInHour } from "socket-function/src/misc";
|
|
5
5
|
import { registerAliveChecker } from "../2-proxy/garbageCollection";
|
|
6
6
|
import { generateLoginEmail } from "./loginEmail";
|
|
7
7
|
import { logErrors } from "../errors";
|
|
@@ -15,6 +15,7 @@ import { devDebugbreak, getDomain, isDevDebugbreak } from "../config";
|
|
|
15
15
|
import { delay } from "socket-function/src/batching";
|
|
16
16
|
import { enableErrorNotifications } from "../library-components/errorNotifications";
|
|
17
17
|
import { clamp } from "../misc";
|
|
18
|
+
import { sha256 } from "js-sha256";
|
|
18
19
|
|
|
19
20
|
/*
|
|
20
21
|
IMPORTANT!
|
|
@@ -186,6 +187,10 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
186
187
|
claimedDisplayNames: {
|
|
187
188
|
[displayName: string]: string;
|
|
188
189
|
};
|
|
190
|
+
|
|
191
|
+
emailToUserId: {
|
|
192
|
+
[email: string]: string;
|
|
193
|
+
};
|
|
189
194
|
};
|
|
190
195
|
machineSecure: {
|
|
191
196
|
[machineId: string]: {
|
|
@@ -332,6 +337,7 @@ export function wildcard0Owner(config: PermissionsParameters) {
|
|
|
332
337
|
}
|
|
333
338
|
|
|
334
339
|
function isAllowedToBeUser(userId: string) {
|
|
340
|
+
userId = String(userId);
|
|
335
341
|
if (isClient()) {
|
|
336
342
|
return data().users[userId].userId === userId;
|
|
337
343
|
}
|
|
@@ -532,12 +538,20 @@ function internalCreateUser(config: {
|
|
|
532
538
|
return data().users[userId];
|
|
533
539
|
}
|
|
534
540
|
|
|
541
|
+
function createNewUserId() {
|
|
542
|
+
let randBuffer = Buffer.from(sha256(Querysub.nextId()), "hex");
|
|
543
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
544
|
+
return Array.from(randBuffer)
|
|
545
|
+
.map(x => alphabet[x % alphabet.length])
|
|
546
|
+
.join("")
|
|
547
|
+
.slice(0, 16);
|
|
548
|
+
}
|
|
535
549
|
function sendLoginEmail(config: {
|
|
536
|
-
|
|
550
|
+
email: string;
|
|
537
551
|
redirectURL: string;
|
|
538
552
|
loginTokenRandomness: string;
|
|
539
553
|
}) {
|
|
540
|
-
const {
|
|
554
|
+
const { email, redirectURL } = config;
|
|
541
555
|
{
|
|
542
556
|
let redirectObj = new URL(redirectURL);
|
|
543
557
|
redirectObj.port = "443";
|
|
@@ -547,19 +561,24 @@ function sendLoginEmail(config: {
|
|
|
547
561
|
}
|
|
548
562
|
}
|
|
549
563
|
Querysub.ignorePermissionsChecks(() => {
|
|
550
|
-
if (!
|
|
551
|
-
throw new Error(`Invalid email ${
|
|
564
|
+
if (!email.includes("@")) {
|
|
565
|
+
throw new Error(`Invalid email ${email}`);
|
|
552
566
|
}
|
|
553
|
-
|
|
567
|
+
let userId = atomic(data().secure.emailToUserId[email]) || createNewUserId();
|
|
554
568
|
|
|
555
569
|
const signupError = `Signups are currently closed, and user doesn't presently exist. ${userId}. Add users with "yarn addsuperuser <email>"`;
|
|
556
570
|
// NOTE: Using the same errors message makes it a BIT harder to know if you are blocked, so attackers are
|
|
557
571
|
// less likely to change their IP, and more likely to give up.
|
|
558
572
|
throwIfBlockedIP(signupError);
|
|
559
|
-
if (!atomic(data().secure.signupsOpen) && !(userId in data().users)) {
|
|
573
|
+
if (!atomic(data().secure.signupsOpen) && (!userId || !(userId in data().users))) {
|
|
560
574
|
throw new Error(signupError);
|
|
561
575
|
}
|
|
562
576
|
|
|
577
|
+
// Create user if it doesn't exist
|
|
578
|
+
if (!(userId in data().users)) {
|
|
579
|
+
internalCreateUser({ userId, email });
|
|
580
|
+
}
|
|
581
|
+
|
|
563
582
|
const ip = Querysub.getCallerIP();
|
|
564
583
|
const machineId = Querysub.getCallerMachineId();
|
|
565
584
|
const now = Querysub.getCallTime();
|
|
@@ -605,13 +624,6 @@ function sendLoginEmail(config: {
|
|
|
605
624
|
}
|
|
606
625
|
}
|
|
607
626
|
|
|
608
|
-
// Create user if it doesn't exist
|
|
609
|
-
{
|
|
610
|
-
if (!(userId in data().users)) {
|
|
611
|
-
internalCreateUser({ userId, email });
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
627
|
let userObj = data().users[userId];
|
|
616
628
|
|
|
617
629
|
// Per email/user throttle
|
|
@@ -644,7 +656,7 @@ function sendLoginEmail(config: {
|
|
|
644
656
|
// NOTE: We don't track failed calls. Those should be tracked at a framework level and displayed on another page.
|
|
645
657
|
logActivity({ type: "sendLoginEmail", fields: { userId, ip, machineId, redirectURL } });
|
|
646
658
|
const { subject, contents } = generateLoginEmail({
|
|
647
|
-
|
|
659
|
+
email, loginToken, ip, machineId, redirectURL,
|
|
648
660
|
timeoutTime,
|
|
649
661
|
});
|
|
650
662
|
Querysub.onCommitFinished(async () => {
|
|
@@ -756,35 +768,37 @@ function registerPageLoadTime() {
|
|
|
756
768
|
}
|
|
757
769
|
|
|
758
770
|
function inviteUser(config: { email: string }) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
+
Querysub.ignorePermissionsChecks(() => {
|
|
772
|
+
let curUserObj = getUserObjAssert();
|
|
773
|
+
if (config.email in curUserObj.invitedUsers) {
|
|
774
|
+
console.info(`User ${config.email} already invited`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
// If the user already exists, don't invite
|
|
778
|
+
const { email } = config;
|
|
779
|
+
let userId = atomic(data().secure.emailToUserId[email]) || createNewUserId();
|
|
780
|
+
if (userId in data().users) {
|
|
781
|
+
console.info(`User ${userId} already exists, no need to invite`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
771
784
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
785
|
+
const invitesRemaining = Number(curUserObj.invitesRemaining);
|
|
786
|
+
if (invitesRemaining <= 0) {
|
|
787
|
+
throw new Error("No invites remaining");
|
|
788
|
+
}
|
|
776
789
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
790
|
+
curUserObj.invitedUsers[config.email] = atomicObjectWrite({
|
|
791
|
+
time: Querysub.getCallTime(),
|
|
792
|
+
});
|
|
793
|
+
curUserObj.invitesRemaining--;
|
|
781
794
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
795
|
+
if (!(userId in data().users)) {
|
|
796
|
+
// Create the user if they don't exist
|
|
797
|
+
let newUserObj = internalCreateUser({ userId, email });
|
|
798
|
+
newUserObj.invitedBy = curUserObj.userId;
|
|
799
|
+
}
|
|
800
|
+
logActivity({ type: "inviteUser", fields: { inviter: curUserObj.userId, userId, } });
|
|
801
|
+
});
|
|
788
802
|
}
|
|
789
803
|
|
|
790
804
|
function logoutFromMachine(config: { machineId: string; userId: string }) {
|
|
@@ -888,11 +902,11 @@ function specialSetUserType(config: { userId: string; userType: UserType; }) {
|
|
|
888
902
|
|
|
889
903
|
|
|
890
904
|
export function scriptCreateUser(config: {
|
|
891
|
-
|
|
905
|
+
email: string;
|
|
892
906
|
}) {
|
|
893
|
-
const {
|
|
894
|
-
let
|
|
895
|
-
data().users[
|
|
907
|
+
const { email } = config;
|
|
908
|
+
let userId = createNewUserId();
|
|
909
|
+
data().users[userId] = {
|
|
896
910
|
userId,
|
|
897
911
|
email,
|
|
898
912
|
settings: { displayName: userId },
|
|
@@ -905,6 +919,7 @@ export function scriptCreateUser(config: {
|
|
|
905
919
|
invitedUsers: {},
|
|
906
920
|
invitesRemaining: 0,
|
|
907
921
|
};
|
|
922
|
+
data().secure.emailToUserId[email] = userId;
|
|
908
923
|
}
|
|
909
924
|
|
|
910
925
|
function setPostmarkAPIKey(config: { apiKey: string; }) {
|