querysub 0.433.0 → 0.436.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.
Files changed (74) hide show
  1. package/.eslintrc.js +50 -50
  2. package/bin/deploy.js +0 -0
  3. package/bin/function.js +0 -0
  4. package/bin/server.js +0 -0
  5. package/costsBenefits.txt +115 -115
  6. package/deploy.ts +2 -2
  7. package/package.json +1 -1
  8. package/spec.txt +1192 -1192
  9. package/src/-a-archives/archives.ts +202 -202
  10. package/src/-a-archives/archivesBackBlaze.ts +1 -0
  11. package/src/-a-archives/archivesDisk.ts +454 -454
  12. package/src/-a-auth/certs.ts +540 -540
  13. package/src/-a-auth/node-forge-ed25519.d.ts +16 -16
  14. package/src/-b-authorities/dnsAuthority.ts +138 -138
  15. package/src/-c-identity/IdentityController.ts +258 -258
  16. package/src/-d-trust/NetworkTrust2.ts +180 -180
  17. package/src/-e-certs/EdgeCertController.ts +252 -252
  18. package/src/-e-certs/certAuthority.ts +201 -201
  19. package/src/-f-node-discovery/NodeDiscovery.ts +640 -640
  20. package/src/-g-core-values/NodeCapabilities.ts +200 -200
  21. package/src/-h-path-value-serialize/stringSerializer.ts +175 -175
  22. package/src/0-path-value-core/PathValueCommitter.ts +468 -468
  23. package/src/0-path-value-core/pathValueCore.ts +2 -2
  24. package/src/2-proxy/PathValueProxyWatcher.ts +2542 -2542
  25. package/src/2-proxy/TransactionDelayer.ts +94 -94
  26. package/src/2-proxy/pathDatabaseProxyBase.ts +36 -36
  27. package/src/2-proxy/pathValueProxy.ts +159 -159
  28. package/src/3-path-functions/PathFunctionRunnerMain.ts +87 -87
  29. package/src/3-path-functions/pathFunctionLoader.ts +516 -516
  30. package/src/3-path-functions/tests/rejectTest.ts +76 -76
  31. package/src/4-deploy/deployCheck.ts +6 -6
  32. package/src/4-dom/css.tsx +29 -29
  33. package/src/4-dom/cssTypes.d.ts +211 -211
  34. package/src/4-dom/qreact.tsx +2799 -2799
  35. package/src/4-dom/qreactTest.tsx +410 -410
  36. package/src/4-querysub/permissions.ts +335 -335
  37. package/src/4-querysub/querysubPrediction.ts +483 -483
  38. package/src/5-diagnostics/qreactDebug.tsx +346 -346
  39. package/src/TestController.ts +34 -34
  40. package/src/bits.ts +104 -104
  41. package/src/buffers.ts +69 -69
  42. package/src/diagnostics/ActionsHistory.ts +57 -57
  43. package/src/diagnostics/listenOnDebugger.ts +71 -71
  44. package/src/diagnostics/periodic.ts +111 -111
  45. package/src/diagnostics/trackResources.ts +91 -91
  46. package/src/diagnostics/watchdog.ts +120 -120
  47. package/src/errors.ts +133 -133
  48. package/src/forceProduction.ts +2 -2
  49. package/src/fs.ts +80 -80
  50. package/src/functional/diff.ts +857 -857
  51. package/src/functional/promiseCache.ts +78 -78
  52. package/src/functional/random.ts +8 -8
  53. package/src/functional/stats.ts +60 -60
  54. package/src/heapDumps.ts +665 -665
  55. package/src/https.ts +1 -1
  56. package/src/library-components/AspectSizedComponent.tsx +87 -87
  57. package/src/library-components/ButtonSelector.tsx +64 -64
  58. package/src/library-components/DropdownCustom.tsx +150 -150
  59. package/src/library-components/DropdownSelector.tsx +31 -31
  60. package/src/library-components/InlinePopup.tsx +66 -66
  61. package/src/misc/color.ts +29 -29
  62. package/src/misc/hash.ts +83 -83
  63. package/src/misc/ipPong.js +13 -13
  64. package/src/misc/networking.ts +1 -1
  65. package/src/misc/random.ts +44 -44
  66. package/src/misc.ts +196 -196
  67. package/src/path.ts +255 -255
  68. package/src/persistentLocalStore.ts +41 -41
  69. package/src/promise.ts +14 -14
  70. package/src/storage/fileSystemPointer.ts +71 -71
  71. package/src/test/heapProcess.ts +35 -35
  72. package/src/zip.ts +15 -15
  73. package/tsconfig.json +26 -26
  74. package/yarnSpec.txt +56 -56
