@harperfast/harper 5.2.0-beta.4 → 5.2.1

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 (320) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/copyDb.ts +21 -4
  4. package/bin/harper.ts +43 -55
  5. package/bin/help.ts +216 -0
  6. package/components/Application.ts +236 -46
  7. package/components/ApplicationScope.ts +26 -0
  8. package/components/EntryHandler.ts +410 -105
  9. package/components/RuntimeModuleTracker.ts +189 -0
  10. package/components/Scope.ts +120 -30
  11. package/components/componentLoader.ts +177 -25
  12. package/components/deployLifecycle.ts +119 -33
  13. package/components/mcp/toolRegistry.ts +10 -0
  14. package/components/mcp/tools/application.ts +12 -5
  15. package/components/mcp/tools/operations.ts +9 -0
  16. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  17. package/components/mcp/tools/schemas/operations.ts +9 -0
  18. package/components/operations.js +5 -6
  19. package/components/operationsValidation.js +32 -2
  20. package/components/scopeMount.ts +150 -0
  21. package/config/configUtils.ts +12 -9
  22. package/config-root.schema.json +14 -0
  23. package/dataLayer/backupManifest.ts +102 -0
  24. package/dataLayer/blobBackup.ts +286 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
  26. package/dataLayer/hdbInfoController.ts +8 -0
  27. package/dataLayer/restoreMarker.ts +276 -0
  28. package/dataLayer/rocksdbBackup.ts +1100 -0
  29. package/dataLayer/schemaDescribe.ts +2 -1
  30. package/dist/bin/backup.d.ts +9 -0
  31. package/dist/bin/backup.js +192 -0
  32. package/dist/bin/backup.js.map +1 -0
  33. package/dist/bin/cliOperations.d.ts +13 -0
  34. package/dist/bin/cliOperations.js +89 -70
  35. package/dist/bin/cliOperations.js.map +1 -1
  36. package/dist/bin/copyDb.js +13 -2
  37. package/dist/bin/copyDb.js.map +1 -1
  38. package/dist/bin/harper.d.ts +13 -0
  39. package/dist/bin/harper.js +45 -56
  40. package/dist/bin/harper.js.map +1 -1
  41. package/dist/bin/help.d.ts +8 -0
  42. package/dist/bin/help.js +192 -0
  43. package/dist/bin/help.js.map +1 -0
  44. package/dist/components/Application.d.ts +16 -1
  45. package/dist/components/Application.js +210 -38
  46. package/dist/components/Application.js.map +1 -1
  47. package/dist/components/ApplicationScope.d.ts +7 -0
  48. package/dist/components/ApplicationScope.js +22 -0
  49. package/dist/components/ApplicationScope.js.map +1 -1
  50. package/dist/components/EntryHandler.d.ts +4 -4
  51. package/dist/components/EntryHandler.js +386 -95
  52. package/dist/components/EntryHandler.js.map +1 -1
  53. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  54. package/dist/components/RuntimeModuleTracker.js +189 -0
  55. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  56. package/dist/components/Scope.d.ts +38 -1
  57. package/dist/components/Scope.js +117 -28
  58. package/dist/components/Scope.js.map +1 -1
  59. package/dist/components/componentLoader.d.ts +2 -6
  60. package/dist/components/componentLoader.js +160 -18
  61. package/dist/components/componentLoader.js.map +1 -1
  62. package/dist/components/deployLifecycle.d.ts +6 -2
  63. package/dist/components/deployLifecycle.js +109 -31
  64. package/dist/components/deployLifecycle.js.map +1 -1
  65. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  66. package/dist/components/mcp/toolRegistry.js +10 -0
  67. package/dist/components/mcp/toolRegistry.js.map +1 -1
  68. package/dist/components/mcp/tools/application.js +11 -5
  69. package/dist/components/mcp/tools/application.js.map +1 -1
  70. package/dist/components/mcp/tools/operations.js +9 -0
  71. package/dist/components/mcp/tools/operations.js.map +1 -1
  72. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  73. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  74. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  75. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  76. package/dist/components/operations.js +6 -6
  77. package/dist/components/operations.js.map +1 -1
  78. package/dist/components/operationsValidation.js +34 -2
  79. package/dist/components/operationsValidation.js.map +1 -1
  80. package/dist/components/scopeMount.d.ts +86 -0
  81. package/dist/components/scopeMount.js +131 -0
  82. package/dist/components/scopeMount.js.map +1 -0
  83. package/dist/config/configUtils.js +13 -9
  84. package/dist/config/configUtils.js.map +1 -1
  85. package/dist/dataLayer/backupManifest.d.ts +26 -0
  86. package/dist/dataLayer/backupManifest.js +97 -0
  87. package/dist/dataLayer/backupManifest.js.map +1 -0
  88. package/dist/dataLayer/blobBackup.d.ts +87 -0
  89. package/dist/dataLayer/blobBackup.js +282 -0
  90. package/dist/dataLayer/blobBackup.js.map +1 -0
  91. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  92. package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
  93. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  94. package/dist/dataLayer/hdbInfoController.js +4 -0
  95. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  96. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  97. package/dist/dataLayer/restoreMarker.js +261 -0
  98. package/dist/dataLayer/restoreMarker.js.map +1 -0
  99. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  100. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  101. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  102. package/dist/dataLayer/schemaDescribe.js +2 -1
  103. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  104. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  105. package/dist/resources/DatabaseTransaction.js +282 -109
  106. package/dist/resources/DatabaseTransaction.js.map +1 -1
  107. package/dist/resources/ResourceInterface.d.ts +8 -5
  108. package/dist/resources/ResourceInterface.js.map +1 -1
  109. package/dist/resources/Resources.js +22 -4
  110. package/dist/resources/Resources.js.map +1 -1
  111. package/dist/resources/Table.d.ts +6 -5
  112. package/dist/resources/Table.js +114 -37
  113. package/dist/resources/Table.js.map +1 -1
  114. package/dist/resources/analytics/write.js +6 -6
  115. package/dist/resources/analytics/write.js.map +1 -1
  116. package/dist/resources/blob.d.ts +8 -1
  117. package/dist/resources/blob.js +31 -14
  118. package/dist/resources/blob.js.map +1 -1
  119. package/dist/resources/databases.d.ts +59 -1
  120. package/dist/resources/databases.js +386 -41
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/jsResource.d.ts +4 -26
  123. package/dist/resources/jsResource.js +5 -59
  124. package/dist/resources/jsResource.js.map +1 -1
  125. package/dist/resources/models/Models.d.ts +11 -1
  126. package/dist/resources/models/Models.js +10 -1
  127. package/dist/resources/models/Models.js.map +1 -1
  128. package/dist/resources/models/backendRegistry.d.ts +9 -0
  129. package/dist/resources/models/backendRegistry.js +10 -0
  130. package/dist/resources/models/backendRegistry.js.map +1 -1
  131. package/dist/resources/models/openaiStream.d.ts +16 -1
  132. package/dist/resources/models/openaiStream.js +113 -21
  133. package/dist/resources/models/openaiStream.js.map +1 -1
  134. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  135. package/dist/resources/models/v1/chatCompletions.js +115 -0
  136. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  137. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  138. package/dist/resources/models/v1/embeddings.js +71 -0
  139. package/dist/resources/models/v1/embeddings.js.map +1 -0
  140. package/dist/resources/models/v1/errors.d.ts +54 -0
  141. package/dist/resources/models/v1/errors.js +130 -0
  142. package/dist/resources/models/v1/errors.js.map +1 -0
  143. package/dist/resources/models/v1/index.d.ts +36 -0
  144. package/dist/resources/models/v1/index.js +75 -0
  145. package/dist/resources/models/v1/index.js.map +1 -0
  146. package/dist/resources/models/v1/models.d.ts +26 -0
  147. package/dist/resources/models/v1/models.js +44 -0
  148. package/dist/resources/models/v1/models.js.map +1 -0
  149. package/dist/resources/models/v1/translation.d.ts +133 -0
  150. package/dist/resources/models/v1/translation.js +298 -0
  151. package/dist/resources/models/v1/translation.js.map +1 -0
  152. package/dist/resources/roles.d.ts +1 -1
  153. package/dist/resources/roles.js +54 -7
  154. package/dist/resources/roles.js.map +1 -1
  155. package/dist/resources/transaction.js +0 -3
  156. package/dist/resources/transaction.js.map +1 -1
  157. package/dist/security/jsLoader.js +84 -33
  158. package/dist/security/jsLoader.js.map +1 -1
  159. package/dist/security/role.js +4 -0
  160. package/dist/security/role.js.map +1 -1
  161. package/dist/security/superUserGuard.d.ts +7 -0
  162. package/dist/security/superUserGuard.js +23 -0
  163. package/dist/security/superUserGuard.js.map +1 -0
  164. package/dist/security/tokenAuthentication.d.ts +0 -1
  165. package/dist/security/tokenAuthentication.js +6 -2
  166. package/dist/security/tokenAuthentication.js.map +1 -1
  167. package/dist/security/user.d.ts +6 -1
  168. package/dist/security/user.js +23 -1
  169. package/dist/security/user.js.map +1 -1
  170. package/dist/server/REST.js +25 -9
  171. package/dist/server/REST.js.map +1 -1
  172. package/dist/server/fastifyRoutes.js +15 -1
  173. package/dist/server/fastifyRoutes.js.map +1 -1
  174. package/dist/server/http.d.ts +23 -2
  175. package/dist/server/http.js +99 -12
  176. package/dist/server/http.js.map +1 -1
  177. package/dist/server/itc/serverHandlers.js +7 -1
  178. package/dist/server/itc/serverHandlers.js.map +1 -1
  179. package/dist/server/jobs/jobProcess.js +20 -1
  180. package/dist/server/jobs/jobProcess.js.map +1 -1
  181. package/dist/server/jobs/jobRunner.js +10 -0
  182. package/dist/server/jobs/jobRunner.js.map +1 -1
  183. package/dist/server/jobs/jobs.js +11 -0
  184. package/dist/server/jobs/jobs.js.map +1 -1
  185. package/dist/server/middlewareChain.d.ts +10 -1
  186. package/dist/server/middlewareChain.js +81 -21
  187. package/dist/server/middlewareChain.js.map +1 -1
  188. package/dist/server/operationsServer.d.ts +0 -2
  189. package/dist/server/operationsServer.js.map +1 -1
  190. package/dist/server/serverHelpers/multipartParser.js +9 -0
  191. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  192. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  193. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  194. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  195. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  196. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  197. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  198. package/dist/server/serverHelpers/serverHandlers.js +27 -7
  199. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  200. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  201. package/dist/server/serverHelpers/serverUtilities.js +21 -7
  202. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  203. package/dist/server/static.js +68 -21
  204. package/dist/server/static.js.map +1 -1
  205. package/dist/server/storageReclamation.d.ts +17 -0
  206. package/dist/server/storageReclamation.js +90 -6
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/socketRouter.js +20 -0
  209. package/dist/server/threads/socketRouter.js.map +1 -1
  210. package/dist/server/threads/threadServer.js +12 -0
  211. package/dist/server/threads/threadServer.js.map +1 -1
  212. package/dist/sqlEngine/diff/differential.js +7 -3
  213. package/dist/sqlEngine/diff/differential.js.map +1 -1
  214. package/dist/sqlTranslator/index.js +6 -1
  215. package/dist/sqlTranslator/index.js.map +1 -1
  216. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  217. package/dist/upgrade/upgradePrompt.js +22 -3
  218. package/dist/upgrade/upgradePrompt.js.map +1 -1
  219. package/dist/utility/OperationFunctionCaller.js +25 -6
  220. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  221. package/dist/utility/errors/commonErrors.d.ts +1 -0
  222. package/dist/utility/errors/commonErrors.js +1 -0
  223. package/dist/utility/errors/commonErrors.js.map +1 -1
  224. package/dist/utility/hdbTerms.d.ts +13 -1
  225. package/dist/utility/hdbTerms.js +13 -1
  226. package/dist/utility/hdbTerms.js.map +1 -1
  227. package/dist/utility/install/installer.js +32 -0
  228. package/dist/utility/install/installer.js.map +1 -1
  229. package/dist/utility/logging/harper_logger.d.ts +47 -0
  230. package/dist/utility/logging/harper_logger.js +803 -38
  231. package/dist/utility/logging/harper_logger.js.map +1 -1
  232. package/dist/utility/operation_authorization.js +13 -3
  233. package/dist/utility/operation_authorization.js.map +1 -1
  234. package/npm-shrinkwrap.json +198 -193
  235. package/package.json +12 -7
  236. package/resources/DESIGN.md +2 -0
  237. package/resources/DatabaseTransaction.ts +310 -104
  238. package/resources/ResourceInterface.ts +8 -5
  239. package/resources/Resources.ts +22 -4
  240. package/resources/Table.ts +341 -263
  241. package/resources/analytics/write.ts +22 -20
  242. package/resources/blob.ts +31 -14
  243. package/resources/databases.ts +387 -46
  244. package/resources/jsResource.ts +5 -62
  245. package/resources/models/Models.ts +14 -1
  246. package/resources/models/backendRegistry.ts +10 -0
  247. package/resources/models/openaiStream.ts +131 -19
  248. package/resources/models/v1/chatCompletions.ts +128 -0
  249. package/resources/models/v1/embeddings.ts +70 -0
  250. package/resources/models/v1/errors.ts +141 -0
  251. package/resources/models/v1/index.ts +72 -0
  252. package/resources/models/v1/models.ts +53 -0
  253. package/resources/models/v1/translation.ts +362 -0
  254. package/resources/roles.ts +67 -7
  255. package/resources/transaction.ts +0 -3
  256. package/security/jsLoader.ts +84 -30
  257. package/security/role.ts +7 -0
  258. package/security/superUserGuard.ts +20 -0
  259. package/security/tokenAuthentication.ts +6 -3
  260. package/security/user.ts +26 -1
  261. package/server/DESIGN.md +61 -34
  262. package/server/REST.ts +25 -9
  263. package/server/fastifyRoutes.ts +20 -1
  264. package/server/http.ts +100 -13
  265. package/server/itc/serverHandlers.js +7 -1
  266. package/server/jobs/jobProcess.ts +18 -1
  267. package/server/jobs/jobRunner.ts +10 -0
  268. package/server/jobs/jobs.ts +11 -0
  269. package/server/middlewareChain.ts +79 -20
  270. package/server/operationsServer.ts +0 -2
  271. package/server/serverHelpers/multipartParser.ts +9 -0
  272. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  273. package/server/serverHelpers/registeredOperations.ts +19 -15
  274. package/server/serverHelpers/serverHandlers.js +28 -7
  275. package/server/serverHelpers/serverUtilities.ts +29 -7
  276. package/server/static.ts +87 -23
  277. package/server/storageReclamation.ts +104 -8
  278. package/server/threads/socketRouter.ts +20 -0
  279. package/server/threads/threadServer.js +11 -0
  280. package/sqlTranslator/index.ts +6 -1
  281. package/static/defaultConfig.yaml +3 -0
  282. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
  283. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
  284. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
  285. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  286. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
  287. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  288. package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
  289. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
  290. package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
  291. package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
  292. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
  293. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  294. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
  295. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
  296. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
  297. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
  298. package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
  299. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
  300. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
  301. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  302. package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
  303. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
  304. package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
  305. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
  306. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  307. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  308. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
  309. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
  310. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  311. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  312. package/studio/web/index.html +1 -1
  313. package/upgrade/upgradePrompt.ts +22 -3
  314. package/utility/OperationFunctionCaller.ts +24 -3
  315. package/utility/errors/commonErrors.ts +2 -0
  316. package/utility/hdbTerms.ts +13 -1
  317. package/utility/install/installer.ts +37 -0
  318. package/utility/logging/harper_logger.ts +786 -41
  319. package/utility/operation_authorization.ts +31 -3
  320. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
