@harperfast/harper-pro 5.0.5 → 5.0.6

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 (107) hide show
  1. package/core/bin/status.js +2 -2
  2. package/core/bin/stop.js +5 -6
  3. package/core/components/EntryHandler.ts +4 -2
  4. package/core/components/Scope.ts +1 -1
  5. package/core/components/componentLoader.ts +11 -4
  6. package/core/components/requestRestart.ts +17 -2
  7. package/core/dataLayer/harperBridge/TableSizeObject.ts +35 -0
  8. package/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.ts +24 -0
  9. package/core/package-lock.json +1020 -101
  10. package/core/resources/DatabaseTransaction.ts +8 -3
  11. package/core/resources/Table.ts +12 -17
  12. package/core/resources/databases.ts +2 -2
  13. package/core/resources/graphql.ts +163 -165
  14. package/core/resources/indexes/HierarchicalNavigableSmallWorld.ts +14 -3
  15. package/core/resources/indexes/vector.ts +17 -0
  16. package/core/resources/loadEnv.ts +20 -16
  17. package/core/resources/login.ts +4 -3
  18. package/core/resources/roles.ts +60 -65
  19. package/core/security/auth.ts +15 -14
  20. package/core/security/jsLoader.ts +27 -9
  21. package/core/server/REST.ts +10 -11
  22. package/core/server/fastifyRoutes.ts +30 -29
  23. package/core/server/graphqlQuerying.ts +4 -3
  24. package/core/server/http.ts +175 -1
  25. package/core/server/mqtt.ts +8 -2
  26. package/core/server/serverHelpers/serverUtilities.ts +2 -5
  27. package/core/server/threads/threadServer.js +30 -2
  28. package/core/server/throttle.ts +18 -0
  29. package/core/utility/environment/environmentManager.js +10 -4
  30. package/core/utility/environment/systemInformation.ts +698 -0
  31. package/core/utility/hdbTerms.ts +1 -0
  32. package/core/utility/operation_authorization.js +2 -5
  33. package/dist/core/bin/status.js +2 -2
  34. package/dist/core/bin/status.js.map +1 -1
  35. package/dist/core/bin/stop.js +5 -5
  36. package/dist/core/bin/stop.js.map +1 -1
  37. package/dist/core/components/EntryHandler.js +4 -2
  38. package/dist/core/components/EntryHandler.js.map +1 -1
  39. package/dist/core/components/Scope.js +1 -1
  40. package/dist/core/components/Scope.js.map +1 -1
  41. package/dist/core/components/componentLoader.js +11 -3
  42. package/dist/core/components/componentLoader.js.map +1 -1
  43. package/dist/core/components/requestRestart.js +12 -1
  44. package/dist/core/components/requestRestart.js.map +1 -1
  45. package/dist/core/dataLayer/harperBridge/TableSizeObject.js +32 -0
  46. package/dist/core/dataLayer/harperBridge/TableSizeObject.js.map +1 -0
  47. package/dist/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.js +18 -19
  48. package/dist/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.js.map +1 -1
  49. package/dist/core/resources/DatabaseTransaction.js +6 -1
  50. package/dist/core/resources/DatabaseTransaction.js.map +1 -1
  51. package/dist/core/resources/Table.js +14 -18
  52. package/dist/core/resources/Table.js.map +1 -1
  53. package/dist/core/resources/databases.js +2 -1
  54. package/dist/core/resources/databases.js.map +1 -1
  55. package/dist/core/resources/graphql.js +176 -176
  56. package/dist/core/resources/graphql.js.map +1 -1
  57. package/dist/core/resources/indexes/HierarchicalNavigableSmallWorld.js +14 -2
  58. package/dist/core/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  59. package/dist/core/resources/indexes/vector.js +14 -0
  60. package/dist/core/resources/indexes/vector.js.map +1 -1
  61. package/dist/core/resources/loadEnv.js +20 -17
  62. package/dist/core/resources/loadEnv.js.map +1 -1
  63. package/dist/core/resources/login.js +4 -4
  64. package/dist/core/resources/login.js.map +1 -1
  65. package/dist/core/resources/roles.js +64 -68
  66. package/dist/core/resources/roles.js.map +1 -1
  67. package/dist/core/security/auth.js +17 -15
  68. package/dist/core/security/auth.js.map +1 -1
  69. package/dist/core/security/jsLoader.js +29 -9
  70. package/dist/core/security/jsLoader.js.map +1 -1
  71. package/dist/core/server/REST.js +11 -11
  72. package/dist/core/server/REST.js.map +1 -1
  73. package/dist/core/server/fastifyRoutes.js +30 -29
  74. package/dist/core/server/fastifyRoutes.js.map +1 -1
  75. package/dist/core/server/graphqlQuerying.js +5 -4
  76. package/dist/core/server/graphqlQuerying.js.map +1 -1
  77. package/dist/core/server/http.js +179 -0
  78. package/dist/core/server/http.js.map +1 -1
  79. package/dist/core/server/mqtt.js +5 -3
  80. package/dist/core/server/mqtt.js.map +1 -1
  81. package/dist/core/server/serverHelpers/serverUtilities.js +2 -2
  82. package/dist/core/server/serverHelpers/serverUtilities.js.map +1 -1
  83. package/dist/core/server/threads/threadServer.js +26 -2
  84. package/dist/core/server/threads/threadServer.js.map +1 -1
  85. package/dist/core/server/throttle.js +17 -0
  86. package/dist/core/server/throttle.js.map +1 -1
  87. package/dist/core/utility/environment/environmentManager.js +9 -4
  88. package/dist/core/utility/environment/environmentManager.js.map +1 -1
  89. package/dist/core/utility/environment/systemInformation.js +359 -219
  90. package/dist/core/utility/environment/systemInformation.js.map +1 -1
  91. package/dist/core/utility/hdbTerms.js +1 -0
  92. package/dist/core/utility/hdbTerms.js.map +1 -1
  93. package/dist/core/utility/operation_authorization.js +2 -2
  94. package/dist/core/utility/operation_authorization.js.map +1 -1
  95. package/dist/licensing/usageLicensing.js +1 -1
  96. package/dist/licensing/usageLicensing.js.map +1 -1
  97. package/licensing/usageLicensing.ts +1 -1
  98. package/npm-shrinkwrap.json +982 -62
  99. package/package.json +2 -1
  100. package/studio/web/assets/{index-D07pIqJt.js → index-qbLPhOzw.js} +2 -2
  101. package/studio/web/assets/{index-D07pIqJt.js.map → index-qbLPhOzw.js.map} +1 -1
  102. package/studio/web/index.html +1 -1
  103. package/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/TableSizeObject.js +0 -25
  104. package/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.js +0 -34
  105. package/core/utility/environment/systemInformation.js +0 -355
  106. package/dist/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/TableSizeObject.js +0 -24
  107. package/dist/core/dataLayer/harperBridge/lmdbBridge/lmdbUtility/TableSizeObject.js.map +0 -1