@@ -1,336 +1,336 @@
1
- import { cache, cacheArgsEqual, cacheLimited } from "socket-function/src/caching";
2
- import { measureFnc, measureWrap, nameFunction } from "socket-function/src/profiling/measure";
3
- import { getPathSuffix, getPathDepth, trimPathStrToDepth, getPathFromStr, rootPathStr, getPathIndex, getPathStr1, joinPathStres, appendToPathStr, getPathStr } from "../path";
4
- import { atomic, atomicObjectRead, isSynced, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
5
- import { SchemaObject, getSchemaObject, hasWildcardMatch, getWildcardMatches, PERMISSIONS_FUNCTION_ID, PermissionsCheckResult, getDevelopmentModule } from "../3-path-functions/syncSchema";
6
- import { getModuleFromConfig } from "../3-path-functions/pathFunctionLoader";
7
- import { getSchemaPartsFromPath, functionSchema, DEPTH_TO_DATA, overrideCurrentCall, CallSpec, FunctionSpec, DOMAIN_INDEX, MODULE_INDEX } from "../3-path-functions/PathFunctionRunner";
8
- import debugbreak from "debugbreak";
9
- import { blue, red, yellow } from "socket-function/src/formatting/logColors";
10
- import { ignoreErrors } from "../errors";
11
- import { epochTime } from "../0-path-value-core/pathValueCore";
12
- import { isClient } from "../config2";
13
- import { sort } from "socket-function/src/misc";
14
- import { Querysub } from "./QuerysubController";
15
- import { CALL_PERMISSIONS_KEY } from "./permissionsShared";
16
- import { getDomain, isLocal, isPublic } from "../config";
17
-
18
- function watchModule(config: FunctionSpec): NodeJS.Module | undefined {
19
- let module = getModuleFromConfig(config);
20
- if (module && typeof module === "object" && module instanceof Promise) {
21
- // NOTE: The errors should throw correctly when proxyWatch is triggered (which will be immediately
22
- // after our ignore handler runs), because the underlying implementation should convert promises
23
- // to throws (or undefined, if errors are ignored).
24
- ignoreErrors(module);
25
- proxyWatcher.triggerOnPromiseFinish(module, { waitReason: "waitForModuleToLoad" });
26
- return undefined;
27
- }
28
- return module;
29
- }
30
-
31
- const callPermissionsPath = getPathStr1(CALL_PERMISSIONS_KEY);
32
-
33
- /** NOTE: This can only be used synchronously, and must be recreated after the current
34
- * synchronous code is finished.
35
- */
36
- export class PermissionsCheck {
37
- private dead = false;
38
- private static skippingChecks = false;
39
-
40
- public static DEBUG = false;
41
-
42
- constructor(private callerConfig: { callerMachineId: string; callerIP: string; }) {
43
- setImmediate(() => this.dead = true);
44
- }
45
-
46
- private perSchema = cacheArgsEqual((schema: SchemaObject, fnc: FunctionSpec) => {
47
- return new PermissionsCheckSchema(
48
- { machineID: this.callerConfig.callerMachineId, IP: this.callerConfig.callerIP },
49
- schema,
50
- fnc
51
- );
52
- });
53
-
54
- private getModulePermissions(domainName: string) {
55
- if (!isPublic() && domainName === getDomain()) {
56
- return function (moduleId: string): {
57
- schema: SchemaObject;
58
- fnc: FunctionSpec;
59
- } | undefined {
60
- let fnc: FunctionSpec = {
61
- DomainName: domainName,
62
- ModuleId: moduleId,
63
- FunctionId: PERMISSIONS_FUNCTION_ID,
64
- exportPathStr: callPermissionsPath,
65
- // NOTE: These SHOULDN'T be required, as we don't commit this function to the FunctionRunner,
66
- // we just use this to run the function on the local machine.
67
- FilePath: "LOCAL_PERMISSIONS_HACK",
68
- gitURL: "LOCAL_PERMISSIONS_HACK",
69
- gitRef: "LOCAL_PERMISSIONS_HACK",
70
- };
71
- let modulePermissions = getDevelopmentModule(moduleId);
72
- if (!modulePermissions) throw new Error(`No development module found for ${moduleId}. Was it imported? Try restarting the edge server.`);
73
- let schema = getSchemaObject(modulePermissions);
74
- if (!schema) throw new Error(`Module does not export a schema: ${moduleId}`);
75
- return {
76
- schema,
77
- fnc,
78
- };
79
- };
80
- }
81
- return this.getModulePermissionsBase(domainName);
82
- }
83
-
84
- private getModulePermissionsBase = cache((domainName: string) => {
85
- return cache((moduleId: string) => {
86
- let functionConfigProxy = PermissionsCheck.skipPermissionsChecks(() =>
87
- functionSchema()[domainName].PathFunctionRunner[moduleId].Sources[PERMISSIONS_FUNCTION_ID],
88
- );
89
- const functionConfig = PermissionsCheck.skipPermissionsChecks(() => atomic(functionConfigProxy));
90
- if (!functionConfig) {
91
- if (isSynced(functionConfigProxy)) {
92
- console.warn(red(`Missing path to permissions. You might need to rerun "yarn deploy", ensuring all schemas are imported in a file called "/deploy.ts". Path ${domainName}.${moduleId}.${PERMISSIONS_FUNCTION_ID}`));
93
- }
94
- return undefined;
95
- }
96
- let module: NodeJS.Module | undefined;
97
- try {
98
- module = watchModule(functionConfig);
99
- } catch (e: any) {
100
- console.warn(yellow(`Rejecting permissions due to error when loading module ${functionConfig.gitURL}#${functionConfig.gitRef}:${functionConfig.FilePath}\n${e.stack}`));
101
- return undefined;
102
- }
103
-
104
- if (!module) return undefined;
105
- let schema = getSchemaObject(module);
106
- if (!schema) return undefined;
107
- return {
108
- schema,
109
- fnc: functionConfig,
110
- };
111
- });
112
- });
113
-
114
- pathPartsCache = new Map<string, {
115
- domainName: string;
116
- moduleId: string;
117
- pathPrefix: string;
118
- rootKey: string;
119
- callExamplePath: string;
120
- emptyKeyPath: string;
121
- }>();
122
- // IMPORTANT! This function USED TO BE a major hotspot for function evaluation, and so is heavily optimized.
123
- public checkPermissions(path: string): { permissionsPath: string; allowed: boolean; } {
124
- if (this.dead) throw new Error("PermissionsCheck MUST be used synchronously, after which it cannot be reused");
125
- if (PermissionsCheck.skippingChecks) return { permissionsPath: rootPathStr, allowed: true };
126
-
127
- let pathPrefix = trimPathStrToDepth(path, DEPTH_TO_DATA);
128
- let pathParts = this.pathPartsCache.get(pathPrefix);
129
- if (!pathParts) {
130
- const domainName = getPathIndex(path, DOMAIN_INDEX) || "";
131
- const moduleId = getPathIndex(path, MODULE_INDEX) || "";
132
- let rootKey = getPathIndex(path, DEPTH_TO_DATA - 1) || "";
133
-
134
- pathParts = {
135
- domainName,
136
- moduleId,
137
- pathPrefix,
138
- rootKey,
139
- callExamplePath: appendToPathStr(pathPrefix, "ExampleCallId"),
140
- emptyKeyPath: appendToPathStr(pathPrefix, ""),
141
- };
142
- this.pathPartsCache.set(pathPrefix, pathParts);
143
- }
144
-
145
- if (!pathParts.domainName) return { permissionsPath: trimPathStrToDepth(path, DOMAIN_INDEX + 1), allowed: false };
146
- if (!pathParts.moduleId) return { permissionsPath: trimPathStrToDepth(path, MODULE_INDEX + 1), allowed: false };
147
- if (!pathParts.rootKey) return { permissionsPath: trimPathStrToDepth(path, DEPTH_TO_DATA), allowed: false };
148
-
149
- const schemaObj = this.getModulePermissions(pathParts.domainName)(pathParts.moduleId);
150
- if (!schemaObj) return { permissionsPath: trimPathStrToDepth(path, MODULE_INDEX + 1), allowed: false };
151
-
152
- let instance = this.perSchema(schemaObj.schema, schemaObj.fnc);
153
- instance.pathPrefix = pathPrefix;
154
-
155
- const rootKey = pathParts.rootKey;
156
-
157
- if (rootKey === "Module" || rootKey === "Sources") {
158
- return instance.checkPermissionsBase(callPermissionsPath);
159
- }
160
- if (rootKey === "Calls" || rootKey === "Results") {
161
- // Don't allow "" call to be read, as this would allow running Object.values() to read all calls.
162
- if (path === pathParts.emptyKeyPath) return { permissionsPath: path, allowed: false };
163
- // NOTE: A lot of the time, anyone will technically be able to read calls, as we don't inspect the call.
164
- // However... they would need to know the id, which will be very hard to guess.
165
- // - Also inspecting the call adds a lot of call overhead, so we would prefer not to do that.
166
- let result = instance.checkPermissionsBase(callPermissionsPath);
167
- return {
168
- // Make sure the path is specific, so when we are called again callId is not undefined
169
- permissionsPath: pathParts.callExamplePath,
170
- allowed: result.allowed
171
- };
172
- }
173
- if (rootKey === "Data") {
174
- let dataPath = getPathSuffix(path, DEPTH_TO_DATA);
175
- return instance.checkPermissionsBase(dataPath);
176
- }
177
- return { permissionsPath: path, allowed: false };
178
- }
179
-
180
- public static ignorePermissionsChecks = this.skipPermissionsChecks;
181
- public static skipPermissionsChecks<T>(code: () => T) {
182
- let prev = this.skippingChecks;
183
- this.skippingChecks = true;
184
- try {
185
- return code();
186
- } finally {
187
- this.skippingChecks = prev;
188
- }
189
- }
190
- }
191
- class PermissionsCheckSchema {
192
- public pathPrefix = rootPathStr;
193
-
194
- constructor(
195
- private callerConfig: { machineID: string; IP: string },
196
- private schema: SchemaObject,
197
- private fnc: FunctionSpec,
198
- ) { }
199
- private exampleCall: CallSpec = {
200
- callerMachineId: this.callerConfig.machineID,
201
- callerIP: this.callerConfig.IP,
202
- CallId: "",
203
- DomainName: "",
204
- FunctionId: "",
205
- ModuleId: "",
206
- runAtTime: epochTime,
207
- argsEncoded: "",
208
- };
209
-
210
- /** Converts a specific path to a more general path. All paths which map to this general path
211
- * will have identical permissions checks.
212
- */
213
- private getCachePermissionsPath(path: string): string {
214
- if (this.schema.permissionsNonWildcards.has(path)) {
215
- return path;
216
- }
217
- let depth = getPathDepth(path);
218
- for (let i = depth - 1; i > 0; i--) {
219
- let ancestorPath = trimPathStrToDepth(path, i);
220
- if (this.schema.permissionsNonWildcards.has(ancestorPath)) {
221
- return ancestorPath;
222
- }
223
- }
224
- if (this.schema.permissionsWildcards.length > 0) {
225
- let valuePath = getPathFromStr(path);
226
- // Using a tree would be algorithmically faster, but... might not even be faster in practice?
227
- // (at least for a small number of checks, which there should almost always be)
228
- for (let wildcardCheck of this.schema.permissionsWildcards) {
229
- if (hasWildcardMatch(wildcardCheck.pathParts, valuePath)) {
230
- return trimPathStrToDepth(path, wildcardCheck.pathParts.length);
231
- }
232
- }
233
- }
234
-
235
- return rootPathStr;
236
- }
237
-
238
- // NOTE: For our trivial "127.0.0.1" check this cache isn't needed. But permissions checks might be A LOT slower,
239
- // so it is important to cache it based on the permissionsPath.
240
- private checkCache = cacheLimited(1000 * 1000, measureWrap((permissionsPath: string) => {
241
- let checks = this.getChecks(permissionsPath);
242
- let allowed = overrideCurrentCall({
243
- spec: this.exampleCall,
244
- fnc: this.fnc,
245
- }, () => {
246
- // NOTE: If we read inside of a permissions check we don't want to ALSO check
247
- // permissions checks on those because:
248
- // 1) It might infinitely loop
249
- // 2) When checking permissions we need to be allowed to read all values!
250
- return PermissionsCheck.skipPermissionsChecks(() => {
251
- for (let check of checks) {
252
- try {
253
- let result = check();
254
- if (!result && PermissionsCheck.DEBUG) {
255
- let anyUnsynced = false;
256
- try {
257
- anyUnsynced = Querysub.anyUnsynced();
258
- } catch { }
259
- if (!anyUnsynced) {
260
- // NOTE: This gets loud, as it happens EVERY TIME we check permissions. We already have a check in QuerysubController,
261
- // which should be enough.
262
- //console.log(blue(`Denied permissions for ${joinPathStres(this.pathPrefix, permissionsPath)} due to check at ${(check as any).debugName}`));
263
- check();
264
- }
265
- }
266
- if (typeof result === "object") {
267
- if (!result.allowed) {
268
- return false;
269
- }
270
- if (result.skipParentChecks) {
271
- return result.allowed;
272
- }
273
- } else if (!result) {
274
- return false;
275
- }
276
- } catch {
277
- return false;
278
- }
279
- }
280
- return true;
281
- });
282
- });
283
- return { permissionsPath: joinPathStres(this.pathPrefix, permissionsPath), allowed };
284
- }, "permissionsCheckInsideCache"));
285
-
286
- public checkPermissionsBase(dataPath: string): { permissionsPath: string; allowed: boolean; } {
287
- let permissionsPath = this.getCachePermissionsPath(dataPath);
288
- return this.checkCache(permissionsPath);
289
- }
290
- // NOTE: We don't cache the result of a permissions check, because it might change while
291
- // a function is evaluating. If they need to be skipped for the purposes of speed (which is hard
292
- // to believe, considering all the other sources of overhead we have), skipPermissionsChecks
293
- // can be used to quickly skip them.
294
- private getChecks = cacheLimited(1000 * 1000, ((path: string): (() => PermissionsCheckResult)[] => {
295
- let checks: {
296
- check: () => PermissionsCheckResult;
297
- path: string;
298
- }[] = [];
299
- let depth = getPathDepth(path);
300
- for (let i = depth; i >= 0; i--) {
301
- let ancestorPath = trimPathStrToDepth(path, i);
302
- const permissions = this.schema.permissionsNonWildcards.get(ancestorPath);
303
- if (permissions) {
304
- let check = () => permissions({
305
- callerMachineId: this.callerConfig.machineID,
306
- matchedPath: ancestorPath,
307
- pathWildcards: [],
308
- });
309
- if (PermissionsCheck.DEBUG) {
310
- Object.assign(check, { debugName: ancestorPath });
311
- }
312
- checks.push({ check, path: ancestorPath });
313
- }
314
- }
315
- // Using a tree would be algorithmically faster, but... might not even be faster in practice?
316
- // (at least for a small number of checks, which there should almost always be)
317
- for (let wildcardCheck of this.schema.permissionsWildcards) {
318
- let matches = getWildcardMatches(wildcardCheck.pathParts, getPathFromStr(path));
319
- if (!matches) continue;
320
- let matchedPath = trimPathStrToDepth(path, wildcardCheck.pathParts.length);
321
- let check = () => wildcardCheck.callback({
322
- callerMachineId: this.callerConfig.machineID,
323
- matchedPath,
324
- pathWildcards: matches || [],
325
- });
326
- if (PermissionsCheck.DEBUG) {
327
- Object.assign(check, { debugName: wildcardCheck.pathParts.join(".") });
328
- }
329
- checks.push({ check, path: matchedPath });
330
- }
331
-
332
- sort(checks, x => -x.path.length);
333
-
334
- return checks.map(x => x.check);
335
- }));
1
+ import { cache, cacheArgsEqual, cacheLimited } from "socket-function/src/caching";
2
+ import { measureFnc, measureWrap, nameFunction } from "socket-function/src/profiling/measure";
3
+ import { getPathSuffix, getPathDepth, trimPathStrToDepth, getPathFromStr, rootPathStr, getPathIndex, getPathStr1, joinPathStres, appendToPathStr, getPathStr } from "../path";
4
+ import { atomic, atomicObjectRead, isSynced, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
5
+ import { SchemaObject, getSchemaObject, hasWildcardMatch, getWildcardMatches, PERMISSIONS_FUNCTION_ID, PermissionsCheckResult, getDevelopmentModule } from "../3-path-functions/syncSchema";
6
+ import { getModuleFromConfig } from "../3-path-functions/pathFunctionLoader";
7
+ import { getSchemaPartsFromPath, functionSchema, DEPTH_TO_DATA, overrideCurrentCall, CallSpec, FunctionSpec, DOMAIN_INDEX, MODULE_INDEX } from "../3-path-functions/PathFunctionRunner";
8
+ import debugbreak from "debugbreak";
9
+ import { blue, red, yellow } from "socket-function/src/formatting/logColors";
10
+ import { ignoreErrors } from "../errors";
11
+ import { epochTime } from "../0-path-value-core/pathValueCore";
12
+ import { isClient } from "../config2";
13
+ import { sort } from "socket-function/src/misc";
14
+ import { Querysub } from "./QuerysubController";
15
+ import { CALL_PERMISSIONS_KEY } from "./permissionsShared";
16
+ import { getDomain, isLocal, isPublic } from "../config";
17
+
18
+ function watchModule(config: FunctionSpec): NodeJS.Module | undefined {
19
+ let module = getModuleFromConfig(config);
20
+ if (module && typeof module === "object" && module instanceof Promise) {
21
+ // NOTE: The errors should throw correctly when proxyWatch is triggered (which will be immediately
22
+ // after our ignore handler runs), because the underlying implementation should convert promises
23
+ // to throws (or undefined, if errors are ignored).
24
+ ignoreErrors(module);
25
+ proxyWatcher.triggerOnPromiseFinish(module, { waitReason: "waitForModuleToLoad" });
26
+ return undefined;
27
+ }
28
+ return module;
29
+ }
30
+
31
+ const callPermissionsPath = getPathStr1(CALL_PERMISSIONS_KEY);
32
+
33
+ /** NOTE: This can only be used synchronously, and must be recreated after the current
34
+ * synchronous code is finished.
35
+ */
36
+ export class PermissionsCheck {
37
+ private dead = false;
38
+ private static skippingChecks = false;
39
+
40
+ public static DEBUG = false;
41
+
42
+ constructor(private callerConfig: { callerMachineId: string; callerIP: string; }) {
43
+ setImmediate(() => this.dead = true);
44
+ }
45
+
46
+ private perSchema = cacheArgsEqual((schema: SchemaObject, fnc: FunctionSpec) => {
47
+ return new PermissionsCheckSchema(
48
+ { machineID: this.callerConfig.callerMachineId, IP: this.callerConfig.callerIP },
49
+ schema,
50
+ fnc
51
+ );
52
+ });
53
+
54
+ private getModulePermissions(domainName: string) {
55
+ if (!isPublic() && domainName === getDomain()) {
56
+ return function (moduleId: string): {
57
+ schema: SchemaObject;
58
+ fnc: FunctionSpec;
59
+ } | undefined {
60
+ let fnc: FunctionSpec = {
61
+ DomainName: domainName,
62
+ ModuleId: moduleId,
63
+ FunctionId: PERMISSIONS_FUNCTION_ID,
64
+ exportPathStr: callPermissionsPath,
65
+ // NOTE: These SHOULDN'T be required, as we don't commit this function to the FunctionRunner,
66
+ // we just use this to run the function on the local machine.
67
+ FilePath: "LOCAL_PERMISSIONS_HACK",
68
+ gitURL: "LOCAL_PERMISSIONS_HACK",
69
+ gitRef: "LOCAL_PERMISSIONS_HACK",
70
+ };
71
+ let modulePermissions = getDevelopmentModule(moduleId);
72
+ if (!modulePermissions) throw new Error(`No development module found for ${moduleId}. Was it imported? Try restarting the edge server.`);
73
+ let schema = getSchemaObject(modulePermissions);
74
+ if (!schema) throw new Error(`Module does not export a schema: ${moduleId}`);
75
+ return {
76
+ schema,
77
+ fnc,
78
+ };
79
+ };
80
+ }
81
+ return this.getModulePermissionsBase(domainName);
82
+ }
83
+
84
+ private getModulePermissionsBase = cache((domainName: string) => {
85
+ return cache((moduleId: string) => {
86
+ let functionConfigProxy = PermissionsCheck.skipPermissionsChecks(() =>
87
+ functionSchema()[domainName].PathFunctionRunner[moduleId].Sources[PERMISSIONS_FUNCTION_ID],
88
+ );
89
+ const functionConfig = PermissionsCheck.skipPermissionsChecks(() => atomic(functionConfigProxy));
90
+ if (!functionConfig) {
91
+ if (isSynced(functionConfigProxy)) {
92
+ console.warn(red(`Missing path to permissions. You might need to rerun "yarn deploy", ensuring all schemas are imported in a file called "/deploy.ts". Path ${domainName}.${moduleId}.${PERMISSIONS_FUNCTION_ID}`));
93
+ }
94
+ return undefined;
95
+ }
96
+ let module: NodeJS.Module | undefined;
97
+ try {
98
+ module = watchModule(functionConfig);
99
+ } catch (e: any) {
100
+ console.warn(yellow(`Rejecting permissions due to error when loading module ${functionConfig.gitURL}#${functionConfig.gitRef}:${functionConfig.FilePath}\n${e.stack}`));
101
+ return undefined;
102
+ }
103
+
104
+ if (!module) return undefined;
105
+ let schema = getSchemaObject(module);
106
+ if (!schema) return undefined;
107
+ return {
108
+ schema,
109
+ fnc: functionConfig,
110
+ };
111
+ });
112
+ });
113
+
114
+ pathPartsCache = new Map<string, {
115
+ domainName: string;
116
+ moduleId: string;
117
+ pathPrefix: string;
118
+ rootKey: string;
119
+ callExamplePath: string;
120
+ emptyKeyPath: string;
121
+ }>();
122
+ // IMPORTANT! This function USED TO BE a major hotspot for function evaluation, and so is heavily optimized.
123
+ public checkPermissions(path: string): { permissionsPath: string; allowed: boolean; } {
124
+ if (this.dead) throw new Error("PermissionsCheck MUST be used synchronously, after which it cannot be reused");
125
+ if (PermissionsCheck.skippingChecks) return { permissionsPath: rootPathStr, allowed: true };
126
+
127
+ let pathPrefix = trimPathStrToDepth(path, DEPTH_TO_DATA);
128
+ let pathParts = this.pathPartsCache.get(pathPrefix);
129
+ if (!pathParts) {
130
+ const domainName = getPathIndex(path, DOMAIN_INDEX) || "";
131
+ const moduleId = getPathIndex(path, MODULE_INDEX) || "";
132
+ let rootKey = getPathIndex(path, DEPTH_TO_DATA - 1) || "";
133
+
134
+ pathParts = {
135
+ domainName,
136
+ moduleId,
137
+ pathPrefix,
138
+ rootKey,
139
+ callExamplePath: appendToPathStr(pathPrefix, "ExampleCallId"),
140
+ emptyKeyPath: appendToPathStr(pathPrefix, ""),
141
+ };
142
+ this.pathPartsCache.set(pathPrefix, pathParts);
143
+ }
144
+
145
+ if (!pathParts.domainName) return { permissionsPath: trimPathStrToDepth(path, DOMAIN_INDEX + 1), allowed: false };
146
+ if (!pathParts.moduleId) return { permissionsPath: trimPathStrToDepth(path, MODULE_INDEX + 1), allowed: false };
147
+ if (!pathParts.rootKey) return { permissionsPath: trimPathStrToDepth(path, DEPTH_TO_DATA), allowed: false };
148
+
149
+ const schemaObj = this.getModulePermissions(pathParts.domainName)(pathParts.moduleId);
150
+ if (!schemaObj) return { permissionsPath: trimPathStrToDepth(path, MODULE_INDEX + 1), allowed: false };
151
+
152
+ let instance = this.perSchema(schemaObj.schema, schemaObj.fnc);
153
+ instance.pathPrefix = pathPrefix;
154
+
155
+ const rootKey = pathParts.rootKey;
156
+
157
+ if (rootKey === "Module" || rootKey === "Sources") {
158
+ return instance.checkPermissionsBase(callPermissionsPath);
159
+ }
160
+ if (rootKey === "Calls" || rootKey === "Results") {
161
+ // Don't allow "" call to be read, as this would allow running Object.values() to read all calls.
162
+ if (path === pathParts.emptyKeyPath) return { permissionsPath: path, allowed: false };
163
+ // NOTE: A lot of the time, anyone will technically be able to read calls, as we don't inspect the call.
164
+ // However... they would need to know the id, which will be very hard to guess.
165
+ // - Also inspecting the call adds a lot of call overhead, so we would prefer not to do that.
166
+ let result = instance.checkPermissionsBase(callPermissionsPath);
167
+ return {
168
+ // Make sure the path is specific, so when we are called again callId is not undefined
169
+ permissionsPath: pathParts.callExamplePath,
170
+ allowed: result.allowed
171
+ };
172
+ }
173
+ if (rootKey === "Data") {
174
+ let dataPath = getPathSuffix(path, DEPTH_TO_DATA);
175
+ return instance.checkPermissionsBase(dataPath);
176
+ }
177
+ return { permissionsPath: path, allowed: false };
178
+ }
179
+
180
+ public static ignorePermissionsChecks = this.skipPermissionsChecks;
181
+ public static skipPermissionsChecks<T>(code: () => T) {
182
+ let prev = this.skippingChecks;
183
+ this.skippingChecks = true;
184
+ try {
185
+ return code();
186
+ } finally {
187
+ this.skippingChecks = prev;
188
+ }
189
+ }
190
+ }
191
+ class PermissionsCheckSchema {
192
+ public pathPrefix = rootPathStr;
193
+
194
+ constructor(
195
+ private callerConfig: { machineID: string; IP: string },
196
+ private schema: SchemaObject,
197
+ private fnc: FunctionSpec,
198
+ ) { }
199
+ private exampleCall: CallSpec = {
200
+ callerMachineId: this.callerConfig.machineID,
201
+ callerIP: this.callerConfig.IP,
202
+ CallId: "",
203
+ DomainName: "",
204
+ FunctionId: "",
205
+ ModuleId: "",
206
+ runAtTime: epochTime,
207
+ argsEncoded: "",
208
+ };
209
+
210
+ /** Converts a specific path to a more general path. All paths which map to this general path
211
+ * will have identical permissions checks.
212
+ */
213
+ private getCachePermissionsPath(path: string): string {
214
+ if (this.schema.permissionsNonWildcards.has(path)) {
215
+ return path;
216
+ }
217
+ let depth = getPathDepth(path);
218
+ for (let i = depth - 1; i > 0; i--) {
219
+ let ancestorPath = trimPathStrToDepth(path, i);
220
+ if (this.schema.permissionsNonWildcards.has(ancestorPath)) {
221
+ return ancestorPath;
222
+ }
223
+ }
224
+ if (this.schema.permissionsWildcards.length > 0) {
225
+ let valuePath = getPathFromStr(path);
226
+ // Using a tree would be algorithmically faster, but... might not even be faster in practice?
227
+ // (at least for a small number of checks, which there should almost always be)
228
+ for (let wildcardCheck of this.schema.permissionsWildcards) {
229
+ if (hasWildcardMatch(wildcardCheck.pathParts, valuePath)) {
230
+ return trimPathStrToDepth(path, wildcardCheck.pathParts.length);
231
+ }
232
+ }
233
+ }
234
+
235
+ return rootPathStr;
236
+ }
237
+
238
+ // NOTE: For our trivial "127.0.0.1" check this cache isn't needed. But permissions checks might be A LOT slower,
239
+ // so it is important to cache it based on the permissionsPath.
240
+ private checkCache = cacheLimited(1000 * 1000, measureWrap((permissionsPath: string) => {
241
+ let checks = this.getChecks(permissionsPath);
242
+ let allowed = overrideCurrentCall({
243
+ spec: this.exampleCall,
244
+ fnc: this.fnc,
245
+ }, () => {
246
+ // NOTE: If we read inside of a permissions check we don't want to ALSO check
247
+ // permissions checks on those because:
248
+ // 1) It might infinitely loop
249
+ // 2) When checking permissions we need to be allowed to read all values!
250
+ return PermissionsCheck.skipPermissionsChecks(() => {
251
+ for (let check of checks) {
252
+ try {
253
+ let result = check();
254
+ if (!result && PermissionsCheck.DEBUG) {
255
+ let anyUnsynced = false;
256
+ try {
257
+ anyUnsynced = Querysub.anyUnsynced();
258
+ } catch { }
259
+ if (!anyUnsynced) {
260
+ // NOTE: This gets loud, as it happens EVERY TIME we check permissions. We already have a check in QuerysubController,
261
+ // which should be enough.
262
+ //console.log(blue(`Denied permissions for ${joinPathStres(this.pathPrefix, permissionsPath)} due to check at ${(check as any).debugName}`));
263
+ check();
264
+ }
265
+ }
266
+ if (typeof result === "object") {
267
+ if (!result.allowed) {
268
+ return false;
269
+ }
270
+ if (result.skipParentChecks) {
271
+ return result.allowed;
272
+ }
273
+ } else if (!result) {
274
+ return false;
275
+ }
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+ return true;
281
+ });
282
+ });
283
+ return { permissionsPath: joinPathStres(this.pathPrefix, permissionsPath), allowed };
284
+ }, "permissionsCheckInsideCache"));
285
+
286
+ public checkPermissionsBase(dataPath: string): { permissionsPath: string; allowed: boolean; } {
287
+ let permissionsPath = this.getCachePermissionsPath(dataPath);
288
+ return this.checkCache(permissionsPath);
289
+ }
290
+ // NOTE: We don't cache the result of a permissions check, because it might change while
291
+ // a function is evaluating. If they need to be skipped for the purposes of speed (which is hard
292
+ // to believe, considering all the other sources of overhead we have), skipPermissionsChecks
293
+ // can be used to quickly skip them.
294
+ private getChecks = cacheLimited(1000 * 1000, ((path: string): (() => PermissionsCheckResult)[] => {
295
+ let checks: {
296
+ check: () => PermissionsCheckResult;
297
+ path: string;
298
+ }[] = [];
299
+ let depth = getPathDepth(path);
300
+ for (let i = depth; i >= 0; i--) {
301
+ let ancestorPath = trimPathStrToDepth(path, i);
302
+ const permissions = this.schema.permissionsNonWildcards.get(ancestorPath);
303
+ if (permissions) {
304
+ let check = () => permissions({
305
+ callerMachineId: this.callerConfig.machineID,
306
+ matchedPath: ancestorPath,
307
+ pathWildcards: [],
308
+ });
309
+ if (PermissionsCheck.DEBUG) {
310
+ Object.assign(check, { debugName: ancestorPath });
311
+ }
312
+ checks.push({ check, path: ancestorPath });
313
+ }
314
+ }
315
+ // Using a tree would be algorithmically faster, but... might not even be faster in practice?
316
+ // (at least for a small number of checks, which there should almost always be)
317
+ for (let wildcardCheck of this.schema.permissionsWildcards) {
318
+ let matches = getWildcardMatches(wildcardCheck.pathParts, getPathFromStr(path));
319
+ if (!matches) continue;
320
+ let matchedPath = trimPathStrToDepth(path, wildcardCheck.pathParts.length);
321
+ let check = () => wildcardCheck.callback({
322
+ callerMachineId: this.callerConfig.machineID,
323
+ matchedPath,
324
+ pathWildcards: matches || [],
325
+ });
326
+ if (PermissionsCheck.DEBUG) {
327
+ Object.assign(check, { debugName: wildcardCheck.pathParts.join(".") });
328
+ }
329
+ checks.push({ check, path: matchedPath });
330
+ }
331
+
332
+ sort(checks, x => -x.path.length);
333
+
334
+ return checks.map(x => x.check);
335
+ }));
336
336
  }