@@ -4,7 +4,7 @@ const terms = require('../../utility/hdbTerms.ts');
4
4
  const hdbUtil = require('../../utility/common_utils.ts');
5
5
  const harperLogger = require('../../utility/logging/harper_logger.ts');
6
6
  const { realExit } = require('../threads/workerProcessGuard.ts');
7
- const { handleHDBError, hdbErrors } = require('../../utility/errors/hdbError.ts');
7
+ const { ClientError, handleHDBError, hdbErrors } = require('../../utility/errors/hdbError.ts');
8
8
  const { isMainThread } = require('worker_threads');
9
9
  const { Readable } = require('stream');
10
10
 
@@ -43,6 +43,19 @@ const NO_AUTH_OPERATIONS = [
43
43
  terms.OPERATIONS_ENUM.LOGOUT,
44
44
  ];
45
45
 
46
+ const UNSAFE_REQUEST_BODY_PROPERTIES = ['__proto__', 'constructor', 'prototype'];
47
+
48
+ function validateRequestBodyProperties(body) {
49
+ if (!body || typeof body !== 'object') {
50
+ throw new ClientError('Invalid request body', 400);
51
+ }
52
+ for (const property of UNSAFE_REQUEST_BODY_PROPERTIES) {
53
+ if (Object.hasOwn(body, property)) {
54
+ throw new ClientError(`Request body property "${property}" is not allowed`, 400);
55
+ }
56
+ }
57
+ }
58
+
46
59
  function handleServerUncaughtException(err) {
47
60
  let message = `Found an uncaught exception with message: ${err.message}. ${os.EOL}Stack: ${err.stack} ${
48
61
  os.EOL
@@ -145,8 +158,12 @@ async function handlePostRequest(req, res, _bypassAuth = false) {
145
158
  let operation_function;
146
159
 
147
160
  try {
148
- // Just in case someone tries to bypass auth
149
- if (req.body.bypass_auth) delete req.body.bypass_auth;
161
+ validateRequestBodyProperties(req.body);
162
+ // Authorization bypass is internal dispatch state, never client request data. Dispatch no
163
+ // longer reads these properties, but remove both true and false values before operation
164
+ // code sees the body.
165
+ if (Object.hasOwn(req.body, 'bypass_auth')) delete req.body.bypass_auth;
166
+ if (Object.hasOwn(req.body, 'bypassAuth')) delete req.body.bypassAuth;
150
167
 
151
168
  operation_function = serverUtilities.chooseOperation(req.body);
152
169
 
@@ -174,9 +191,12 @@ async function handlePostRequest(req, res, _bypassAuth = false) {
174
191
  res.header(name, value);
175
192
  }
176
193
  // fastify-compress has one job. I don't know why it can't do it. So we compress here to
177
- // handle the case of returning a stream. Streams marked preCompressed (e.g. a stored
178
- // .tar.gz payload) are passed through as-is recompressing them wastes CPU for zero gain.
179
- if (!result.preCompressed && req.headers['accept-encoding']?.includes('gzip')) {
194
+ // handle the case of returning a stream. Streams marked `noCompression` opt out (e.g.
195
+ // RocksDB get_backup tars: the binding gzips when requested; compressing here would
196
+ // mislabel a gzip:false tar or double-compress a gzip:true one). Streams marked
197
+ // `preCompressed` (e.g. a stored .tar.gz payload) are passed through as-is —
198
+ // recompressing them wastes CPU for zero gain.
199
+ if (req.headers['accept-encoding']?.includes('gzip') && !result.noCompression && !result.preCompressed) {
180
200
  res.header('content-encoding', 'gzip');
181
201
  const gzip = createGzip({ level: constants.Z_BEST_SPEED }); // go fast
182
202
  // .pipe() does not tear down across the pipe in either direction, so wire both:
@@ -187,7 +207,8 @@ async function handlePostRequest(req, res, _bypassAuth = false) {
187
207
  // - gzip close → destroy source: when the client disconnects mid-download Fastify
188
208
  // destroys only the stream it was handed (gzip); destroy the source too so its
189
209
  // underlying file/blob read stops and descriptors release. (No-op after a normal
190
- // end. preCompressed streams skip this block; Fastify handles them directly.)
210
+ // end. noCompression/preCompressed streams skip this block; Fastify handles them
211
+ // directly.)
191
212
  const source = result;
192
213
  source.on('error', (error) => gzip.destroy(error));
193
214
  gzip.on('close', () => source.destroy());
@@ -12,6 +12,7 @@ import { isDeployValidating } from './deployValidationState.ts';
12
12
  import harperLogger from '../../utility/logging/harper_logger.ts';
13
13
  import readLog from '../../utility/logging/readLog.ts';
14
14
  import * as export_ from '../../dataLayer/export.ts';
15
+ import * as rocksdbBackup from '../../dataLayer/rocksdbBackup.ts';
15
16
  import * as opAuth from '../../utility/operation_authorization.ts';
16
17
  import * as jobs from '../jobs/jobs.ts';
17
18
  import * as terms from '../../utility/hdbTerms.ts';
@@ -47,6 +48,7 @@ import {
47
48
  getRemoteOperationFunction,
48
49
  setLocalOperationDispatch,
49
50
  } from './registeredOperations.ts';
51
+ import { runWithOperationAuthorizationBypass } from './operationAuthorizationState.ts';
50
52
 
51
53
  const pSearchSearch = util.promisify(search.search);
52
54
  let pEvaluateSql: (sql: string) => Promise<any>;
@@ -202,7 +204,7 @@ server.setMcpQuotaHandler = (handler) => {
202
204
  setMcpQuotaHandler(handler);
203
205
  };
204
206
 
205
- export function chooseOperation(json: OperationRequestBody) {
207
+ export function chooseOperation(json: OperationRequestBody, bypassAuth = false) {
206
208
  let getOpResult: OperationFunctionObject;
207
209
  try {
208
210
  getOpResult = getOperationFunction(json);
@@ -213,7 +215,7 @@ export function chooseOperation(json: OperationRequestBody) {
213
215
  // and its metadata actually exist, so no perm check is skipped by returning early
214
216
  // here (#1736). Workers never re-forward, so an unknown op can't loop.
215
217
  if (isMainThread) {
216
- const remoteOperationFunction = getRemoteOperationFunction(json.operation);
218
+ const remoteOperationFunction = getRemoteOperationFunction(json.operation, bypassAuth);
217
219
  if (remoteOperationFunction) return remoteOperationFunction;
218
220
  }
219
221
  operationLog.error(`Error when selecting operation function - ${err}`);
@@ -230,7 +232,7 @@ export function chooseOperation(json: OperationRequestBody) {
230
232
  const sqlStatement = json.operation === 'sql' ? json.sql : json.search_operation.sql;
231
233
  const parsedSqlObject = sql.convertSQLToAST(sqlStatement);
232
234
  json.parsed_sql_object = parsedSqlObject;
233
- if (!json.bypass_auth) {
235
+ if (!bypassAuth) {
234
236
  const astPermCheck = sql.checkASTPermissions(json, parsedSqlObject);
235
237
  if (astPermCheck) {
236
238
  operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
@@ -247,7 +249,7 @@ export function chooseOperation(json: OperationRequestBody) {
247
249
  }
248
250
  //we need to bypass permission checks to allow the createAuthorizationTokens
249
251
  } else if (
250
- !json.bypass_auth &&
252
+ !bypassAuth &&
251
253
  json.operation !== terms.OPERATIONS_ENUM.CREATE_AUTHENTICATION_TOKENS &&
252
254
  json.operation !== terms.OPERATIONS_ENUM.LOGIN &&
253
255
  json.operation !== terms.OPERATIONS_ENUM.LOGOUT
@@ -309,9 +311,11 @@ _assignPackageExport('operation', operation);
309
311
  */
310
312
  export function operation(operation: OperationRequestBody, context: Context, authorize: boolean) {
311
313
  operation.hdb_user = context?.user;
312
- operation.bypass_auth = !authorize;
313
- const operation_function = chooseOperation(operation);
314
- return processLocalTransaction({ body: operation }, operation_function);
314
+ const bypassAuth = !authorize;
315
+ return runWithOperationAuthorizationBypass(bypassAuth, () => {
316
+ const operation_function = chooseOperation(operation, bypassAuth);
317
+ return processLocalTransaction({ body: operation }, operation_function);
318
+ });
315
319
  }
316
320
 
317
321
  interface Transaction {
@@ -388,6 +392,9 @@ export async function executeJob(json: OperationRequestBody): Promise<JobResult>
388
392
  };
389
393
  }
390
394
  } catch (err) {
395
+ // errors that already carry a statusCode (e.g. ClientError from job validation) are
396
+ // client-facing as-is; wrapping them here would turn a 400/404/409 into a 500
397
+ if (err instanceof Error && typeof (err as any).statusCode === 'number') throw err;
391
398
  const error = err instanceof Error ? err : null;
392
399
  const message = `There was an error executing job: ${error && 'http_resp_msg' in error ? error.http_resp_msg : err}`;
393
400
  operationLog.error(message);
@@ -581,6 +588,21 @@ function initializeOperationFunctionMap(): Map<OperationFunctionName, OperationF
581
588
  );
582
589
  opFuncMap.set(terms.OPERATIONS_ENUM.INSTALL_NODE_MODULES, new OperationFunctionObject(npmUtilities.installModules));
583
590
  opFuncMap.set(terms.OPERATIONS_ENUM.GET_BACKUP, new OperationFunctionObject(schema.getBackup));
591
+ opFuncMap.set(
592
+ terms.OPERATIONS_ENUM.CREATE_BACKUP,
593
+ new OperationFunctionObject(executeJob, rocksdbBackup.createBackup)
594
+ );
595
+ opFuncMap.set(terms.OPERATIONS_ENUM.LIST_BACKUPS, new OperationFunctionObject(rocksdbBackup.listBackups));
596
+ opFuncMap.set(
597
+ terms.OPERATIONS_ENUM.VERIFY_BACKUP,
598
+ new OperationFunctionObject(executeJob, rocksdbBackup.verifyBackup)
599
+ );
600
+ opFuncMap.set(terms.OPERATIONS_ENUM.DELETE_BACKUP, new OperationFunctionObject(rocksdbBackup.deleteBackup));
601
+ opFuncMap.set(terms.OPERATIONS_ENUM.PURGE_BACKUPS, new OperationFunctionObject(rocksdbBackup.purgeBackups));
602
+ opFuncMap.set(
603
+ terms.OPERATIONS_ENUM.RESTORE_BACKUP,
604
+ new OperationFunctionObject(executeJob, rocksdbBackup.restoreBackup)
605
+ );
584
606
  opFuncMap.set(terms.OPERATIONS_ENUM.CLEANUP_ORPHAN_BLOBS, new OperationFunctionObject(schema.cleanupOrphanBlobs));
585
607
 
586
608
  opFuncMap.set(terms.OPERATIONS_ENUM.GET_ANALYTICS, new OperationFunctionObject(analytics.getOp));
package/server/static.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { realpathSync, existsSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { dirname, join } from 'node:path';
3
3
  import { Scope } from '../components/Scope';
4
4
  import { resolveBaseURLPath } from '../components/resolveBaseURLPath.ts';
5
5
  import { convertToMS } from '../utility/common_utils.ts';
@@ -52,7 +52,7 @@ import send from 'send';
52
52
  *
53
53
  * This plugin dynamically updates its behavior based on the current configuration file. Users can make updates and immediately see the changes reflect in the next request.
54
54
  *
55
- * Updates to the `files` option will clear the in-memory maps and allow them to regenerate based on the new configuration (since the default EntryHandler will regenerate anyways).
55
+ * Updates to the `files` option incrementally add newly matched paths and remove paths that no longer match while preserving common entries.
56
56
  * Updates to `urlPath` request a restart: the HTTP route mount is registered once at load and cannot be re-registered on a live server (#1583).
57
57
  */
58
58
  /**
@@ -146,12 +146,52 @@ export function handleApplication(scope: Scope) {
146
146
  // in-memory map of static files
147
147
  // keys are the URL paths relative to the mount base, values are the absolute paths to the files
148
148
  const staticFiles = new Map<string, string>();
149
- const indexEntries = new Map<string, string>();
149
+ const indexEntries = new Map<string, string | null>();
150
+ const staticFileOwners = new Map<string, Map<string, string>>();
151
+ const indexEntryOwners = new Map<string, Map<string, string | null>>();
152
+
153
+ function setOwnedPath<Path>(
154
+ paths: Map<string, Path>,
155
+ ownersByURL: Map<string, Map<string, Path>>,
156
+ urlPath: string,
157
+ owner: string,
158
+ path: Path
159
+ ) {
160
+ let owners = ownersByURL.get(urlPath);
161
+ if (!owners) ownersByURL.set(urlPath, (owners = new Map()));
162
+ owners.delete(owner);
163
+ owners.set(owner, path);
164
+ paths.set(urlPath, path);
165
+ }
166
+
167
+ function removeOwnedPath<Path>(
168
+ paths: Map<string, Path>,
169
+ ownersByURL: Map<string, Map<string, Path>>,
170
+ urlPath: string,
171
+ owner: string
172
+ ) {
173
+ const owners = ownersByURL.get(urlPath);
174
+ if (!owners?.delete(owner)) return;
175
+ const replacement = [...owners.values()].at(-1);
176
+ if (replacement === undefined) {
177
+ ownersByURL.delete(urlPath);
178
+ paths.delete(urlPath);
179
+ } else {
180
+ paths.set(urlPath, replacement);
181
+ }
182
+ }
150
183
 
151
184
  // The HTTP route below is registered once, with the urlPath in effect at load time; the mount
152
185
  // cannot be re-registered at runtime. Capture the matching base once so map keys always agree
153
186
  // with the registered route (#1583).
154
187
  const baseURLPath = resolveBaseURLPath(scope.pluginName, (scope.options.getAll() as any)?.urlPath);
188
+ // The same base as the client sees. `baseURLPath` is mount-relative — it must stay that way to
189
+ // keep agreeing with the entry URL paths the file map is keyed by — but a redirect Location has
190
+ // to carry the application's mount too, or it would point outside the mount (#1583).
191
+ const externalBaseURLPath = scope.externalBasePath(baseURLPath);
192
+ let urlPathRestartPending = false;
193
+ let fileSelectionChangePending = false;
194
+ let entryHandler: ReturnType<Scope['handleEntry']>;
155
195
 
156
196
  // A bare `before:` / `after:` key in YAML parses as null — treat it as unset, like before this
157
197
  // option was validated.
@@ -183,14 +223,19 @@ export function handleApplication(scope: Scope) {
183
223
 
184
224
  scope.options.on('change', (key) => {
185
225
  if (key[0] === 'files') {
186
- // If the files option changes, clear the maps and let the entry handler regenerate them
187
- staticFiles.clear();
188
- indexEntries.clear();
189
- scope.logger.info(`Static files reinitialized due to change in ${key.join('.')}`);
226
+ fileSelectionChangePending = true;
227
+ const updateReady = entryHandler?.ready;
228
+ const clearPending = () => {
229
+ if (entryHandler?.ready === updateReady) fileSelectionChangePending = false;
230
+ };
231
+ updateReady?.then(clearPending, clearPending);
232
+ // EntryHandler updates are incremental: paths no longer matched emit unlink, newly matched
233
+ // paths emit add, and common unchanged paths remain valid in these maps.
234
+ scope.logger.info(`Static file matches updated due to change in ${key.join('.')}`);
190
235
  return;
191
236
  }
192
237
  if (key[0] === 'urlPath') {
193
- // The route mount cannot be changed on a live server registration — restart to apply
238
+ urlPathRestartPending = true;
194
239
  scope.requestRestart();
195
240
  return;
196
241
  }
@@ -207,7 +252,12 @@ export function handleApplication(scope: Scope) {
207
252
  });
208
253
 
209
254
  // Handle entry events for the default entry handler based on the `files` and `urlPath` options
210
- scope.handleEntry((entry) => {
255
+ entryHandler = scope.handleEntry((entry) => {
256
+ if (
257
+ urlPathRestartPending &&
258
+ (!fileSelectionChangePending || (entry.eventType !== 'unlink' && entry.eventType !== 'unlinkDir'))
259
+ )
260
+ return;
211
261
  // entry.urlPath includes the component's base URL path, but when a `urlPath` is configured
212
262
  // the routing chain strips that mount prefix from req.pathname before this plugin's handler
213
263
  // runs — so key the maps relative to the base (#1583)
@@ -218,34 +268,43 @@ export function handleApplication(scope: Scope) {
218
268
  switch (entry.eventType) {
219
269
  // Directories only matter for the `index` files
220
270
  case 'addDir':
221
- case 'unlinkDir':
222
271
  // Handle `index.html` for directories for if/when the user enables the `index` option
223
272
  const indexPath = join(entry.absolutePath, 'index.html');
224
- if (existsSync(indexPath)) {
225
- indexEntries[entry.eventType === 'addDir' ? 'set' : 'delete'](urlPath, indexPath);
226
- }
273
+ if (existsSync(indexPath)) setOwnedPath(indexEntries, indexEntryOwners, urlPath, entry.absolutePath, indexPath);
274
+ break;
275
+ case 'unlinkDir':
276
+ removeOwnedPath(indexEntries, indexEntryOwners, urlPath, entry.absolutePath);
227
277
  break;
228
278
  // Otherwise, user must specify pattern to match individual files
229
279
  case 'add':
230
280
  // Store the file in memory for serving
231
- staticFiles.set(urlPath, entry.absolutePath);
281
+ setOwnedPath(staticFiles, staticFileOwners, urlPath, entry.absolutePath, entry.absolutePath);
232
282
  // If the file is an index.html, also store it in the index entries
233
283
  if (urlPath.endsWith('index.html')) {
234
284
  // Without trailing slash; null -> 301 redirect to trailing slash
235
285
  let lastSlashIndex = urlPath.lastIndexOf('/');
236
- indexEntries.set(urlPath.slice(0, lastSlashIndex), null);
286
+ setOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex), entry.absolutePath, null);
237
287
  // With trailing slash; serves the index.html file
238
- indexEntries.set(urlPath.slice(0, lastSlashIndex + 1), entry.absolutePath);
288
+ setOwnedPath(
289
+ indexEntries,
290
+ indexEntryOwners,
291
+ urlPath.slice(0, lastSlashIndex + 1),
292
+ entry.absolutePath,
293
+ entry.absolutePath
294
+ );
239
295
  }
240
296
  break;
241
297
  case 'unlink':
242
- // Remove the file from memory when it is deleted
243
- staticFiles.delete(urlPath);
298
+ removeOwnedPath(staticFiles, staticFileOwners, urlPath, entry.absolutePath);
244
299
  // If the file is an index.html, remove it from the index entries as well
245
300
  if (urlPath.endsWith('index.html')) {
246
301
  let lastSlashIndex = urlPath.lastIndexOf('/');
247
- indexEntries.delete(urlPath.slice(0, lastSlashIndex));
248
- indexEntries.delete(urlPath.slice(0, lastSlashIndex + 1));
302
+ const directoryURLPath = urlPath.slice(0, lastSlashIndex);
303
+ const directoryOwner = dirname(entry.absolutePath);
304
+ removeOwnedPath(indexEntries, indexEntryOwners, directoryURLPath, entry.absolutePath);
305
+ removeOwnedPath(indexEntries, indexEntryOwners, directoryURLPath, directoryOwner);
306
+ removeOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex + 1), entry.absolutePath);
307
+ removeOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex + 1), directoryOwner);
249
308
  }
250
309
  break;
251
310
  }
@@ -283,7 +342,11 @@ export function handleApplication(scope: Scope) {
283
342
  // redirect the no-slash form so relative links on the index page resolve under
284
343
  // the mount (#1583). Query string is preserved across both redirects; compute it
285
344
  // lazily inside each branch so the common (non-redirect) index serve stays allocation-free.
286
- if (staticFile && req.pathname === '/' && baseURLPath !== '/') {
345
+ // Gated on the EXTERNAL base path, not the plugin-local one: a root-level static
346
+ // plugin (baseURLPath === '/') still needs this redirect when the application
347
+ // itself carries a host/urlPath mount, since the client-visible mount root is then
348
+ // externalBaseURLPath, not '/' (review finding).
349
+ if (staticFile && req.pathname === '/' && externalBaseURLPath !== '/') {
287
350
  const originalPathname: string | undefined = (req as any).originalPathname;
288
351
  if (originalPathname && !originalPathname.endsWith('/')) {
289
352
  const queryIndex = (req.url as string).indexOf('?');
@@ -291,7 +354,7 @@ export function handleApplication(scope: Scope) {
291
354
  return {
292
355
  status: 301,
293
356
  headers: {
294
- Location: baseURLPath + query,
357
+ Location: externalBaseURLPath + query,
295
358
  },
296
359
  };
297
360
  }
@@ -300,7 +363,8 @@ export function handleApplication(scope: Scope) {
300
363
  // If `null`, redirect to trailing slash. req.pathname arrives with the mount
301
364
  // prefix stripped, so rebuild the external path for the Location header (#1583)
302
365
  if (staticFile === null) {
303
- const externalPath = baseURLPath === '/' ? req.pathname : baseURLPath.slice(0, -1) + req.pathname;
366
+ const externalPath =
367
+ externalBaseURLPath === '/' ? req.pathname : externalBaseURLPath.slice(0, -1) + req.pathname;
304
368
  const queryIndex = (req.url as string).indexOf('?');
305
369
  const query = queryIndex === -1 ? '' : (req.url as string).slice(queryIndex);
306
370
  return {
@@ -1,6 +1,6 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, realpath } from 'node:fs/promises';
2
2
  import { statfs } from 'node:fs/promises';
3
- import { join } from 'node:path';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { getWorkerIndex, getWorkerCount } from '../server/threads/manageThreads.js';
5
5
  import { logger } from '../utility/logging/logger.ts';
6
6
  import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
@@ -27,6 +27,56 @@ export type QuotaStatusData = {
27
27
  updatedAt: number;
28
28
  };
29
29
 
30
+ /**
31
+ * Resolves `target` to its real (symlink-free) path. `target` itself may not exist yet (e.g. a
32
+ * table's storage directory that hasn't been created), so walk up to the nearest existing
33
+ * ancestor, resolve that, and rejoin the not-yet-existing remainder lexically — this still
34
+ * catches a symlinked *parent* directory, which is the case that actually matters for quota
35
+ * scoping (leaf directories are created by Harper itself, not symlinked).
36
+ */
37
+ async function resolveRealPath(target: string): Promise<string> {
38
+ try {
39
+ return await realpath(target);
40
+ } catch {
41
+ const parent = dirname(target);
42
+ if (parent === target) return resolve(target);
43
+ return join(await resolveRealPath(parent), basename(target));
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Whether `target` resolves to a path inside `root`. quota-status.json is scoped to a single
49
+ * ROOTPATH; a database or table configured with its own STORAGE_PATH (or a STORAGE_BLOBPATHS
50
+ * entry) can live on an entirely different volume, and must not be reported against the root's
51
+ * quota. Compares real (symlink-resolved) paths, not just lexically-normalized ones — a `..`-free
52
+ * path can still be a symlink into another volume, and `resolve()` alone wouldn't catch that.
53
+ */
54
+ async function isPathWithinRoot(root: string, target: string): Promise<boolean> {
55
+ const [resolvedRoot, resolvedTarget] = await Promise.all([resolveRealPath(root), resolveRealPath(target)]);
56
+ const relativePath = relative(resolvedRoot, resolvedTarget);
57
+ if (relativePath === '') return true;
58
+ if (isAbsolute(relativePath)) return false;
59
+ // Check the first segment specifically (`..`), not a `..`-prefix match — a child directory
60
+ // literally named e.g. `..data` (the Kubernetes ConfigMap/Secret atomic-update symlink
61
+ // convention) is not a parent-traversal and must not be rejected as escaping the root.
62
+ const firstSegment = relativePath.split(sep)[0];
63
+ return firstSegment !== '..';
64
+ }
65
+
66
+ /**
67
+ * Guards against a syntactically valid but semantically broken quota-status.json (missing or
68
+ * negative usage, non-positive quota, non-finite timestamp) being read as "100% free" or worse.
69
+ */
70
+ function isValidQuotaStatus(status: QuotaStatusData): boolean {
71
+ return (
72
+ Number.isFinite(status.updatedAt) &&
73
+ Number.isFinite(status.quotaBytes) &&
74
+ status.quotaBytes > 0 &&
75
+ Number.isFinite(status.usedBytes) &&
76
+ status.usedBytes >= 0
77
+ );
78
+ }
79
+
30
80
  /**
31
81
  * Reads the quota-status.json file written by host-manager.
32
82
  * Returns undefined if the file is absent or malformed; does not apply age filtering.
@@ -70,15 +120,61 @@ export function onStorageReclamation(
70
120
  }
71
121
  let reclamationTimer: NodeJS.Timeout;
72
122
 
73
- // If a fresh quota-status.json exists (written by host-manager every ~90s), use quota-based ratio.
74
- // Otherwise fall back to statfs for the registered path.
75
- const defaultGetAvailableSpaceRatio = async (path: string): Promise<number> => {
123
+ export type StorageSpaceStats = {
124
+ available: number;
125
+ free: number;
126
+ size: number;
127
+ // Whether these numbers reflect a quota (accurate for what Harper can actually write) or
128
+ // raw filesystem-level statfs (can be wildly wrong under a quota-limited data directory).
129
+ basis: 'quota' | 'filesystem';
130
+ };
131
+
132
+ /**
133
+ * Get available/free/size storage stats for a path, preferring a fresh quota-status.json (see
134
+ * getQuotaStatus) over statfs. statfs describes the filesystem, which is misleading whenever
135
+ * the data directory is quota-limited on a larger shared volume (#1976).
136
+ */
137
+ export async function getStorageSpaceStats(path: string): Promise<StorageSpaceStats> {
138
+ const rootPath = envMgr.get(CONFIG_PARAMS.ROOTPATH);
76
139
  const status = await getQuotaStatus();
77
- if (status?.quotaBytes && Date.now() - status.updatedAt < QUOTA_STATUS_MAX_AGE_MS) {
78
- return Math.max(0, status.quotaBytes - status.usedBytes) / status.quotaBytes;
140
+ const statusAge = status ? Date.now() - status.updatedAt : undefined;
141
+ if (
142
+ status &&
143
+ isValidQuotaStatus(status) &&
144
+ rootPath &&
145
+ // A negative age (clock skew, or a corrupt/future updatedAt) is not "fresh" — treat it the
146
+ // same as stale rather than trusting quota data indefinitely.
147
+ statusAge >= 0 &&
148
+ statusAge < QUOTA_STATUS_MAX_AGE_MS &&
149
+ // Checked last: it's the only async/IO-bound condition, so short-circuit it behind the
150
+ // cheap synchronous checks above.
151
+ (await isPathWithinRoot(rootPath, path))
152
+ ) {
153
+ const available = Math.max(0, status.quotaBytes - status.usedBytes);
154
+ return { available, free: available, size: status.quotaBytes, basis: 'quota' };
79
155
  }
80
156
  const fsStats = await statfs(path);
81
- return fsStats.bavail / fsStats.blocks;
157
+ return {
158
+ available: fsStats.bavail * fsStats.bsize,
159
+ free: fsStats.bfree * fsStats.bsize,
160
+ size: fsStats.blocks * fsStats.bsize,
161
+ basis: 'filesystem',
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Ratio of available to total size, guarding the 0/0 = NaN case (a zero-size statfs/quota result,
167
+ * e.g. an empty or misconfigured mount) so a full disk still reports a ratio of 0 rather than NaN.
168
+ */
169
+ export function computeAvailableRatio(available: number, size: number): number {
170
+ return size > 0 ? available / size : 0;
171
+ }
172
+
173
+ // If a fresh quota-status.json exists (written by host-manager every ~90s), use quota-based ratio.
174
+ // Otherwise fall back to statfs for the registered path.
175
+ const defaultGetAvailableSpaceRatio = async (path: string): Promise<number> => {
176
+ const { available, size } = await getStorageSpaceStats(path);
177
+ return computeAvailableRatio(available, size);
82
178
  };
83
179
  let getAvailableSpaceRatio: (path: string) => Promise<number> = defaultGetAvailableSpaceRatio;
84
180
 
@@ -8,6 +8,12 @@ import { join } from 'path';
8
8
 
9
9
  const workers = [];
10
10
  const workersReady = [];
11
+ // startHTTPThreads() can be called more than once in-process (e.g. a test harness adding more
12
+ // worker threads to an already-running server via addThreads()). The crash-path sweep below must
13
+ // run only on the very first call — a later call happens after real mirrors are already bound, and
14
+ // sweeping the sockets directory then would delete their live files, reintroducing the exact outage
15
+ // this guards against.
16
+ let sweptSocketsDirectory = false;
11
17
 
12
18
  if (isMainThread) {
13
19
  process.on('uncaughtException', (error) => {
@@ -23,6 +29,20 @@ if (isMainThread) {
23
29
  }
24
30
 
25
31
  export async function startHTTPThreads(threadCount = 2, dynamicThreads?: boolean) {
32
+ // Crash-path defense: a hard crash can skip a worker's exit-time UDS cleanup and leave stale
33
+ // mirror files behind. This runs before any worker below can start (and thus before any mirror
34
+ // can bind), so it can only ever clear files nothing is using yet — never a live mirror. The
35
+ // inode ownership guard in cleanupUdsFiles()/markUdsBindFailed() (see http.ts) is the matching
36
+ // defense for the in-process rolling-restart case, which this sweep does not cover.
37
+ // Lazy dynamic import (not a top-level one), matching server/status/index.ts's own lazy import of
38
+ // http.ts: http.ts's module graph reaches security/auth.ts, whose module-scope table() call needs
39
+ // config already initialized — pulling that graph in at this file's own top level (which
40
+ // bin/run.ts imports before parsing argv / initializing config) breaks startup with "Unable to
41
+ // determine database storage path" before main() ever runs.
42
+ if (isMainThread && !sweptSocketsDirectory) {
43
+ sweptSocketsDirectory = true;
44
+ (await import('../http.ts')).cleanupSocketsDirectory();
45
+ }
26
46
  recordHostname().catch((err) => harperLogger.error?.('Error recording hostname for analytics:', err));
27
47
  // Drive transaction-log cooling from the main thread (the registry is a
28
48
  // process-global singleton; see startTransactionLogCooling). Runs for all
@@ -275,6 +275,11 @@ function listenOnDomainSocket(port, server) {
275
275
  }
276
276
  function onListening() {
277
277
  server.removeListener('error', onError);
278
+ // Record ownership of the inode we just bound, so cleanupUdsFiles()/markUdsBindFailed()
279
+ // (see http.ts) can tell this worker's own file apart from a replacement's later rebind
280
+ // at the same path (see registerUdsCleanupPaths). A no-op for domain sockets that aren't
281
+ // UDS mirrors (e.g. the operations API's primary socket), which were never registered.
282
+ httpComponent.recordUdsBindSuccess(port);
278
283
  harperLogger.info('Domain socket listening on ' + port);
279
284
  resolve({ port, name: server.name, protocol_name: server.protocol_name });
280
285
  }
@@ -387,6 +392,9 @@ function listenOnPorts() {
387
392
  callback?.();
388
393
  },
389
394
  };
395
+ // createUwsServer() only resolves once uWS's own listen_unix() confirms the bind, so
396
+ // ownership is confirmed now (see http.ts's ownership-aware cleanupUdsFiles()).
397
+ if (cfg.socketPath) httpComponent.recordUdsBindSuccess(cfg.socketPath);
390
398
  harperLogger.info('uWS listening on ' + (cfg.socketPath ?? cfg.port));
391
399
  return { port: key };
392
400
  })
@@ -517,6 +525,9 @@ async function listenOnPortsBun() {
517
525
  });
518
526
  SERVERS[udsPath] = udsServer;
519
527
  httpComponent.registerUdsCleanupPaths(udsPath, yamlPath);
528
+ // Bun.serve() above already succeeded (a bind failure throws synchronously into the
529
+ // surrounding catch), so this worker's ownership of udsPath is confirmed now.
530
+ httpComponent.recordUdsBindSuccess(udsPath);
520
531
 
521
532
  const writeMetadata = () => httpComponent.writeUdsMetadata(yamlPath, port, config.pseudoServer);
522
533
  config.tlsSelector.ready.then(writeMetadata);
@@ -17,6 +17,7 @@ import * as terms from '../utility/hdbTerms.ts';
17
17
  import { handleHDBError } from '../utility/errors/hdbError.ts';
18
18
  import { HTTP_STATUS_CODES } from '../utility/errors/commonErrors.ts';
19
19
  import * as sqlEngineRouter from '../sqlEngine/router.ts';
20
+ import { isOperationAuthorizationBypassed } from '../server/serverHelpers/operationAuthorizationState.ts';
20
21
 
21
22
  //here we call to define and import custom functions to alasql
22
23
  alasqlFunctionImporter(alasql);
@@ -129,7 +130,11 @@ export function processAST(jsonMessage: any, parsedSqlObject: any, callback: any
129
130
  try {
130
131
  let sqlFunction = nullFunction;
131
132
 
132
- if (!jsonMessage.bypass_auth && !parsedSqlObject.permissions_checked) {
133
+ // Trusted bypass is dispatch/async-context state (set by chooseOperation's caller via
134
+ // runWithOperationAuthorizationBypass), never body state — jsonMessage.bypass_auth is
135
+ // caller-controlled and is stripped before operation code sees it (see
136
+ // server/serverHelpers/serverHandlers.js and components/mcp/tools/operations.ts).
137
+ if (!isOperationAuthorizationBypassed() && !parsedSqlObject.permissions_checked) {
133
138
  let permissionsCheck = checkASTPermissions(jsonMessage, parsedSqlObject);
134
139
  if (permissionsCheck && permissionsCheck.length > 0) {
135
140
  return callback(UNAUTHORIZED_RESPONSE, permissionsCheck);
@@ -72,8 +72,11 @@ storage:
72
72
  writeAsync: false
73
73
  caching: true
74
74
  compression: true
75
+ rocks:
76
+ compression: null
75
77
  noReadAhead: false
76
78
  path: null
79
+ backupPath: null
77
80
  prefetchWrites: true
78
81
  tls:
79
82
  privateKey: null