@@ -9,73 +9,72 @@ const USERS_NOT_DBS = ['super_user', 'structure_user'];
9
9
  * This is the component for handling role declarations in the Harper system. This will read roles.yaml for role
10
10
  * definitions and ensure that they are created in the system database.
11
11
  */
12
- // eslint-disable-next-line no-unused-vars
13
- export function start({ ensureTable }) {
14
- return {
15
- handleFile,
16
- setupFile: handleFile,
17
- };
12
+ export function handleApplication(scope: import('../components/Scope.ts').Scope) {
13
+ scope.handleEntry(async (entry) => {
14
+ if (entry.eventType === 'unlink') return;
15
+ return handleFile(entry.contents);
16
+ });
17
+ }
18
18
 
19
- /**
20
- * This function will handle the roles.yaml file content that has been read, and ensure that the roles are translated to
21
- * the right shape and created in the system database.
22
- * @param rolesContent
23
- */
24
- async function handleFile(rolesContent) {
25
- let rolesToDefine = parseDocument(rolesContent.toString(), { simpleKeys: true }).toJSON();
26
- for (let roleName in rolesToDefine) {
27
- let role = rolesToDefine[roleName];
28
- if (!role.permission) {
29
- // we allow the permission object to be collapsed into the root object for convenience
30
- role = {
31
- permission: role,
32
- };
33
- if (role.permission.access) {
34
- // this is the designed property object for user-defined flags and access levels
35
- role.access = role.permission.access;
36
- delete role.permission.access;
37
- }
19
+ /**
20
+ * This function will handle the roles.yaml file content that has been read, and ensure that the roles are translated to
21
+ * the right shape and created in the system database.
22
+ * @param rolesContent
23
+ */
24
+ async function handleFile(rolesContent) {
25
+ let rolesToDefine = parseDocument(rolesContent.toString(), { simpleKeys: true }).toJSON();
26
+ for (let roleName in rolesToDefine) {
27
+ let role = rolesToDefine[roleName];
28
+ if (!role.permission) {
29
+ // we allow the permission object to be collapsed into the root object for convenience
30
+ role = {
31
+ permission: role,
32
+ };
33
+ if (role.permission.access) {
34
+ // this is the designed property object for user-defined flags and access levels
35
+ role.access = role.permission.access;
36
+ delete role.permission.access;
38
37
  }
39
- for (let dbName in role.permission) {
40
- if (USERS_NOT_DBS.includes(dbName)) continue;
41
- let db = role.permission[dbName];
42
- if (!db.tables) {
43
- // we allow the tables object to be collapsed into the root object for convenience
44
- role.permission[dbName] = db = { tables: db };
45
- }
46
- for (let tableName in db.tables) {
47
- let table = db.tables[tableName];
48
- // ensure that all the flags are boolean
49
- table.read = Boolean(table.read);
50
- table.insert = Boolean(table.insert);
51
- table.update = Boolean(table.update);
52
- table.delete = Boolean(table.delete);
53
- if (table.attributes) {
54
- // allow attributes to be defined with an object, translating to an array
55
- let attributes = [];
56
- for (let attribute_name in table.attributes) {
57
- let attribute = table.attributes[attribute_name];
58
- attribute.attribute_name = attribute_name;
59
- attributes.push(attribute);
60
- }
61
- table.attribute_permissions = attributes;
62
- delete table.attributes;
38
+ }
39
+ for (let dbName in role.permission) {
40
+ if (USERS_NOT_DBS.includes(dbName)) continue;
41
+ let db = role.permission[dbName];
42
+ if (!db.tables) {
43
+ // we allow the tables object to be collapsed into the root object for convenience
44
+ role.permission[dbName] = db = { tables: db };
45
+ }
46
+ for (let tableName in db.tables) {
47
+ let table = db.tables[tableName];
48
+ // ensure that all the flags are boolean
49
+ table.read = Boolean(table.read);
50
+ table.insert = Boolean(table.insert);
51
+ table.update = Boolean(table.update);
52
+ table.delete = Boolean(table.delete);
53
+ if (table.attributes) {
54
+ // allow attributes to be defined with an object, translating to an array
55
+ let attributes = [];
56
+ for (let attribute_name in table.attributes) {
57
+ let attribute = table.attributes[attribute_name];
58
+ attribute.attribute_name = attribute_name;
59
+ attributes.push(attribute);
63
60
  }
64
- if (table.attribute_permissions) {
65
- if (!Array.isArray(table.attribute_permissions))
66
- throw new Error('attribute_permissions must be an array if defined');
67
- for (let attribute of table.attribute_permissions) {
68
- // ensure that all the flags are boolean
69
- attribute.read = Boolean(attribute.read);
70
- attribute.insert = Boolean(attribute.insert);
71
- attribute.update = Boolean(attribute.update);
72
- }
73
- } else table.attribute_permissions = null;
61
+ table.attribute_permissions = attributes;
62
+ delete table.attributes;
74
63
  }
64
+ if (table.attribute_permissions) {
65
+ if (!Array.isArray(table.attribute_permissions))
66
+ throw new Error('attribute_permissions must be an array if defined');
67
+ for (let attribute of table.attribute_permissions) {
68
+ // ensure that all the flags are boolean
69
+ attribute.read = Boolean(attribute.read);
70
+ attribute.insert = Boolean(attribute.insert);
71
+ attribute.update = Boolean(attribute.update);
72
+ }
73
+ } else table.attribute_permissions = null;
75
74
  }
76
- role.role = role.id = roleName;
77
- await ensureRole(role);
78
75
  }
76
+ role.role = role.id = roleName;
77
+ await ensureRole(role);
79
78
  }
80
79
  }
81
80
  async function ensureRole(role) {
@@ -92,7 +91,3 @@ async function ensureRole(role) {
92
91
  }
93
92
  return addRole(role);
94
93
  }
95
-
96
- // we can define these on the main thread
97
- export const startOnMainThread = start;
98
- // useful for testing
@@ -243,7 +243,9 @@ export async function authentication(request, nextHandler) {
243
243
  request.user = await server.getUser(session.user, null, request);
244
244
  } else if (
245
245
  (AUTHORIZE_LOCAL && (request.ip?.includes('127.0.0.') || request.ip == '::1')) ||
246
- (request?._nodeRequest?.socket?.server?._pipeName && request.ip === undefined) // allow socket domain
246
+ (request?._nodeRequest?.socket?.server?._pipeName &&
247
+ request?._nodeRequest?.socket?.server?.bypassLocalAuth &&
248
+ request.ip === undefined) // allow operations API domain socket
247
249
  ) {
248
250
  request.user = await getSuperUser();
249
251
  }
@@ -344,19 +346,18 @@ export async function authentication(request, nextHandler) {
344
346
  return response;
345
347
  }
346
348
  }
347
- let started;
348
- export function start({ server, port, securePort }) {
349
- server.http(authentication, port || securePort ? { port, securePort } : { port: 'all' });
350
- // keep it cleaned out periodically
351
- if (!started) {
352
- started = true;
353
- setInterval(() => {
354
- authorizationCache = new Map();
355
- }, env.get(CONFIG_PARAMS.AUTHENTICATION_CACHETTL)).unref();
356
- user.addListener(() => {
357
- authorizationCache = new Map();
358
- });
359
- }
349
+ setInterval(() => {
350
+ authorizationCache = new Map();
351
+ }, env.get(CONFIG_PARAMS.AUTHENTICATION_CACHETTL)).unref();
352
+ user.addListener(() => {
353
+ authorizationCache = new Map();
354
+ });
355
+ let started = false;
356
+ export function handleApplication(scope: import('../components/Scope.ts').Scope) {
357
+ if (started) return;
358
+ started = true;
359
+ const { port, securePort } = scope.options.getAll() as { port?: number; securePort?: number };
360
+ scope.server.http(authentication, port || securePort ? { port, securePort } : { port: 'all' });
360
361
  }
361
362
  // operations
362
363
  export async function login(loginObject) {
@@ -181,7 +181,13 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope, useC
181
181
  if (parts[0] === 'file:') {
182
182
  return specifier;
183
183
  }
184
- const resolved = createRequire(referrer).resolve(specifier);
184
+ let resolveReferrer = referrer;
185
+ if (referrer.startsWith('file:')) {
186
+ try {
187
+ resolveReferrer = pathToFileURL(realpathSync(fileURLToPath(referrer))).toString();
188
+ } catch {}
189
+ }
190
+ const resolved = createRequire(resolveReferrer).resolve(specifier);
185
191
  if (isAbsolute(resolved)) {
186
192
  return pathToFileURL(resolved).toString();
187
193
  }
@@ -206,18 +212,30 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope, useC
206
212
  cjsModule.exports = parseJsonModule(source, url);
207
213
  return cjsModule;
208
214
  }
209
- const require = createRequire(url);
215
+ let requireUrl = url;
216
+ if (url.startsWith('file://')) {
217
+ try {
218
+ requireUrl = pathToFileURL(realpathSync(fileURLToPath(url))).toString();
219
+ } catch {}
220
+ }
221
+ const require = createRequire(requireUrl);
210
222
 
211
223
  const cjsRequire = (spec: string) => {
212
- const resolvedPath = require.resolve(spec);
213
- if (isAbsolute(resolvedPath)) {
214
- const source = readFileSync(resolvedPath, { encoding: 'utf-8' });
215
- return loadCJS(resolvedPath, source).exports;
216
- } else {
217
- return require(spec);
224
+ const resolvedUrl = resolveModule(spec, url);
225
+ if (resolvedUrl === 'harper') {
226
+ return getHarperExports(scope);
218
227
  }
228
+ if (resolvedUrl.startsWith('file://')) {
229
+ const source = readFileSync(new URL(resolvedUrl), { encoding: 'utf-8' });
230
+ return loadCJS(resolvedUrl, source).exports;
231
+ }
232
+ return require(resolvedUrl);
233
+ };
234
+ cjsRequire.resolve = (spec: string) => {
235
+ const resolvedUrl = resolveModule(spec, url);
236
+ if (resolvedUrl.startsWith('file://')) return fileURLToPath(resolvedUrl);
237
+ return resolvedUrl;
219
238
  };
220
- cjsRequire.resolve = require.resolve;
221
239
 
222
240
  const cjsWrapper = `
223
241
  (function(module, exports, require, __filename, __dirname) {
@@ -1,7 +1,6 @@
1
1
  import { serialize, serializeMessage, getDeserializer } from '../server/serverHelpers/contentTypes.ts';
2
2
  import { addAnalyticsListener, recordAction, recordActionBinary } from '../resources/analytics/write.ts';
3
3
  import * as harperLogger from '../utility/logging/harper_logger.js';
4
- import { ServerOptions } from 'http';
5
4
  import { ServerError, ClientError } from '../utility/errors/hdbError.js';
6
5
  import { Resources } from '../resources/Resources.ts';
7
6
  import { Resource, missingMethod, allowedMethods } from '../resources/Resource.ts';
@@ -277,26 +276,26 @@ async function http(request: Context & Request, nextHandler) {
277
276
  }
278
277
  }
279
278
 
280
- let started;
279
+ let started = false;
281
280
  let resources: Resources;
282
281
  let addedMetrics;
283
282
  let connectionCount = 0;
284
283
 
285
- export function start(options: ServerOptions & { path: string; port: number; server: any; resources: Resources }) {
286
- httpOptions = options;
287
- if (options.includeExpensiveRecordCountEstimates) {
284
+ export function handleApplication(scope: import('../components/Scope.ts').Scope) {
285
+ httpOptions = scope.options.getAll();
286
+ if ((httpOptions as any).includeExpensiveRecordCountEstimates) {
288
287
  // If they really want to enable expensive record count estimates
289
288
  Request.prototype.includeExpensiveRecordCountEstimates = true;
290
289
  }
290
+ resources = scope.resources;
291
291
  if (started) return;
292
292
  started = true;
293
- resources = options.resources;
294
- options.server.http(async (request: Request, nextHandler) => {
293
+ scope.server.http(async (request: Request, nextHandler) => {
295
294
  if (request.isWebSocket) return;
296
295
  return http(request, nextHandler);
297
- }, options);
298
- if (options.webSocket === false) return;
299
- options.server.ws(async (ws, request, chainCompletion) => {
296
+ }, httpOptions);
297
+ if ((httpOptions as any).webSocket === false) return;
298
+ scope.server.ws(async (ws, request, chainCompletion) => {
300
299
  connectionCount++;
301
300
  const incomingMessages = new IterableEventQueue();
302
301
  if (!addedMetrics) {
@@ -382,7 +381,7 @@ export function start(options: ServerOptions & { path: string; port: number; ser
382
381
  );
383
382
  }
384
383
  ws.close();
385
- }, options);
384
+ }, httpOptions);
386
385
  }
387
386
  const HTTP_TO_WEBSOCKET_CLOSE_CODES = {
388
387
  401: 3000,
@@ -1,4 +1,4 @@
1
- import { dirname } from 'path';
1
+ import { dirname, basename } from 'path';
2
2
  import { existsSync } from 'fs';
3
3
  import fastify from 'fastify';
4
4
  import fastifyCors from '@fastify/cors';
@@ -32,35 +32,36 @@ const routeFolders = new Set();
32
32
  * @param filePath
33
33
  * @param projectName
34
34
  */
35
- export function start(options) {
35
+ export function handleApplication(scope: import('../components/Scope.ts').Scope) {
36
36
  // if we have a secure port, need to use the secure HTTP server for fastify (it can be used for HTTP as well)
37
- const isHttps = options.securePort > 0;
38
- return {
39
- // eslint-disable-next-line no-unused-vars
40
- async handleFile(jsContent, relativePath, filePath, projectName) {
41
- if (!fastifyServer) {
42
- fastifyServer = buildServer(isHttps);
43
- server.http((await fastifyServer).server);
44
- }
45
- const resolvedServer = await fastifyServer;
46
- const routeFolder = dirname(filePath);
47
- let prefix = dirname(relativePath);
48
- if (prefix.startsWith('/')) prefix = prefix.slice(1);
49
- if (!routeFolders.has(routeFolder)) {
50
- routeFolders.add(routeFolder);
51
- try {
52
- resolvedServer.register(buildRouteFolder(routeFolder, prefix));
53
- } catch (error) {
54
- if (error.message === 'Root plugin has already booted')
55
- harperLogger.warn(
56
- `Could not load root fastify route for ${filePath}, this may require a restart to install properly`
57
- );
58
- else throw error;
59
- }
37
+ const isHttps = (scope.options.getAll() as { securePort?: number }).securePort > 0;
38
+ scope.handleEntry(async (entry) => {
39
+ if (entry.eventType !== 'add') {
40
+ scope.requestRestart();
41
+ return;
42
+ }
43
+ if (!fastifyServer) {
44
+ fastifyServer = buildServer(isHttps);
45
+ server.http((await fastifyServer).server);
46
+ }
47
+ const resolvedServer = await fastifyServer;
48
+ const routeFolder = dirname(entry.absolutePath);
49
+ let prefix = basename(scope.appName);
50
+ if (prefix.startsWith('/')) prefix = prefix.slice(1);
51
+ if (!routeFolders.has(routeFolder)) {
52
+ routeFolders.add(routeFolder);
53
+ try {
54
+ resolvedServer.register(buildRouteFolder(routeFolder, prefix));
55
+ } catch (error) {
56
+ if (error.message === 'Root plugin has already booted')
57
+ harperLogger.warn(
58
+ `Could not load root fastify route for ${entry.absolutePath}, this may require a restart to install properly`
59
+ );
60
+ else throw error;
60
61
  }
61
- },
62
- ready,
63
- };
62
+ }
63
+ await ready();
64
+ });
64
65
  }
65
66
  /**
66
67
  * Function called to start up server instance on a forked process - this method is called from customFunctionServer after process is
@@ -118,7 +119,7 @@ async function setUp() {
118
119
  }
119
120
  }
120
121
 
121
- // eslint-disable-next-line require-await
122
+ //
122
123
  function buildRouteFolder(routesFolder, projectName) {
123
124
  return async function (cfServer) {
124
125
  try {
@@ -570,8 +570,9 @@ async function graphqlQueryingHandler(request: Request) {
570
570
  }
571
571
  }
572
572
 
573
- export function start(options) {
574
- options.server.http(
573
+ export function handleApplication(scope: import('../components/Scope.ts').Scope) {
574
+ const { port, securePort } = scope.options.getAll() as { port?: number; securePort?: number };
575
+ scope.server.http(
575
576
  async (request, nextLayer) => {
576
577
  if (!request.url.startsWith('/graphql')) {
577
578
  return nextLayer(request);
@@ -695,6 +696,6 @@ export function start(options) {
695
696
  throw error;
696
697
  }
697
698
  },
698
- { port: options.port, securePort: options.securePort }
699
+ { port, securePort }
699
700
  );
700
701
  }
@@ -2,6 +2,7 @@
2
2
  * This module represents the HTTP component for Harper, and receives the HTTP options and uses them to configure
3
3
  * HTTP servers
4
4
  */
5
+ import { currentThreadId } from '@harperfast/rocksdb-js';
5
6
  import { Scope } from '../components/Scope.ts';
6
7
  import { Socket } from 'node:net';
7
8
  import harperLogger from '../utility/logging/harper_logger.js';
@@ -9,7 +10,7 @@ import { parentPort } from 'node:worker_threads';
9
10
  import env from '../utility/environment/environmentManager.js';
10
11
  import * as terms from '../utility/hdbTerms.ts';
11
12
  import { getConfigPath } from '../config/configUtils.js';
12
- import { getTicketKeys } from './threads/manageThreads.js';
13
+ import { getTicketKeys, getWorkerIndex } from './threads/manageThreads.js';
13
14
  import { createTLSSelector } from '../security/keys.js';
14
15
  import { createSecureServer } from 'node:http2';
15
16
  import { createServer as createSecureServerHttp1 } from 'node:https';
@@ -19,6 +20,8 @@ import { appendHeader, Headers } from './serverHelpers/Headers.ts';
19
20
  import { Blob } from '../resources/blob.ts';
20
21
  import { recordAction, recordActionBinary } from '../resources/analytics/write.ts';
21
22
  import { Readable } from 'node:stream';
23
+ import { mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
24
+ import { join } from 'node:path';
22
25
  import { server, type ServerOptions, type HttpOptions, type UpgradeOptions, UpgradeListener } from './Server.ts';
23
26
  import { setPortServerMap, SERVERS } from './serverRegistry.ts';
24
27
  import { getComponentName } from '../components/componentLoader.ts';
@@ -36,6 +39,77 @@ const httpServers = {},
36
39
  httpResponders = [];
37
40
  let httpOptions: HttpOptions = {};
38
41
  export const universalHeaders: [string, string][] = [];
42
+ const udsCleanupPaths: { socketPath: string; yamlPath: string }[] = [];
43
+
44
+ export function registerUdsCleanupPaths(socketPath: string, yamlPath: string) {
45
+ udsCleanupPaths.push({ socketPath, yamlPath });
46
+ }
47
+
48
+ export function cleanupUdsFiles() {
49
+ for (const { socketPath, yamlPath } of udsCleanupPaths) {
50
+ try {
51
+ unlinkSync(socketPath);
52
+ } catch {}
53
+ try {
54
+ unlinkSync(yamlPath);
55
+ } catch {}
56
+ }
57
+ }
58
+
59
+ /** Write YAML metadata for a UDS mirror socket, describing the TLS certs from the corresponding secure server. */
60
+ export function writeUdsMetadata(yamlPath: string, port: number | string, secureServer: any) {
61
+ const contexts = secureServer.secureContexts;
62
+ let yaml = `pid: ${process.pid}\ntid: ${currentThreadId()}\nport: ${port}\n`;
63
+ yaml += `certificates:\n`;
64
+ if (contexts?.size > 0) {
65
+ const seen = new Set();
66
+ for (const [, ctx] of contexts) {
67
+ if (seen.has(ctx.name)) continue;
68
+ seen.add(ctx.name);
69
+ yaml += ` - name: ${JSON.stringify(ctx.name)}\n`;
70
+ yaml += ` hostnames:\n`;
71
+ for (const [h, c] of contexts) {
72
+ if (c.name === ctx.name) yaml += ` - ${JSON.stringify(h)}\n`;
73
+ }
74
+ if (ctx.options.key_file) {
75
+ yaml += ` privateKeyFile: ${JSON.stringify(join(env.get(terms.CONFIG_PARAMS.ROOTPATH), 'keys', ctx.options.key_file))}\n`;
76
+ }
77
+ if (ctx.options.cert) {
78
+ yaml += ` certificate: |\n`;
79
+ for (const line of ctx.options.cert.trimEnd().split('\n')) {
80
+ yaml += ` ${line}\n`;
81
+ }
82
+ }
83
+ if (ctx.certificateAuthorities?.length > 0) {
84
+ yaml += ` certificateAuthorities:\n`;
85
+ for (const [, ca] of ctx.certificateAuthorities) {
86
+ yaml += ` - |\n`;
87
+ for (const line of ca.trimEnd().split('\n')) {
88
+ yaml += ` ${line}\n`;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ try {
95
+ writeFileSync(yamlPath, yaml);
96
+ } catch (error) {
97
+ harperLogger.error('Error writing UDS metadata to ' + yamlPath, error);
98
+ }
99
+ }
100
+
101
+ /** Clean all files in the sockets directory. Call from main thread on process startup. */
102
+ export function cleanupSocketsDirectory() {
103
+ if (!env.get(terms.CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) return;
104
+ const socketsDir = join(env.getHdbBasePath(), 'sockets');
105
+ try {
106
+ for (const file of readdirSync(socketsDir)) {
107
+ try {
108
+ unlinkSync(join(socketsDir, file));
109
+ } catch {}
110
+ }
111
+ } catch {}
112
+ }
39
113
 
40
114
  export function handleApplication(scope: Scope) {
41
115
  httpOptions = scope.options.getAll() as HttpOptions;
@@ -443,6 +517,46 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
443
517
  server.isSecure = true;
444
518
  }
445
519
  registerServer(server, port);
520
+
521
+ // Operations API domain socket connections bypass auth (equivalent to local access)
522
+ if (isOperationsServer && String(port).includes('/')) server.bypassLocalAuth = true;
523
+
524
+ // Create a corresponding Unix Domain Socket mirror for secure ports
525
+ if (secure && env.get(terms.CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) {
526
+ const socketsDir = join(env.getHdbBasePath(), 'sockets');
527
+ mkdirSync(socketsDir, { recursive: true });
528
+ const socketName = `${getWorkerIndex()}-${port}`;
529
+ const udsPath = join(socketsDir, `${socketName}.sock`);
530
+ const yamlPath = join(socketsDir, `${socketName}.yaml`);
531
+
532
+ // Create a plain HTTP server (no TLS) with the same request handler
533
+ const udsServer = createServer(
534
+ {
535
+ keepAliveTimeout,
536
+ headersTimeout,
537
+ requestTimeout,
538
+ highWaterMark: 128 * 1024,
539
+ noDelay: true,
540
+ keepAlive: true,
541
+ keepAliveInitialDelay: 600,
542
+ maxHeaderSize: env.get(terms.CONFIG_PARAMS.HTTP_MAXHEADERSIZE),
543
+ },
544
+ (nodeRequest: IncomingMessage, nodeResponse: any) => {
545
+ const method = nodeRequest.method;
546
+ if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD') requestHandler(nodeRequest, nodeResponse);
547
+ else throttledRequestHandler(nodeRequest, nodeResponse);
548
+ }
549
+ );
550
+
551
+ udsServer.isPerThreadSocket = true;
552
+ enableProxyProtocol(udsServer);
553
+ SERVERS[udsPath] = udsServer;
554
+ registerUdsCleanupPaths(udsPath, yamlPath);
555
+
556
+ const writeMetadata = () => writeUdsMetadata(yamlPath, port, server);
557
+ options.SNICallback.ready.then(writeMetadata);
558
+ server.secureContextsListeners.push(writeMetadata);
559
+ }
446
560
  }
447
561
  return httpServers[port];
448
562
  }
@@ -584,6 +698,66 @@ function onWebSocket(listener: (ws: WebSocket) => void, options: OnWebSocketOpti
584
698
  return servers;
585
699
  }
586
700
 
701
+ // PROXY protocol v1 max header length per spec: 108 bytes
702
+ const PROXY_V1_MAX_HEADER = 108;
703
+
704
+ function enableProxyProtocol(httpServer) {
705
+ // In Node.js v24+, the HTTP parser's data path goes through the C++ stream layer
706
+ // and does not call socket.emit('data') via JavaScript method dispatch.
707
+ // Overriding socket.emit or socket.push has no effect on the HTTP parser's data intake.
708
+ //
709
+ // Instead: use process.nextTick inside the 'connection' handler to wrap the HTTP
710
+ // parser's 'data' listener after it has been registered (synchronously, by the HTTP
711
+ // parser's own 'connection' handler which runs right after ours).
712
+ // process.nextTick fires before any I/O callbacks, so it is guaranteed to run before
713
+ // the first network data chunk reaches the socket — making the interception race-free.
714
+ httpServer.prependListener('connection', (socket) => {
715
+ process.nextTick(() => {
716
+ // Capture the HTTP parser's 'data' listener(s) registered during this connection event.
717
+ const dataListeners = socket.listeners('data') as ((chunk: Buffer) => void)[];
718
+ if (dataListeners.length === 0) return;
719
+ socket.removeAllListeners('data');
720
+
721
+ let proxyDone = false;
722
+ socket.on('data', (chunk: Buffer) => {
723
+ if (!proxyDone) {
724
+ proxyDone = true;
725
+ // Fast path: PROXY v1 always starts with "PROXY " (0x50 0x52 0x4f 0x58 0x59 0x20)
726
+ if (
727
+ chunk.length >= 6 &&
728
+ chunk[0] === 0x50 &&
729
+ chunk[1] === 0x52 &&
730
+ chunk[2] === 0x4f &&
731
+ chunk[3] === 0x58 &&
732
+ chunk[4] === 0x59 &&
733
+ chunk[5] === 0x20
734
+ ) {
735
+ const header = chunk.toString('latin1', 0, Math.min(PROXY_V1_MAX_HEADER, chunk.length));
736
+ const eol = header.indexOf('\r\n');
737
+ if (eol !== -1) {
738
+ // "PROXY TCP4 <src-ip> <dst-ip> <src-port> <dst-port>"
739
+ const parts = header.slice(0, eol).split(' ');
740
+ if (parts.length === 6) {
741
+ // Override the UDS socket's undefined remoteAddress/remotePort with the real client values.
742
+ Object.defineProperty(socket, 'remoteAddress', { value: parts[2], configurable: true });
743
+ Object.defineProperty(socket, 'remotePort', { value: parseInt(parts[4], 10), configurable: true });
744
+ }
745
+ // Forward only the bytes after the PROXY header to the HTTP parser.
746
+ const rest = chunk.subarray(eol + 2);
747
+ if (rest.length > 0) {
748
+ for (const listener of dataListeners) listener.call(socket, rest);
749
+ }
750
+ return;
751
+ }
752
+ }
753
+ }
754
+ // Not a PROXY header (or already handled) — forward unchanged.
755
+ for (const listener of dataListeners) listener.call(socket, chunk);
756
+ });
757
+ });
758
+ });
759
+ }
760
+
587
761
  function defaultNotFound(request, response) {
588
762
  if (response.headersSent || response.writableEnded) return;
589
763
  response.writeHead(404);
@@ -23,7 +23,14 @@ export function bypassAuth() {
23
23
  const authorizeLocal = (remoteAddress: string) =>
24
24
  AUTHORIZE_LOCAL && (remoteAddress.includes('127.0.0.') || remoteAddress === '::1');
25
25
 
26
- export function start({ server, port, network, webSocket, securePort, requireAuthentication }) {
26
+ export function handleApplication(scope: import('../components/Scope.ts').Scope) {
27
+ const { network, webSocket, requireAuthentication } = scope.options.getAll() as {
28
+ network?: any;
29
+ webSocket?: any;
30
+ requireAuthentication?: boolean;
31
+ };
32
+ const server = scope.server;
33
+ const { port, securePort } = network ?? {};
27
34
  // here we basically normalize the different types of sockets to pass to our socket/message handler
28
35
  if (!server.mqtt) {
29
36
  server.mqtt = {
@@ -160,7 +167,6 @@ export function start({ server, port, network, webSocket, securePort, requireAut
160
167
  )
161
168
  );
162
169
  }
163
- return serverInstances;
164
170
  }
165
171
  let addingMetrics,
166
172
  numberOfConnections = 0;
@@ -19,7 +19,7 @@ import restart from '../../bin/restart.js';
19
19
  import * as util from 'util';
20
20
  import insert from '../../dataLayer/insert.js';
21
21
  import globalSchema from '../../utility/globalSchema.js';
22
- import systemInformation from '../../utility/environment/systemInformation.js';
22
+ import { systemInformation } from '../../utility/environment/systemInformation.ts';
23
23
  import jobRunner from '../jobs/jobRunner.js';
24
24
  import * as tokenAuthentication from '../../security/tokenAuthentication.ts';
25
25
  import * as auth from '../../security/auth.ts';
@@ -363,10 +363,7 @@ function initializeOperationFunctionMap(): Map<OperationFunctionName, OperationF
363
363
  opFuncMap.set(terms.OPERATIONS_ENUM.RESTART, new OperationFunctionObject(restart.restart));
364
364
  opFuncMap.set(terms.OPERATIONS_ENUM.RESTART_SERVICE, new OperationFunctionObject(executeJob, restart.restartService));
365
365
  opFuncMap.set(terms.OPERATIONS_ENUM.CATCHUP, new OperationFunctionObject(catchup));
366
- opFuncMap.set(
367
- terms.OPERATIONS_ENUM.SYSTEM_INFORMATION,
368
- new OperationFunctionObject(systemInformation.systemInformation)
369
- );
366
+ opFuncMap.set(terms.OPERATIONS_ENUM.SYSTEM_INFORMATION, new OperationFunctionObject(systemInformation));
370
367
  opFuncMap.set(
371
368
  terms.OPERATIONS_ENUM.DELETE_AUDIT_LOGS_BEFORE,
372
369
  new OperationFunctionObject(executeJob, delete_.deleteAuditLogsBefore)