@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.
- package/bin/backup.ts +169 -0
- package/bin/cliOperations.ts +92 -73
- package/bin/copyDb.ts +21 -4
- package/bin/harper.ts +43 -55
- package/bin/help.ts +216 -0
- package/components/Application.ts +236 -46
- package/components/ApplicationScope.ts +26 -0
- package/components/EntryHandler.ts +410 -105
- package/components/RuntimeModuleTracker.ts +189 -0
- package/components/Scope.ts +120 -30
- package/components/componentLoader.ts +177 -25
- package/components/deployLifecycle.ts +119 -33
- package/components/mcp/toolRegistry.ts +10 -0
- package/components/mcp/tools/application.ts +12 -5
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
- package/components/mcp/tools/schemas/operations.ts +9 -0
- package/components/operations.js +5 -6
- package/components/operationsValidation.js +32 -2
- package/components/scopeMount.ts +150 -0
- package/config/configUtils.ts +12 -9
- package/config-root.schema.json +14 -0
- package/dataLayer/backupManifest.ts +102 -0
- package/dataLayer/blobBackup.ts +286 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/restoreMarker.ts +276 -0
- package/dataLayer/rocksdbBackup.ts +1100 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/backup.d.ts +9 -0
- package/dist/bin/backup.js +192 -0
- package/dist/bin/backup.js.map +1 -0
- package/dist/bin/cliOperations.d.ts +13 -0
- package/dist/bin/cliOperations.js +89 -70
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.js +13 -2
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +13 -0
- package/dist/bin/harper.js +45 -56
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.d.ts +8 -0
- package/dist/bin/help.js +192 -0
- package/dist/bin/help.js.map +1 -0
- package/dist/components/Application.d.ts +16 -1
- package/dist/components/Application.js +210 -38
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +7 -0
- package/dist/components/ApplicationScope.js +22 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.d.ts +4 -4
- package/dist/components/EntryHandler.js +386 -95
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.d.ts +11 -0
- package/dist/components/RuntimeModuleTracker.js +189 -0
- package/dist/components/RuntimeModuleTracker.js.map +1 -0
- package/dist/components/Scope.d.ts +38 -1
- package/dist/components/Scope.js +117 -28
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -6
- package/dist/components/componentLoader.js +160 -18
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deployLifecycle.d.ts +6 -2
- package/dist/components/deployLifecycle.js +109 -31
- package/dist/components/deployLifecycle.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +7 -0
- package/dist/components/mcp/toolRegistry.js +10 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.js +11 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +9 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/operations.js +6 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +34 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/scopeMount.d.ts +86 -0
- package/dist/components/scopeMount.js +131 -0
- package/dist/components/scopeMount.js.map +1 -0
- package/dist/config/configUtils.js +13 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/backupManifest.d.ts +26 -0
- package/dist/dataLayer/backupManifest.js +97 -0
- package/dist/dataLayer/backupManifest.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +87 -0
- package/dist/dataLayer/blobBackup.js +282 -0
- package/dist/dataLayer/blobBackup.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +122 -0
- package/dist/dataLayer/restoreMarker.js +261 -0
- package/dist/dataLayer/restoreMarker.js.map +1 -0
- package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
- package/dist/dataLayer/rocksdbBackup.js +1039 -0
- package/dist/dataLayer/rocksdbBackup.js.map +1 -0
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +55 -0
- package/dist/resources/DatabaseTransaction.js +282 -109
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -5
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js +22 -4
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -5
- package/dist/resources/Table.js +114 -37
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -6
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +8 -1
- package/dist/resources/blob.js +31 -14
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +59 -1
- package/dist/resources/databases.js +386 -41
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/jsResource.d.ts +4 -26
- package/dist/resources/jsResource.js +5 -59
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -1
- package/dist/resources/models/Models.js +10 -1
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +9 -0
- package/dist/resources/models/backendRegistry.js +10 -0
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +16 -1
- package/dist/resources/models/openaiStream.js +113 -21
- package/dist/resources/models/openaiStream.js.map +1 -1
- package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
- package/dist/resources/models/v1/chatCompletions.js +115 -0
- package/dist/resources/models/v1/chatCompletions.js.map +1 -0
- package/dist/resources/models/v1/embeddings.d.ts +11 -0
- package/dist/resources/models/v1/embeddings.js +71 -0
- package/dist/resources/models/v1/embeddings.js.map +1 -0
- package/dist/resources/models/v1/errors.d.ts +54 -0
- package/dist/resources/models/v1/errors.js +130 -0
- package/dist/resources/models/v1/errors.js.map +1 -0
- package/dist/resources/models/v1/index.d.ts +36 -0
- package/dist/resources/models/v1/index.js +75 -0
- package/dist/resources/models/v1/index.js.map +1 -0
- package/dist/resources/models/v1/models.d.ts +26 -0
- package/dist/resources/models/v1/models.js +44 -0
- package/dist/resources/models/v1/models.js.map +1 -0
- package/dist/resources/models/v1/translation.d.ts +133 -0
- package/dist/resources/models/v1/translation.js +298 -0
- package/dist/resources/models/v1/translation.js.map +1 -0
- package/dist/resources/roles.d.ts +1 -1
- package/dist/resources/roles.js +54 -7
- package/dist/resources/roles.js.map +1 -1
- package/dist/resources/transaction.js +0 -3
- package/dist/resources/transaction.js.map +1 -1
- package/dist/security/jsLoader.js +84 -33
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/role.js +4 -0
- package/dist/security/role.js.map +1 -1
- package/dist/security/superUserGuard.d.ts +7 -0
- package/dist/security/superUserGuard.js +23 -0
- package/dist/security/superUserGuard.js.map +1 -0
- package/dist/security/tokenAuthentication.d.ts +0 -1
- package/dist/security/tokenAuthentication.js +6 -2
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.d.ts +6 -1
- package/dist/security/user.js +23 -1
- package/dist/security/user.js.map +1 -1
- package/dist/server/REST.js +25 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +15 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +7 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +20 -1
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobRunner.js +10 -0
- package/dist/server/jobs/jobRunner.js.map +1 -1
- package/dist/server/jobs/jobs.js +11 -0
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +10 -1
- package/dist/server/middlewareChain.js +81 -21
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/operationsServer.d.ts +0 -2
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/multipartParser.js +9 -0
- package/dist/server/serverHelpers/multipartParser.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
- package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
- package/dist/server/serverHelpers/registeredOperations.js +14 -13
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +27 -7
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +21 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +68 -21
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +17 -0
- package/dist/server/storageReclamation.js +90 -6
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/socketRouter.js +20 -0
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +12 -0
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/diff/differential.js +7 -3
- package/dist/sqlEngine/diff/differential.js.map +1 -1
- package/dist/sqlTranslator/index.js +6 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/upgradePrompt.d.ts +2 -2
- package/dist/upgrade/upgradePrompt.js +22 -3
- package/dist/upgrade/upgradePrompt.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +25 -6
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +1 -0
- package/dist/utility/errors/commonErrors.js +1 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +13 -1
- package/dist/utility/hdbTerms.js +13 -1
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +32 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +47 -0
- package/dist/utility/logging/harper_logger.js +803 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/operation_authorization.js +13 -3
- package/dist/utility/operation_authorization.js.map +1 -1
- package/npm-shrinkwrap.json +198 -193
- package/package.json +12 -7
- package/resources/DESIGN.md +2 -0
- package/resources/DatabaseTransaction.ts +310 -104
- package/resources/ResourceInterface.ts +8 -5
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +341 -263
- package/resources/analytics/write.ts +22 -20
- package/resources/blob.ts +31 -14
- package/resources/databases.ts +387 -46
- package/resources/jsResource.ts +5 -62
- package/resources/models/Models.ts +14 -1
- package/resources/models/backendRegistry.ts +10 -0
- package/resources/models/openaiStream.ts +131 -19
- package/resources/models/v1/chatCompletions.ts +128 -0
- package/resources/models/v1/embeddings.ts +70 -0
- package/resources/models/v1/errors.ts +141 -0
- package/resources/models/v1/index.ts +72 -0
- package/resources/models/v1/models.ts +53 -0
- package/resources/models/v1/translation.ts +362 -0
- package/resources/roles.ts +67 -7
- package/resources/transaction.ts +0 -3
- package/security/jsLoader.ts +84 -30
- package/security/role.ts +7 -0
- package/security/superUserGuard.ts +20 -0
- package/security/tokenAuthentication.ts +6 -3
- package/security/user.ts +26 -1
- package/server/DESIGN.md +61 -34
- package/server/REST.ts +25 -9
- package/server/fastifyRoutes.ts +20 -1
- package/server/http.ts +100 -13
- package/server/itc/serverHandlers.js +7 -1
- package/server/jobs/jobProcess.ts +18 -1
- package/server/jobs/jobRunner.ts +10 -0
- package/server/jobs/jobs.ts +11 -0
- package/server/middlewareChain.ts +79 -20
- package/server/operationsServer.ts +0 -2
- package/server/serverHelpers/multipartParser.ts +9 -0
- package/server/serverHelpers/operationAuthorizationState.ts +11 -0
- package/server/serverHelpers/registeredOperations.ts +19 -15
- package/server/serverHelpers/serverHandlers.js +28 -7
- package/server/serverHelpers/serverUtilities.ts +29 -7
- package/server/static.ts +87 -23
- package/server/storageReclamation.ts +104 -8
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +3 -0
- package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
- package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
- package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
- package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
- package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
- package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
- package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
- package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
- package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
- package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
- package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
- package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
- package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
- package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
- package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
- package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
- package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
- package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
- package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
- package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
- package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
- package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
- package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/OperationFunctionCaller.ts +24 -3
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +13 -1
- package/utility/install/installer.ts +37 -0
- package/utility/logging/harper_logger.ts +786 -41
- package/utility/operation_authorization.ts +31 -3
- 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
|
-
|
|
149
|
-
|
|
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
|
|
178
|
-
//
|
|
179
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
!
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
286
|
+
setOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex), entry.absolutePath, null);
|
|
237
287
|
// With trailing slash; serves the index.html file
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
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);
|
package/sqlTranslator/index.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|