querysub 0.157.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 +4 -3
- package/src/-b-authorities/dnsAuthority.ts +0 -2
- 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/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-dom/qreact.tsx +1 -1
- package/src/4-querysub/Querysub.ts +19 -3
- package/src/5-diagnostics/Table.tsx +6 -1
- 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 +4 -4
- 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",
|
|
@@ -4,15 +4,16 @@ 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
|
-
|
|
7
|
+
|
|
8
|
+
export const keys = lazy(() => getArchives("keys"));
|
|
8
9
|
|
|
9
10
|
export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: string }> => {
|
|
10
|
-
let credsJSON = await keys.get("cloudflare.json");
|
|
11
|
+
let credsJSON = await keys().get("cloudflare.json");
|
|
11
12
|
if (!credsJSON) {
|
|
12
13
|
let localPath = getStorageDir() + "cloudflare.json";
|
|
13
14
|
if (fs.existsSync(localPath)) {
|
|
14
15
|
credsJSON = fs.readFileSync(localPath);
|
|
15
|
-
await keys.set("cloudflare.json", credsJSON);
|
|
16
|
+
await keys().set("cloudflare.json", credsJSON);
|
|
16
17
|
} else {
|
|
17
18
|
throw new Error(`b2:/keys/cloudflare.json is missing. It should contain { "key": "your-key", "email": "your-email" }`);
|
|
18
19
|
}
|
|
@@ -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) {
|
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
|
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";
|
|
@@ -114,6 +114,7 @@ export class Querysub {
|
|
|
114
114
|
public static Fragment = qreact.Fragment;
|
|
115
115
|
public static t = t;
|
|
116
116
|
public static shutdown = () => shutdown();
|
|
117
|
+
public static onShutdown = (fnc: () => Promise<void>) => registerPreshutdownHandler(fnc);
|
|
117
118
|
/**
|
|
118
119
|
IMPORTANT! New schemas must be deployed by calling `yarn deploy`
|
|
119
120
|
|
|
@@ -191,7 +192,8 @@ export class Querysub {
|
|
|
191
192
|
public static getCallId = () => Querysub.getCallerMachineId() + "_" + Querysub.getCallTime();
|
|
192
193
|
// Returns a random value between 0 and 1. The value depends on the callId, and index (being identical for
|
|
193
194
|
// the same callid and index).
|
|
194
|
-
public static callRandom = (index
|
|
195
|
+
public static callRandom = (index = 0) => hashRandom(Querysub.getCallId(), index);
|
|
196
|
+
public static nextId = (index = 0) => Querysub.getCallId() + "_" + index;
|
|
195
197
|
|
|
196
198
|
public static configRootDiscoveryLocation = configRootDiscoveryLocation;
|
|
197
199
|
|
|
@@ -274,6 +276,11 @@ export class Querysub {
|
|
|
274
276
|
watchFunction: fnc
|
|
275
277
|
}));
|
|
276
278
|
}
|
|
279
|
+
public static commitDelayed = (fnc: () => unknown) => {
|
|
280
|
+
setImmediate(() => {
|
|
281
|
+
Querysub.serviceWriteDetached(fnc);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
277
284
|
|
|
278
285
|
public static exit = Querysub.gracefulTerminateProcess;
|
|
279
286
|
public static async gracefulTerminateProcess() {
|
|
@@ -285,6 +292,7 @@ export class Querysub {
|
|
|
285
292
|
|
|
286
293
|
public static syncedCommit = Querysub.serviceWrite;
|
|
287
294
|
public static commitSynced = Querysub.serviceWrite;
|
|
295
|
+
public static commitAsync = Querysub.serviceWrite;
|
|
288
296
|
public static async serviceWrite<T>(fnc: () => T) {
|
|
289
297
|
return await proxyWatcher.commitFunction({
|
|
290
298
|
canWrite: true,
|
|
@@ -383,7 +391,8 @@ export class Querysub {
|
|
|
383
391
|
public static getSelfMachineId = getOwnMachineId;
|
|
384
392
|
|
|
385
393
|
public static getOwnNodeId = getOwnNodeId;
|
|
386
|
-
public static getSelfNodeId =
|
|
394
|
+
public static getSelfNodeId = getOwnNodeId;
|
|
395
|
+
public static getOwnThreadId = getOwnNodeId;
|
|
387
396
|
|
|
388
397
|
/** Set ClientWatcher.DEBUG_SOURCES to true for to be populated */
|
|
389
398
|
public static getTriggerReason() {
|
|
@@ -898,7 +907,14 @@ function getSyncedTime() {
|
|
|
898
907
|
if (call) {
|
|
899
908
|
return call.runAtTime.time;
|
|
900
909
|
}
|
|
910
|
+
let watcher = proxyWatcher.getTriggeredWatcherMaybeUndefined();
|
|
911
|
+
if (watcher?.options.temporary) {
|
|
912
|
+
return watcher.createTime;
|
|
913
|
+
}
|
|
901
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
|
+
}
|
|
902
918
|
return Date.now();
|
|
903
919
|
}
|
|
904
920
|
initTimeLoop();
|
|
@@ -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>
|
|
@@ -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.
|
|
@@ -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; }) {
|