querysub 0.437.0 → 0.438.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 (73) 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/archivesDisk.ts +454 -454
  11. package/src/-a-auth/certs.ts +540 -540
  12. package/src/-a-auth/node-forge-ed25519.d.ts +16 -16
  13. package/src/-b-authorities/dnsAuthority.ts +138 -138
  14. package/src/-c-identity/IdentityController.ts +258 -258
  15. package/src/-d-trust/NetworkTrust2.ts +180 -180
  16. package/src/-e-certs/EdgeCertController.ts +252 -252
  17. package/src/-e-certs/certAuthority.ts +201 -201
  18. package/src/-f-node-discovery/NodeDiscovery.ts +640 -640
  19. package/src/-g-core-values/NodeCapabilities.ts +200 -200
  20. package/src/-h-path-value-serialize/stringSerializer.ts +175 -175
  21. package/src/0-path-value-core/PathValueCommitter.ts +468 -468
  22. package/src/0-path-value-core/PathValueController.ts +0 -2
  23. package/src/2-proxy/PathValueProxyWatcher.ts +2542 -2542
  24. package/src/2-proxy/TransactionDelayer.ts +94 -94
  25. package/src/2-proxy/pathDatabaseProxyBase.ts +36 -36
  26. package/src/2-proxy/pathValueProxy.ts +159 -159
  27. package/src/3-path-functions/PathFunctionRunnerMain.ts +87 -87
  28. package/src/3-path-functions/pathFunctionLoader.ts +516 -516
  29. package/src/3-path-functions/tests/rejectTest.ts +76 -76
  30. package/src/4-deploy/deployCheck.ts +6 -6
  31. package/src/4-dom/css.tsx +29 -29
  32. package/src/4-dom/cssTypes.d.ts +211 -211
  33. package/src/4-dom/qreact.tsx +2799 -2799
  34. package/src/4-dom/qreactTest.tsx +410 -410
  35. package/src/4-querysub/permissions.ts +335 -335
  36. package/src/4-querysub/querysubPrediction.ts +483 -483
  37. package/src/5-diagnostics/qreactDebug.tsx +377 -346
  38. package/src/TestController.ts +34 -34
  39. package/src/bits.ts +104 -104
  40. package/src/buffers.ts +69 -69
  41. package/src/diagnostics/ActionsHistory.ts +57 -57
  42. package/src/diagnostics/listenOnDebugger.ts +71 -71
  43. package/src/diagnostics/periodic.ts +111 -111
  44. package/src/diagnostics/trackResources.ts +91 -91
  45. package/src/diagnostics/watchdog.ts +120 -120
  46. package/src/errors.ts +133 -133
  47. package/src/forceProduction.ts +2 -2
  48. package/src/fs.ts +80 -80
  49. package/src/functional/diff.ts +857 -857
  50. package/src/functional/promiseCache.ts +78 -78
  51. package/src/functional/random.ts +8 -8
  52. package/src/functional/stats.ts +60 -60
  53. package/src/heapDumps.ts +665 -665
  54. package/src/https.ts +1 -1
  55. package/src/library-components/AspectSizedComponent.tsx +87 -87
  56. package/src/library-components/ButtonSelector.tsx +64 -64
  57. package/src/library-components/DropdownCustom.tsx +150 -150
  58. package/src/library-components/DropdownSelector.tsx +31 -31
  59. package/src/library-components/InlinePopup.tsx +66 -66
  60. package/src/misc/color.ts +29 -29
  61. package/src/misc/hash.ts +83 -83
  62. package/src/misc/ipPong.js +13 -13
  63. package/src/misc/networking.ts +1 -1
  64. package/src/misc/random.ts +44 -44
  65. package/src/misc.ts +196 -196
  66. package/src/path.ts +255 -255
  67. package/src/persistentLocalStore.ts +41 -41
  68. package/src/promise.ts +14 -14
  69. package/src/storage/fileSystemPointer.ts +71 -71
  70. package/src/test/heapProcess.ts +35 -35
  71. package/src/zip.ts +15 -15
  72. package/tsconfig.json +26 -26
  73. 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
  }