querysub 0.470.0 → 0.472.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/-c-identity/IdentityController.ts +6 -1
- package/src/2-proxy/PathValueProxyWatcher.ts +1 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +4 -0
- package/src/3-path-functions/syncSchema.ts +2 -2
- package/src/4-dom/qreactTest.tsx +8 -1
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +7 -3
- package/src/diagnostics/logs/diskLogGlobalContext.ts +7 -0
- package/src/library-components/InputAutocomplete.tsx +100 -0
- package/src/library-components/URLParam.ts +1 -0
- package/src/library-components/uncaughtToast.tsx +2 -2
- package/src/user-implementation/userData.ts +119 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.472.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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
67
67
|
"pako": "^2.1.0",
|
|
68
68
|
"peggy": "^5.0.6",
|
|
69
|
-
"socket-function": "^1.1.
|
|
69
|
+
"socket-function": "^1.1.35",
|
|
70
70
|
"terser": "^5.31.0",
|
|
71
71
|
"typesafecss": "^0.29.0",
|
|
72
72
|
"yaml": "^2.5.0",
|
|
@@ -54,7 +54,12 @@ export function IdentityController_getReconnectNodeIdAssert(callerContext: Calle
|
|
|
54
54
|
return callerContext.nodeId;
|
|
55
55
|
}
|
|
56
56
|
let reconnectId = IdentityController_getReconnectNodeId(callerContext);
|
|
57
|
-
if (!reconnectId)
|
|
57
|
+
if (!reconnectId) {
|
|
58
|
+
console.error(`Caller did not mount before connecting. This call requires the caller to be listening.`, {
|
|
59
|
+
callerNodeId: callerContext.nodeId,
|
|
60
|
+
});
|
|
61
|
+
throw new Error(`Caller did not mount before connecting. This call requires the caller to be listening.`);
|
|
62
|
+
}
|
|
58
63
|
return reconnectId;
|
|
59
64
|
}
|
|
60
65
|
export function IdentityController_getSecureIP(callerContext: CallerContext): string {
|
|
@@ -1664,7 +1664,7 @@ export class PathValueProxyWatcher {
|
|
|
1664
1664
|
|
|
1665
1665
|
let maxLocks = watcher.options.maxLocksOverride || DEFAULT_MAX_LOCKS;
|
|
1666
1666
|
if (locks.length > maxLocks) {
|
|
1667
|
-
throw new Error(`Too many locks for ${watcher.debugName} (${locks.length} > ${maxLocks}). Use Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. You can override max locks with maxLocksOverride (in options / functionMetadata). Some locks are ${JSON.stringify(locks.slice(
|
|
1667
|
+
throw new Error(`Too many locks for ${watcher.debugName} (${locks.length} > ${maxLocks}). Use Querysub.noLocks(() => ...) around code that is accessing too many values, assuming you don't want to lock them. You can override max locks with maxLocksOverride (in options / functionMetadata). Some locks are ${JSON.stringify(locks.slice(-10).map(x => ({ path: x.path, startTime: x.startTime, endTime: x.endTime })))}.`);
|
|
1668
1668
|
}
|
|
1669
1669
|
|
|
1670
1670
|
|
|
@@ -144,6 +144,10 @@ export function writeFunctionCall(config: {
|
|
|
144
144
|
}) {
|
|
145
145
|
let { domainName, moduleId, functionId, args, metadata } = config;
|
|
146
146
|
|
|
147
|
+
if (isNode() && !curInterceptor) {
|
|
148
|
+
throw new Error(`Naked function writes are not allowed serverside. Wrap your code in a Querysub.commitAsync!`);
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
// NOTE: Having this be an always-increasing time means that at least if it's the same client making the call ID, the numbers will always be increasing. So we can go from the call ID to determine the order of the calls, which is extremely useful.
|
|
148
152
|
let now = getTimeUnique();
|
|
149
153
|
// IMPORTANT! CallId MUST be secure, otherwise other users can guess it, and read our calls
|
|
@@ -58,7 +58,7 @@ export type FunctionMetadata<Schema = unknown, F = unknown> = {
|
|
|
58
58
|
// NOTE: Eventually we might want to support dynamic paths per call, so we can supported nested lookups.
|
|
59
59
|
// - We also need to support specifying on the lookup that the key is in the nested lookup, which means hashing has to support wildcards, etc. All-in-all the whole change is very complicated, and only needed for hypothetical cases (data().worlds[region][id]...), which could probably be flattened anyways to just a single lookup (data().worlds[region + "-" + id]...)
|
|
60
60
|
/** This should identify the most common lookup accessed, and then give the path to that lookup and the key that we're using to access it. */
|
|
61
|
-
keyOverride
|
|
61
|
+
keyOverride: {
|
|
62
62
|
getPrefix: (data: () => Schema) => unknown;
|
|
63
63
|
getKey: (...args: Args<F>) => string;
|
|
64
64
|
};
|
|
@@ -275,7 +275,7 @@ type SyncSchemaResult<Schema> = {
|
|
|
275
275
|
moduleId: string;
|
|
276
276
|
module: NodeJS.Module;
|
|
277
277
|
functions: Functions;
|
|
278
|
-
functionMetadata
|
|
278
|
+
functionMetadata: { [name in keyof Functions]: FunctionMetadata<Schema, Functions[name]> };
|
|
279
279
|
/** Provide access to [Querysub.CALL_PERMISSIONS_KEY] to allow function calls (or provide access at the root). */
|
|
280
280
|
permissions?: SchemaToPermissions<Schema> & {
|
|
281
281
|
[CALL_PERMISSIONS_KEY]?: {
|
package/src/4-dom/qreactTest.tsx
CHANGED
|
@@ -326,7 +326,14 @@ let { data, functions } = Querysub.createSchema<{
|
|
|
326
326
|
},
|
|
327
327
|
module,
|
|
328
328
|
moduleId: "qreactTest",
|
|
329
|
-
functionMetadata: {
|
|
329
|
+
functionMetadata: {
|
|
330
|
+
setValue: {
|
|
331
|
+
keyOverride: {
|
|
332
|
+
getPrefix: data => data().values,
|
|
333
|
+
getKey: (key: string) => key,
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
330
337
|
permissions: {
|
|
331
338
|
PERMISSIONS: isSuperUserPERMISSIONS
|
|
332
339
|
}
|
|
@@ -95,9 +95,13 @@ export class LogViewer3 extends qreact.Component {
|
|
|
95
95
|
getPaths(): TimeFilePathWithSize[] {
|
|
96
96
|
let paths: TimeFilePathWithSize[] = [];
|
|
97
97
|
if (savedPathsURL.value) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
try {
|
|
99
|
+
const compressed = Buffer.from(savedPathsURL.value, "base64");
|
|
100
|
+
const decompressed = Zip.gunzipSync(compressed);
|
|
101
|
+
paths = JSON.parse(decompressed.toString("utf8"));
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
console.warn(`Error parsing saved paths: ${e.stack}`);
|
|
104
|
+
}
|
|
101
105
|
} else {
|
|
102
106
|
paths = this.state.paths;
|
|
103
107
|
}
|
|
@@ -7,8 +7,14 @@ import { logErrors } from "../../errors";
|
|
|
7
7
|
import { addGlobalContext } from "./diskLogger";
|
|
8
8
|
import child_process from "child_process";
|
|
9
9
|
import { getNodeIdLocation } from "socket-function/src/nodeCache";
|
|
10
|
+
import { getGitRefLive } from "../../4-deploy/git";
|
|
10
11
|
|
|
11
12
|
export function addBuiltInContext() {
|
|
13
|
+
let __gitHash = "";
|
|
14
|
+
setImmediate(async () => {
|
|
15
|
+
__gitHash = await getGitRefLive();
|
|
16
|
+
});
|
|
17
|
+
|
|
12
18
|
addGlobalContext(() => {
|
|
13
19
|
let nodeId = getOwnNodeId();
|
|
14
20
|
let nodeParts = getNodeIdLocation(nodeId);
|
|
@@ -25,6 +31,7 @@ export function addBuiltInContext() {
|
|
|
25
31
|
__os: process.platform,
|
|
26
32
|
__pid: process.pid,
|
|
27
33
|
__call: SocketFunction.TOTAL_CALLS,
|
|
34
|
+
__gitHash: __gitHash,
|
|
28
35
|
};
|
|
29
36
|
});
|
|
30
37
|
const getHostname = lazy(() => child_process.execSync("hostname").toString().trim());
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { css } from "typesafecss";
|
|
2
|
+
import { InputProps } from "./Input";
|
|
3
|
+
import { qreact } from "../4-dom/qreact";
|
|
4
|
+
|
|
5
|
+
export class InputAutocomplete extends qreact.Component<InputProps & {
|
|
6
|
+
value: string | { value: string };
|
|
7
|
+
label?: string;
|
|
8
|
+
options: string[];
|
|
9
|
+
onChangeValue: (value: string) => void;
|
|
10
|
+
focusOnMount?: boolean;
|
|
11
|
+
}> {
|
|
12
|
+
handleInput = (e: Event) => {
|
|
13
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
14
|
+
const inputEvent = e as InputEvent;
|
|
15
|
+
|
|
16
|
+
// Only autocomplete on text insertion, not deletion
|
|
17
|
+
if (!inputEvent.inputType || !inputEvent.inputType.startsWith("insert")) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const selectionStart = input.selectionStart || 0;
|
|
22
|
+
const userTyped = input.value.slice(0, selectionStart);
|
|
23
|
+
|
|
24
|
+
if (!userTyped) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Find matching character names
|
|
29
|
+
const options = this.props.options;
|
|
30
|
+
const lowerUserTyped = userTyped.toLowerCase();
|
|
31
|
+
const match = options.find(name =>
|
|
32
|
+
name.toLowerCase().startsWith(lowerUserTyped)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (match) {
|
|
36
|
+
// Replace with the matched name's casing
|
|
37
|
+
input.value = match;
|
|
38
|
+
input.setSelectionRange(userTyped.length, match.length);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
handleKeyDown = (e: KeyboardEvent) => {
|
|
43
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
44
|
+
|
|
45
|
+
if (e.key === "Tab" || e.key === "Enter") {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
this.props.onChangeValue(input.value);
|
|
48
|
+
input.blur();
|
|
49
|
+
} else if (e.key === "Escape") {
|
|
50
|
+
input.value = this.getValue();
|
|
51
|
+
input.blur();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
handleBlur = (e: FocusEvent) => {
|
|
56
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
57
|
+
this.props.onChangeValue(input.value);
|
|
58
|
+
};
|
|
59
|
+
getValue() {
|
|
60
|
+
if (typeof this.props.value === "string") {
|
|
61
|
+
return this.props.value;
|
|
62
|
+
}
|
|
63
|
+
return this.props.value.value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render() {
|
|
67
|
+
let { value, options, onChangeValue, focusOnMount, ...nativeProps } = this.props;
|
|
68
|
+
const styleClass = (
|
|
69
|
+
css.border("1px solid hsl(0, 0%, 50%)", "soft")
|
|
70
|
+
.background("hsl(0, 0%, 7%)", "soft")
|
|
71
|
+
.color("hsl(0, 0%, 95%)", "soft")
|
|
72
|
+
.display("flex", "soft")
|
|
73
|
+
.outline("3px solid hsl(204, 100%, 50%)", "focus", "soft")
|
|
74
|
+
+ " " + css.width("100%", "soft")
|
|
75
|
+
+ " " + (nativeProps.className || "")
|
|
76
|
+
);
|
|
77
|
+
return (
|
|
78
|
+
<input
|
|
79
|
+
{...nativeProps as any}
|
|
80
|
+
ref={elem => {
|
|
81
|
+
if (elem) {
|
|
82
|
+
elem.value = this.getValue();
|
|
83
|
+
if (focusOnMount) {
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
elem.focus();
|
|
86
|
+
elem.select();
|
|
87
|
+
}, 0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
type="text"
|
|
92
|
+
onFocus={e => (e.currentTarget as HTMLInputElement).select()}
|
|
93
|
+
onInput={this.handleInput}
|
|
94
|
+
onKeyDown={this.handleKeyDown}
|
|
95
|
+
onBlur={this.handleBlur}
|
|
96
|
+
className={styleClass}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -12,9 +12,9 @@ function onUncaught(...args: unknown[]) {
|
|
|
12
12
|
}
|
|
13
13
|
// Ignore ResizeObserver errors, they are spurious
|
|
14
14
|
// - https://github.com/vercel/next.js/discussions/51551
|
|
15
|
-
if (error.message
|
|
15
|
+
if (error.message?.startsWith("ResizeObserver loop")) return;
|
|
16
16
|
// We should really do this better. Basically, if we're disposing or canceling, it's not actually an error, just ignore it.
|
|
17
|
-
if (error.message
|
|
17
|
+
if (error.message?.startsWith("Dispose")) return;
|
|
18
18
|
|
|
19
19
|
onMessage({
|
|
20
20
|
type: "error",
|
|
@@ -285,11 +285,125 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
285
285
|
functionMetadata: {
|
|
286
286
|
// There's no point to predict sending emails, or inviting users, or registering the page load time or sending discord messages, As the main function of these can only be done server-side.
|
|
287
287
|
// verifyMachineId is used when logging in with the login token. We could predict it, but there's no real reason to because if we do, all of our reads are gonna fail until it runs, Which will actually be really confusing to the user and look like a bug.
|
|
288
|
-
sendLoginEmail: {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
sendLoginEmail: {
|
|
289
|
+
nopredict: true,
|
|
290
|
+
keyOverride: {
|
|
291
|
+
getPrefix: data => data().secure.emailToUserId,
|
|
292
|
+
getKey: config => config.email,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
verifyMachineId: {
|
|
296
|
+
nopredict: true,
|
|
297
|
+
keyOverride: {
|
|
298
|
+
getPrefix: data => data().users,
|
|
299
|
+
getKey: config => config.loginToken.split("_")[1],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
inviteUser: {
|
|
303
|
+
nopredict: true,
|
|
304
|
+
keyOverride: {
|
|
305
|
+
getPrefix: data => data().users,
|
|
306
|
+
getKey: () => getCurrentUserAssert(),
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
registerPageLoadTime: {
|
|
310
|
+
nopredict: true,
|
|
311
|
+
keyOverride: {
|
|
312
|
+
getPrefix: data => data().users,
|
|
313
|
+
getKey: () => getCurrentUser() ?? Querysub.getCallerMachineId(),
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
testSendDiscordMessage: {
|
|
317
|
+
nopredict: true,
|
|
318
|
+
keyOverride: {
|
|
319
|
+
getPrefix: data => data().secure,
|
|
320
|
+
getKey: () => "notifyDiscordWebhookURL",
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
updateSettings: {
|
|
324
|
+
keyOverride: {
|
|
325
|
+
getPrefix: data => data().users,
|
|
326
|
+
getKey: () => getCurrentUserAssert(),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
logoutCurrent: {
|
|
330
|
+
keyOverride: {
|
|
331
|
+
getPrefix: data => data().users,
|
|
332
|
+
getKey: () => getCurrentUser() ?? Querysub.getCallerMachineId(),
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
logoutSpecific: {
|
|
336
|
+
keyOverride: {
|
|
337
|
+
getPrefix: data => data().users,
|
|
338
|
+
getKey: () => getCurrentUserAssert(),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
removeLoginToken: {
|
|
342
|
+
keyOverride: {
|
|
343
|
+
getPrefix: data => data().users,
|
|
344
|
+
getKey: () => getCurrentUserAssert(),
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
specialResetLogins: {
|
|
348
|
+
keyOverride: {
|
|
349
|
+
getPrefix: data => data().users,
|
|
350
|
+
getKey: config => config.userId,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
specialBlockIP: {
|
|
354
|
+
keyOverride: {
|
|
355
|
+
getPrefix: data => data().secure.blockedIPs,
|
|
356
|
+
getKey: config => config.ip,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
specialUnblockIP: {
|
|
360
|
+
keyOverride: {
|
|
361
|
+
getPrefix: data => data().secure.blockedIPs,
|
|
362
|
+
getKey: config => config.ip,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
specialSetInviteCount: {
|
|
366
|
+
keyOverride: {
|
|
367
|
+
getPrefix: data => data().users,
|
|
368
|
+
getKey: config => config.userId,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
setPostmarkAPIKey: {
|
|
372
|
+
keyOverride: {
|
|
373
|
+
getPrefix: data => data().secure,
|
|
374
|
+
getKey: () => "postmarkAPIKey",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
setNotifyDiscordWebhookURL: {
|
|
378
|
+
keyOverride: {
|
|
379
|
+
getPrefix: data => data().secure,
|
|
380
|
+
getKey: () => "notifyDiscordWebhookURL",
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
setOpenRouterAPIKey: {
|
|
384
|
+
keyOverride: {
|
|
385
|
+
getPrefix: data => data().secure,
|
|
386
|
+
getKey: () => "openRouterAPIKey",
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
specialSetUserType: {
|
|
390
|
+
keyOverride: {
|
|
391
|
+
getPrefix: data => data().users,
|
|
392
|
+
getKey: config => config.userId,
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
banUser: {
|
|
396
|
+
keyOverride: {
|
|
397
|
+
getPrefix: data => data().users,
|
|
398
|
+
getKey: config => config.userId,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
unbanUser: {
|
|
402
|
+
keyOverride: {
|
|
403
|
+
getPrefix: data => data().users,
|
|
404
|
+
getKey: config => config.userId,
|
|
405
|
+
},
|
|
406
|
+
},
|
|
293
407
|
},
|
|
294
408
|
});
|
|
295
409
|
|