querysub 0.327.0 → 0.329.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/bin/error-email.js +8 -0
- package/bin/error-im.js +8 -0
- package/package.json +4 -3
- package/src/-a-archives/archivesBackBlaze.ts +20 -0
- package/src/-a-archives/archivesCborT.ts +52 -0
- package/src/-a-archives/archivesDisk.ts +5 -5
- package/src/-a-archives/archivesJSONT.ts +19 -5
- package/src/-a-archives/archivesLimitedCache.ts +118 -7
- package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
- package/src/-g-core-values/NodeCapabilities.ts +26 -11
- package/src/0-path-value-core/auditLogs.ts +4 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +7 -0
- package/src/3-path-functions/PathFunctionRunner.ts +2 -2
- package/src/4-querysub/Querysub.ts +1 -1
- package/src/5-diagnostics/GenericFormat.tsx +2 -2
- package/src/config.ts +15 -3
- package/src/deployManager/machineApplyMainCode.ts +10 -8
- package/src/deployManager/machineSchema.ts +4 -3
- package/src/deployManager/setupMachineMain.ts +3 -2
- package/src/diagnostics/logs/FastArchiveAppendable.ts +86 -53
- package/src/diagnostics/logs/FastArchiveController.ts +11 -2
- package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
- package/src/diagnostics/logs/LogViewer2.tsx +78 -34
- package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +5 -4
- package/src/diagnostics/logs/diskLogger.ts +70 -23
- package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +94 -67
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +50 -16
- package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +291 -0
- package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +185 -68
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +10 -19
- package/src/diagnostics/managementPages.tsx +33 -15
- package/src/email_ims_notifications/discord.tsx +203 -0
- package/src/{email → email_ims_notifications}/postmark.tsx +3 -3
- package/src/fs.ts +9 -0
- package/src/functional/SocketChannel.ts +9 -0
- package/src/functional/throttleRender.ts +134 -0
- package/src/library-components/ATag.tsx +2 -2
- package/src/library-components/SyncedController.ts +3 -3
- package/src/misc.ts +18 -0
- package/src/misc2.ts +106 -0
- package/src/user-implementation/SecurityPage.tsx +11 -5
- package/src/user-implementation/userData.ts +57 -23
- package/testEntry2.ts +14 -5
- package/src/user-implementation/setEmailKey.ts +0 -25
- /package/src/{email → email_ims_notifications}/sendgrid.tsx +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// NOTE: Many cases we don't know if we want to throttle, or how long we want to throttle for, until we start the watcher, so this has to be done inside our render, instead of in ProxyWatcher.
|
|
4
|
+
|
|
5
|
+
import { cache } from "socket-function/src/caching";
|
|
6
|
+
import { proxyWatcher, SyncWatcher } from "../2-proxy/PathValueProxyWatcher";
|
|
7
|
+
import { qreact } from "../4-dom/qreact";
|
|
8
|
+
import { onNextPaint } from "./onNextPaint";
|
|
9
|
+
|
|
10
|
+
// Throttles calls that have the same throttleKey
|
|
11
|
+
/** Used near the start of your render, like so:
|
|
12
|
+
if (throttleRender({ key: "NodeControls", frameDelay: 0, frameDelaySmear: 60 })) return undefined;
|
|
13
|
+
- Doesn't break watches, so debug tools will still show you are watching them
|
|
14
|
+
- Doesn't break the rendered output (uses the last fully rendered output)
|
|
15
|
+
- Pauses rendering until the throttle finishes, then forcefully renders once.
|
|
16
|
+
- Always delays by at least 1 frame.
|
|
17
|
+
*/
|
|
18
|
+
export function throttleRender(config: {
|
|
19
|
+
// Throttles are smeared perkey
|
|
20
|
+
key: string;
|
|
21
|
+
// We pick a large value in this range as throttleKey overlaps, allowing you to smear a large number of changes over a larger period of time, BUT, while also waiting less time if there are few changes!
|
|
22
|
+
// - Throttles won't necessarily finish in the same order you call them (we have to wrap around in the range after we hit the end, so later calls might re-trigger earlier
|
|
23
|
+
frameDelay: number;
|
|
24
|
+
// Range is [frameDelay, frameDelay + smear]
|
|
25
|
+
frameDelaySmear?: number;
|
|
26
|
+
// Delays for additional frames. Ex, set frameDelay to 0, smear to 60, and count to 30. The first delay is 0, then 30, then 60
|
|
27
|
+
frameDelayCount?: number;
|
|
28
|
+
}): boolean {
|
|
29
|
+
let watcher = proxyWatcher.getTriggeredWatcher();
|
|
30
|
+
// Never throttle the first render, as that would be noticeable and always unintended
|
|
31
|
+
if (watcher.syncRunCount === 0) return false;
|
|
32
|
+
let { schedule, inSchedule, isTriggered, runScheduler } = throttleManager(config.key);
|
|
33
|
+
if (isTriggered(watcher)) {
|
|
34
|
+
//console.log(`Triggering throttle render for ${watcher.debugName} on frame ${getFrameNumber()}`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
runScheduler();
|
|
38
|
+
if (inSchedule.has(watcher)) {
|
|
39
|
+
//console.log(`Skipping throttle render for ${watcher.debugName} because it is already in the schedule`);
|
|
40
|
+
proxyWatcher.reuseLastWatches();
|
|
41
|
+
qreact.cancelRender();
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
inSchedule.add(watcher);
|
|
45
|
+
//console.log(`Adding throttle render for ${watcher.debugName}`);
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
let count = config.frameDelayCount || 1;
|
|
49
|
+
let smear = config.frameDelaySmear ?? 0;
|
|
50
|
+
let curIndex = config.frameDelay;
|
|
51
|
+
let endIndex = config.frameDelay + smear;
|
|
52
|
+
let targetFillCount = 0;
|
|
53
|
+
// Annoying algorithm to find the lowest slow available...
|
|
54
|
+
while (true) {
|
|
55
|
+
let cur = schedule[curIndex];
|
|
56
|
+
if ((cur?.length || 0) === targetFillCount) {
|
|
57
|
+
schedule[curIndex] = schedule[curIndex] || [];
|
|
58
|
+
schedule[curIndex]!.push(watcher);
|
|
59
|
+
for (let i = 1; i < count; i++) {
|
|
60
|
+
schedule[curIndex + i] = schedule[curIndex + i] || [];
|
|
61
|
+
schedule[curIndex + i]!.push("delay");
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
curIndex++;
|
|
66
|
+
if (curIndex > endIndex) {
|
|
67
|
+
curIndex = config.frameDelay;
|
|
68
|
+
targetFillCount++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
proxyWatcher.reuseLastWatches();
|
|
73
|
+
qreact.cancelRender();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let throttleManager = cache((key: string) => {
|
|
78
|
+
let schedule: ((SyncWatcher | "delay")[] | undefined)[] = [];
|
|
79
|
+
let inSchedule = new Set<SyncWatcher>();
|
|
80
|
+
let triggered = new Set<SyncWatcher>();
|
|
81
|
+
let runningScheduler = false;
|
|
82
|
+
function isTriggered(watcher: SyncWatcher) {
|
|
83
|
+
return triggered.has(watcher);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
schedule,
|
|
88
|
+
inSchedule,
|
|
89
|
+
isTriggered,
|
|
90
|
+
runScheduler: () => {
|
|
91
|
+
if (runningScheduler) return;
|
|
92
|
+
runningScheduler = true;
|
|
93
|
+
void onNextPaint().finally(runNextTick);
|
|
94
|
+
function runNextTick() {
|
|
95
|
+
let next = schedule.shift();
|
|
96
|
+
void onNextPaint().finally(() => {
|
|
97
|
+
triggered.clear();
|
|
98
|
+
});
|
|
99
|
+
if (next) {
|
|
100
|
+
for (let watcher of next) {
|
|
101
|
+
if (watcher === "delay") continue;
|
|
102
|
+
inSchedule.delete(watcher);
|
|
103
|
+
triggered.add(watcher);
|
|
104
|
+
//console.log(`Triggering throttle render for ${watcher.debugName}`);
|
|
105
|
+
watcher.explicitlyTrigger({
|
|
106
|
+
paths: new Set(),
|
|
107
|
+
pathSources: new Set(),
|
|
108
|
+
newParentsSynced: new Set(),
|
|
109
|
+
extraReasons: ["throttleRender trigger"],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (schedule.length > 0) {
|
|
114
|
+
void onNextPaint().finally(runNextTick);
|
|
115
|
+
} else {
|
|
116
|
+
runningScheduler = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
let rendersInFrame = 0;
|
|
126
|
+
export function countRendersInFrame() {
|
|
127
|
+
if (rendersInFrame === 0) {
|
|
128
|
+
void onNextPaint().finally(() => {
|
|
129
|
+
//console.log(`Render in frame ${rendersInFrame}`);
|
|
130
|
+
rendersInFrame = 0;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
rendersInFrame++;
|
|
134
|
+
}
|
|
@@ -3,6 +3,7 @@ import { css, isNode } from "typesafecss";
|
|
|
3
3
|
import { URLParam, parseSearchString, encodeSearchString } from "./URLParam";
|
|
4
4
|
import { qreact } from "../4-dom/qreact";
|
|
5
5
|
import { niceStringify } from "../niceStringify";
|
|
6
|
+
import { getDomain } from "../config";
|
|
6
7
|
|
|
7
8
|
export type URLOverride<T = unknown> = {
|
|
8
9
|
param: URLParam<T>;
|
|
@@ -118,8 +119,7 @@ export const Anchor = ATag;
|
|
|
118
119
|
export const Link = ATag;
|
|
119
120
|
|
|
120
121
|
export function createLink(values: URLOverride[]) {
|
|
121
|
-
|
|
122
|
-
let urlObj = new URL(document.location.href);
|
|
122
|
+
let urlObj = new URL(isNode() ? "https://" + getDomain() : document.location.href);
|
|
123
123
|
let params = parseSearchString(urlObj.search);
|
|
124
124
|
for (let value of values) {
|
|
125
125
|
params[value.param.urlKey] = value.value;
|
|
@@ -109,7 +109,7 @@ export function getSyncedController<T extends SocketRegistered>(
|
|
|
109
109
|
resetAll(): void;
|
|
110
110
|
refreshAll(): void;
|
|
111
111
|
isAnyLoading(): boolean;
|
|
112
|
-
|
|
112
|
+
__baseController: T;
|
|
113
113
|
} {
|
|
114
114
|
if (isNode()) {
|
|
115
115
|
let result = cache((nodeId: string) => {
|
|
@@ -161,7 +161,7 @@ export function getSyncedController<T extends SocketRegistered>(
|
|
|
161
161
|
result.isAnyLoading = () => {
|
|
162
162
|
notAllowedOnServer();
|
|
163
163
|
};
|
|
164
|
-
result.
|
|
164
|
+
result.__baseController = controller;
|
|
165
165
|
return result;
|
|
166
166
|
}
|
|
167
167
|
let id = nextId();
|
|
@@ -416,6 +416,6 @@ export function getSyncedController<T extends SocketRegistered>(
|
|
|
416
416
|
}
|
|
417
417
|
});
|
|
418
418
|
};
|
|
419
|
-
result.
|
|
419
|
+
result.__baseController = controller;
|
|
420
420
|
return result;
|
|
421
421
|
}
|
package/src/misc.ts
CHANGED
|
@@ -126,6 +126,19 @@ export function partialCopyObject(data: unknown, maxFields: number = 500): unkno
|
|
|
126
126
|
|
|
127
127
|
// Handle plain objects
|
|
128
128
|
const result: Record<string, unknown> = {};
|
|
129
|
+
|
|
130
|
+
// Special case for Error objects - include stack and message
|
|
131
|
+
if (value instanceof Error) {
|
|
132
|
+
fieldCount++;
|
|
133
|
+
if (fieldCount < maxFields) {
|
|
134
|
+
result.message = value.message;
|
|
135
|
+
}
|
|
136
|
+
fieldCount++;
|
|
137
|
+
if (fieldCount < maxFields) {
|
|
138
|
+
result.stack = value.stack;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
for (const key of Object.keys(value as object)) {
|
|
130
143
|
fieldCount++;
|
|
131
144
|
if (fieldCount >= maxFields) break;
|
|
@@ -172,4 +185,9 @@ export function streamToIteratable<T>(reader: {
|
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
187
|
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const AsyncFunction = (async () => { }).constructor;
|
|
191
|
+
export function isAsyncFunction(func: unknown): boolean {
|
|
192
|
+
return func instanceof AsyncFunction;
|
|
175
193
|
}
|
package/src/misc2.ts
CHANGED
|
@@ -1,5 +1,111 @@
|
|
|
1
|
+
import { delay } from "socket-function/src/batching";
|
|
2
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
3
|
+
import { timeInHour } from "socket-function/src/misc";
|
|
1
4
|
import { atomic } from "./2-proxy/PathValueProxyWatcher";
|
|
2
5
|
|
|
3
6
|
export function isStrSimilar(a: string | undefined, b: string | undefined) {
|
|
4
7
|
return atomic(a)?.toLowerCase().trim().replaceAll(" ", "") === atomic(b)?.toLowerCase().trim().replaceAll(" ", "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface StreamLike {
|
|
11
|
+
on(event: "data", listener: (chunk: Buffer) => void): void;
|
|
12
|
+
on(event: "end", listener: () => void): void;
|
|
13
|
+
on(event: "error", listener: (error: Error) => void): void;
|
|
14
|
+
}
|
|
15
|
+
export async function* streamToAsyncIterable(stream: StreamLike): AsyncIterable<Buffer> {
|
|
16
|
+
let pendingChunks: Buffer[] = [];
|
|
17
|
+
let streamEnded = false;
|
|
18
|
+
let streamError: Error | undefined = undefined;
|
|
19
|
+
let resolveNext: (() => void) | undefined = undefined;
|
|
20
|
+
|
|
21
|
+
stream.on("data", (chunk: Buffer) => {
|
|
22
|
+
pendingChunks.push(chunk);
|
|
23
|
+
if (resolveNext) {
|
|
24
|
+
resolveNext();
|
|
25
|
+
resolveNext = undefined;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
stream.on("end", () => {
|
|
30
|
+
streamEnded = true;
|
|
31
|
+
if (resolveNext) {
|
|
32
|
+
resolveNext();
|
|
33
|
+
resolveNext = undefined;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
stream.on("error", (error) => {
|
|
38
|
+
streamError = error;
|
|
39
|
+
if (resolveNext) {
|
|
40
|
+
resolveNext();
|
|
41
|
+
resolveNext = undefined;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
while (!streamEnded && !streamError) {
|
|
46
|
+
if (pendingChunks.length > 0) {
|
|
47
|
+
yield pendingChunks.shift()!;
|
|
48
|
+
} else {
|
|
49
|
+
await new Promise<void>((resolve) => {
|
|
50
|
+
resolveNext = resolve;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (streamError) {
|
|
56
|
+
throw streamError;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
while (pendingChunks.length > 0) {
|
|
60
|
+
yield pendingChunks.shift()!;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
export async function runScheduler(hours: number[], func: () => Promise<void>) {
|
|
66
|
+
while (true) {
|
|
67
|
+
try {
|
|
68
|
+
const now = new Date();
|
|
69
|
+
|
|
70
|
+
// Find the next check time from our list
|
|
71
|
+
const sortedHours = [...hours].sort((a, b) => a - b);
|
|
72
|
+
let targetTime = new Date(now);
|
|
73
|
+
|
|
74
|
+
// Find the next check hour after the current time
|
|
75
|
+
let nextHour = sortedHours.find(hour => {
|
|
76
|
+
const checkTime = new Date(now);
|
|
77
|
+
checkTime.setHours(hour, 0, 0, 0);
|
|
78
|
+
return checkTime > now;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (nextHour === undefined) {
|
|
82
|
+
// No more checks today, take the first one tomorrow
|
|
83
|
+
nextHour = sortedHours[0];
|
|
84
|
+
targetTime.setDate(targetTime.getDate() + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
targetTime.setHours(nextHour, 0, 0, 0);
|
|
88
|
+
|
|
89
|
+
console.log(`Next target time: ${targetTime.toLocaleString()}`);
|
|
90
|
+
|
|
91
|
+
// Wait and check every 15 minutes until it's time
|
|
92
|
+
while (true) {
|
|
93
|
+
const currentTime = new Date();
|
|
94
|
+
const timeUntilCheck = targetTime.getTime() - currentTime.getTime();
|
|
95
|
+
|
|
96
|
+
if (timeUntilCheck <= 0) {
|
|
97
|
+
console.log("Time to run the check!");
|
|
98
|
+
await func();
|
|
99
|
+
break; // Break out of inner loop to calculate next check time
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`Time until next check: ${formatTime(timeUntilCheck)}`);
|
|
103
|
+
await delay(timeInHour / 4); // Wait 15 minutes (1/4 of an hour)
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(error);
|
|
107
|
+
// Even if there's an error, wait 15 minutes before trying again
|
|
108
|
+
await delay(timeInHour / 4);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
5
111
|
}
|
|
@@ -13,7 +13,7 @@ import { redButton } from "../library-components/colors";
|
|
|
13
13
|
|
|
14
14
|
export class SecurityPage extends qreact.Component {
|
|
15
15
|
render() {
|
|
16
|
-
return <div class={css.pad(10)}>
|
|
16
|
+
return <div class={css.pad(10).fillWidth}>
|
|
17
17
|
<TabbedUI
|
|
18
18
|
tabs={[
|
|
19
19
|
{ value: "config", title: "Config", contents: <ConfigTab /> },
|
|
@@ -49,13 +49,19 @@ class ConfigTab extends qreact.Component {
|
|
|
49
49
|
<InputLabel
|
|
50
50
|
label={"Postmark API Key"}
|
|
51
51
|
edit
|
|
52
|
-
|
|
52
|
+
fillWidth
|
|
53
53
|
value={user_data().secure.postmarkAPIKey}
|
|
54
54
|
onChangeValue={value => user_functions.setPostmarkAPIKey({ apiKey: value })}
|
|
55
55
|
/>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
<InputLabel
|
|
57
|
+
label={"Discord Webhook URL"}
|
|
58
|
+
edit
|
|
59
|
+
value={user_data().secure.notifyDiscordWebhookURL}
|
|
60
|
+
onChangeValue={value => user_functions.setNotifyDiscordWebhookURL({ webhookURL: value })}
|
|
61
|
+
/>
|
|
62
|
+
<Button onClick={() => user_functions.testSendDiscordMessage()}>
|
|
63
|
+
Test Discord Message
|
|
64
|
+
</Button>
|
|
59
65
|
</>
|
|
60
66
|
);
|
|
61
67
|
}
|
|
@@ -6,17 +6,21 @@ import { registerAliveChecker } from "../2-proxy/garbageCollection";
|
|
|
6
6
|
import { generateLoginEmail } from "./loginEmail";
|
|
7
7
|
import { logErrors } from "../errors";
|
|
8
8
|
import { PermissionsParameters } from "../3-path-functions/syncSchema";
|
|
9
|
-
import { sendEmail_postmark } from "../
|
|
9
|
+
import { sendEmail_postmark } from "../email_ims_notifications/postmark";
|
|
10
10
|
import { isClient } from "../config2";
|
|
11
11
|
import { getExternalIP } from "../misc/networking";
|
|
12
12
|
import { MAX_ACCEPTED_CHANGE_AGE } from "../0-path-value-core/pathValueCore";
|
|
13
13
|
import { createURLSync } from "../library-components/URLParam";
|
|
14
|
-
import { devDebugbreak, getDomain, isDevDebugbreak, isRecovery } from "../config";
|
|
14
|
+
import { devDebugbreak, getDomain, getEmailDomain, isDevDebugbreak, isRecovery } 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
18
|
import { sha256 } from "js-sha256";
|
|
19
19
|
import { logDisk } from "../diagnostics/logs/diskLogger";
|
|
20
|
+
import { createLink } from "../library-components/ATag";
|
|
21
|
+
import { showingManagementURL, managementPageURL } from "../diagnostics/managementPages";
|
|
22
|
+
import { sendDiscordMessage } from "../email_ims_notifications/discord";
|
|
23
|
+
import { formatDateTime } from "socket-function/src/formatting/format";
|
|
20
24
|
|
|
21
25
|
/*
|
|
22
26
|
IMPORTANT!
|
|
@@ -161,6 +165,7 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
161
165
|
secure: {
|
|
162
166
|
signupsOpen?: boolean;
|
|
163
167
|
postmarkAPIKey?: string;
|
|
168
|
+
notifyDiscordWebhookURL?: string;
|
|
164
169
|
|
|
165
170
|
// NOTE: For now these will only be blocked at a user level. If we run into issues, we can
|
|
166
171
|
// make Querysub have configurable IP blocking that accepts a synced read function, which
|
|
@@ -217,18 +222,25 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
217
222
|
specialBlockIP, specialUnblockIP,
|
|
218
223
|
specialSetInviteCount,
|
|
219
224
|
setPostmarkAPIKey,
|
|
225
|
+
setNotifyDiscordWebhookURL,
|
|
220
226
|
specialSetUserType,
|
|
221
227
|
registerPageLoadTime,
|
|
222
228
|
banUser, unbanUser,
|
|
223
|
-
|
|
229
|
+
testSendDiscordMessage: () => {
|
|
224
230
|
assertUserType("superuser");
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
data().users[
|
|
231
|
-
|
|
231
|
+
const webhookURL = atomic(data().secure.notifyDiscordWebhookURL);
|
|
232
|
+
if (!webhookURL) {
|
|
233
|
+
throw new Error("No Discord webhook URL set");
|
|
234
|
+
}
|
|
235
|
+
let callerUserId = getCurrentUserAssert();
|
|
236
|
+
let email = data().users[callerUserId].email;
|
|
237
|
+
Querysub.onCommitFinished(async () => {
|
|
238
|
+
await sendDiscordMessage({
|
|
239
|
+
webhookURL,
|
|
240
|
+
message: `Discord webhook test at ${formatDateTime(Date.now())} by ${email} (${JSON.stringify(callerUserId)})`,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
},
|
|
232
244
|
},
|
|
233
245
|
module,
|
|
234
246
|
moduleId: "userData2",
|
|
@@ -269,11 +281,13 @@ const { data, functions } = Querysub.syncSchema<{
|
|
|
269
281
|
},
|
|
270
282
|
},
|
|
271
283
|
functionMetadata: {
|
|
284
|
+
// 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.
|
|
285
|
+
// 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.
|
|
272
286
|
sendLoginEmail: { nopredict: true },
|
|
273
287
|
verifyMachineId: { nopredict: true },
|
|
274
288
|
inviteUser: { nopredict: true },
|
|
275
|
-
setPostmarkAPIKey: { nopredict: true },
|
|
276
289
|
registerPageLoadTime: { nopredict: true },
|
|
290
|
+
testSendDiscordMessage: { nopredict: true },
|
|
277
291
|
},
|
|
278
292
|
});
|
|
279
293
|
|
|
@@ -593,9 +607,14 @@ function sendLoginEmail(config: {
|
|
|
593
607
|
const machineId = Querysub.getCallerMachineId();
|
|
594
608
|
const now = Querysub.getCallTime();
|
|
595
609
|
|
|
610
|
+
// Check for the API key, even though we don't immediately use it, so that we can get good errors to tell the user (likely the developer) to setup the postmark API key.
|
|
596
611
|
const apiKey = atomic(data().secure.postmarkAPIKey);
|
|
597
612
|
if (!apiKey) {
|
|
598
|
-
|
|
613
|
+
let link = createLink([
|
|
614
|
+
showingManagementURL.getOverride(true),
|
|
615
|
+
managementPageURL.getOverride("SecurityPage"),
|
|
616
|
+
]);
|
|
617
|
+
throw new Error(`No postmark API key, so we can't send login emails. Set the key at ${link}.`);
|
|
599
618
|
}
|
|
600
619
|
|
|
601
620
|
{
|
|
@@ -670,18 +689,36 @@ function sendLoginEmail(config: {
|
|
|
670
689
|
timeoutTime,
|
|
671
690
|
});
|
|
672
691
|
Querysub.onCommitFinished(async () => {
|
|
673
|
-
await
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// TODO: Allow configuring this (defaulting to getDomain() if unconfigured). For now we hardcode it, because it takes
|
|
677
|
-
// a while to verify a new postmark email, and we don't even know what final domain we will be using.
|
|
678
|
-
from: "login@querysub.com",
|
|
692
|
+
await sendEmail({
|
|
693
|
+
to: [email],
|
|
694
|
+
fromPrefix: "login",
|
|
679
695
|
subject,
|
|
680
696
|
contents,
|
|
681
697
|
});
|
|
682
698
|
});
|
|
683
699
|
});
|
|
684
700
|
}
|
|
701
|
+
export async function sendEmail(config: {
|
|
702
|
+
to: string[];
|
|
703
|
+
// The domain should be getDomain
|
|
704
|
+
// TODO: Actually use git domain, for now it's hardcoded, because setting up a new
|
|
705
|
+
fromPrefix: string;
|
|
706
|
+
subject: string;
|
|
707
|
+
contents: preact.VNode;
|
|
708
|
+
}) {
|
|
709
|
+
let key = await Querysub.commitAsync(() => atomic(data().secure.postmarkAPIKey));
|
|
710
|
+
if (!key) {
|
|
711
|
+
console.warn(`No postmark API key setup, so we can't send email`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
await sendEmail_postmark({
|
|
715
|
+
apiKey: key,
|
|
716
|
+
to: config.to,
|
|
717
|
+
from: `${config.fromPrefix}@${getEmailDomain()}`,
|
|
718
|
+
subject: config.subject,
|
|
719
|
+
contents: config.contents,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
685
722
|
|
|
686
723
|
function verifyMachineId(config: {
|
|
687
724
|
loginToken: string
|
|
@@ -938,11 +975,8 @@ export function scriptCreateUser(config: {
|
|
|
938
975
|
function setPostmarkAPIKey(config: { apiKey: string; }) {
|
|
939
976
|
data().secure.postmarkAPIKey = config.apiKey;
|
|
940
977
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
apiKey: string;
|
|
944
|
-
}) {
|
|
945
|
-
setPostmarkAPIKey(config);
|
|
978
|
+
function setNotifyDiscordWebhookURL(config: { webhookURL: string; }) {
|
|
979
|
+
data().secure.notifyDiscordWebhookURL = config.webhookURL;
|
|
946
980
|
}
|
|
947
981
|
|
|
948
982
|
|
package/testEntry2.ts
CHANGED
|
@@ -5,21 +5,34 @@ import { shutdown } from "./src/diagnostics/periodic";
|
|
|
5
5
|
import { testTCPIsListening } from "socket-function/src/networking";
|
|
6
6
|
import { Querysub } from "./src/4-querysub/QuerysubController";
|
|
7
7
|
import { timeInSecond } from "socket-function/src/misc";
|
|
8
|
+
import { LOG_LINE_LIMIT_ID } from "./src/diagnostics/logs/diskLogger";
|
|
9
|
+
import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
|
|
8
10
|
|
|
9
11
|
export async function testMain() {
|
|
10
12
|
Querysub;
|
|
13
|
+
await waitForFirstTimeSync();
|
|
14
|
+
|
|
11
15
|
//let test = await testTCPIsListening("1.1.1.1", 443);
|
|
12
16
|
//console.log(test);
|
|
13
17
|
// Writing heartbeat 2025/09/14 08:37:46 PM for self (5ac8a2fa78fce4ea.971ed8b01743d123.querysubtest.com:13900)
|
|
14
18
|
await delay(timeInSecond);
|
|
15
19
|
await Querysub.hostService("test");
|
|
16
20
|
await delay(timeInSecond * 5);
|
|
21
|
+
|
|
22
|
+
console.error(`A completely new error that is not suppressed. .`);
|
|
23
|
+
await delay(timeInSecond * 15);
|
|
24
|
+
await shutdown();
|
|
25
|
+
return;
|
|
26
|
+
|
|
27
|
+
|
|
17
28
|
// console.log(getOwnThreadId());
|
|
18
29
|
// Log an error every 30 seconds forever.
|
|
19
30
|
while (true) {
|
|
20
31
|
console.error(`Test warning for im testing ${Date.now()}`);
|
|
21
32
|
await delay(timeInSecond * 30);
|
|
22
33
|
}
|
|
34
|
+
|
|
35
|
+
|
|
23
36
|
// console.log(getOwnThreadId());
|
|
24
37
|
// await shutdown();
|
|
25
38
|
//await Querysub.hostService("test");
|
|
@@ -36,8 +49,4 @@ export async function testMain() {
|
|
|
36
49
|
// }
|
|
37
50
|
await delay(timeInSecond * 15);
|
|
38
51
|
await shutdown();
|
|
39
|
-
}
|
|
40
|
-
async function main() {
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
52
|
+
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Querysub } from "../4-querysub/Querysub";
|
|
2
|
-
import { pathValueCommitter } from "../0-path-value-core/PathValueCommitter";
|
|
3
|
-
import { scriptSetPostmarkAPIKey } from "./userData";
|
|
4
|
-
import { isNodeTrue } from "socket-function/src/misc";
|
|
5
|
-
import yargs from "yargs";
|
|
6
|
-
|
|
7
|
-
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
8
|
-
.option("key", { type: "string", desc: `The key to set for the email.` })
|
|
9
|
-
.argv || {}
|
|
10
|
-
;
|
|
11
|
-
|
|
12
|
-
async function main() {
|
|
13
|
-
const howToCall = ` Call with yarn setemailkey --key <key>`;
|
|
14
|
-
const key = yargObj.key;
|
|
15
|
-
if (!key) throw new Error("No key provided." + howToCall);
|
|
16
|
-
|
|
17
|
-
await Querysub.hostService("setEmailKey");
|
|
18
|
-
|
|
19
|
-
await Querysub.commitSynced(() => {
|
|
20
|
-
scriptSetPostmarkAPIKey({ apiKey: key });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
await pathValueCommitter.waitForValuesToCommit();
|
|
24
|
-
}
|
|
25
|
-
main().catch(e => console.log(e)).finally(() => process.exit());
|
|
File without changes
|