@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.2
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/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/DESIGN.md +421 -0
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/DESIGN.md +109 -0
- package/components/mcp/audit.ts +21 -17
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/DESIGN.md +306 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/DESIGN.md +179 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dataLayer/restoreMarker.ts +92 -25
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/audit.d.ts +2 -1
- package/dist/components/mcp/audit.js +21 -17
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +21 -8
- package/dist/dataLayer/restoreMarker.js +94 -27
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +46 -10
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1143 -348
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.d.ts +3 -0
- package/dist/resources/analytics/write.js +49 -16
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/crdt.d.ts +10 -0
- package/dist/resources/crdt.js +22 -0
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +625 -177
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +867 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
- package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2597 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/resources/transactionBroadcast.js +4 -6
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +59 -23
- package/dist/security/auth.js.map +1 -1
- package/dist/security/deferredAuthentication.d.ts +11 -0
- package/dist/security/deferredAuthentication.js +25 -3
- package/dist/security/deferredAuthentication.js.map +1 -1
- package/dist/security/jsLoader.js +9 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +39 -5
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/mqtt.js +5 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
- package/dist/server/serverHelpers/serverUtilities.js +110 -20
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +4 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +218 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +9 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +177 -87
- package/package.json +13 -5
- package/resources/DESIGN.md +689 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +52 -10
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1273 -396
- package/resources/analytics/DESIGN.md +38 -0
- package/resources/analytics/write.ts +50 -17
- package/resources/auditStore.ts +460 -11
- package/resources/crdt.ts +22 -0
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +611 -147
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/DESIGN.md +833 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +924 -46
- package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
- package/resources/indexes/fullTextNativeBinding.ts +146 -0
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/record-locks.md +1407 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3082 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/DESIGN.md +40 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +4 -4
- package/security/DESIGN.md +175 -0
- package/security/auth.ts +53 -24
- package/security/deferredAuthentication.ts +24 -2
- package/security/jsLoader.ts +9 -4
- package/server/DESIGN.md +275 -0
- package/server/REST.ts +42 -6
- package/server/http.ts +34 -2
- package/server/mqtt.ts +6 -4
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +157 -27
- package/server/serverHelpers/uwsServer.ts +4 -1
- package/server/serverHelpers/webSocketCloseReason.ts +25 -0
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-DADFFGe_.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-D_mI-rZ7.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-c2NiSDHa.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-DktUqh7G.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-Cs_75Xhw.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-19b-Y1IN.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-DiYEQMpB.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CmR0y7P_.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-Dm0rfkJ7.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-7vqt2CC3.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-BWtCFuOt.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-Buzzbv9y.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-SqsFWfTM.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-CAB-LZWT.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-DRzmSRxM.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-BNKAl79n.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-CUow_Zw2.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-Dkt3WUMp.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BZRfMD0N.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-D_yiEPO2.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-DhHh1Ge-.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-D7L1PDtV.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-CCwLk1YS.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-BP1j1mjn.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-D7bnYxLw.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-tOuCNT17.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-BSG_3mQT.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-DxiLprBB.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/DESIGN.md +55 -0
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +210 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
|
@@ -89,9 +89,9 @@ export type OperationFunctionName = ValueOf<typeof terms.OPERATIONS_ENUM>;
|
|
|
89
89
|
* name — still stripped, since this runs ahead of the validation that now rejects it). `value` /
|
|
90
90
|
* `values` carry .env secrets from set_env_value; `value` / `envelope` carry secrets from
|
|
91
91
|
* set_secret. `token` is the login-purpose token (login) and the CI identity token
|
|
92
|
-
* (exchange_oidc_token), and is also stripped defensively — no operation declares a top-level
|
|
93
|
-
* but validation allows unknown keys, so a mistyped `harper deploy setup token=…` must not
|
|
94
|
-
* credential. `refresh_token` is the 30-day credential (refresh_operation_token).
|
|
92
|
+
* (exchange_oidc_token), and is also stripped defensively — no operation declares a top-level
|
|
93
|
+
* `token`, but validation allows unknown keys, so a mistyped `harper deploy setup token=…` must not
|
|
94
|
+
* log a live credential. `refresh_token` is the 30-day credential (refresh_operation_token).
|
|
95
95
|
*
|
|
96
96
|
* Redaction runs *before* the handler, so a rejected request logs a still-spendable credential —
|
|
97
97
|
* which is why a new secret-bearing field belongs here rather than left to the default (harper#1527
|
|
@@ -111,10 +111,21 @@ export const UNLOGGABLE_OPERATION_FIELDS = [
|
|
|
111
111
|
'refresh_token',
|
|
112
112
|
];
|
|
113
113
|
|
|
114
|
+
const UNLOGGABLE_FIELDS_BY_OPERATION = new Map<string, ReadonlySet<string>>([
|
|
115
|
+
['add_ssh_key', new Set(['key'])],
|
|
116
|
+
['update_ssh_key', new Set(['key'])],
|
|
117
|
+
]);
|
|
118
|
+
|
|
114
119
|
/** Callers gate this on log level: it allocates, and the operations log is often off. */
|
|
115
120
|
export function redactForOperationLog(body: Record<string, any>): Record<string, any> {
|
|
116
121
|
const clean = { ...body };
|
|
117
122
|
for (const field of UNLOGGABLE_OPERATION_FIELDS) delete clean[field];
|
|
123
|
+
const operationFields = UNLOGGABLE_FIELDS_BY_OPERATION.get(body.operation);
|
|
124
|
+
if (operationFields) {
|
|
125
|
+
for (const field of Object.keys(clean)) {
|
|
126
|
+
if (operationFields.has(field.toLowerCase())) delete clean[field];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
118
129
|
return clean;
|
|
119
130
|
}
|
|
120
131
|
|
|
@@ -235,6 +246,17 @@ server.setMcpQuotaHandler = (handler) => {
|
|
|
235
246
|
setMcpQuotaHandler(handler);
|
|
236
247
|
};
|
|
237
248
|
|
|
249
|
+
// The nested `search_operation` operations dataLayer/export.ts's getRecords can actually run (its
|
|
250
|
+
// VALID_SEARCH_OPERATIONS). Validated at dispatch so an unsupported or missing nested operation is a
|
|
251
|
+
// request-time 400 rather than an async job failure, and so the nested authorization below always
|
|
252
|
+
// resolves a real handler.
|
|
253
|
+
const EXPORT_SEARCH_OPERATIONS = new Set<string>([
|
|
254
|
+
terms.OPERATIONS_ENUM.SEARCH_BY_VALUE,
|
|
255
|
+
terms.OPERATIONS_ENUM.SEARCH_BY_HASH,
|
|
256
|
+
terms.OPERATIONS_ENUM.SEARCH_BY_CONDITIONS,
|
|
257
|
+
terms.OPERATIONS_ENUM.SQL,
|
|
258
|
+
]);
|
|
259
|
+
|
|
238
260
|
export function chooseOperation(json: OperationRequestBody, bypassAuth = false) {
|
|
239
261
|
let getOpResult: OperationFunctionObject;
|
|
240
262
|
try {
|
|
@@ -255,32 +277,112 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
|
|
|
255
277
|
|
|
256
278
|
const { operation_function, job_operation_function } = getOpResult;
|
|
257
279
|
|
|
258
|
-
|
|
259
|
-
//
|
|
280
|
+
const isSqlOperation = json.operation === terms.OPERATIONS_ENUM.SQL;
|
|
281
|
+
// `verifyPerms` derives the tables it checks from the object it is handed, so the caller-supplied
|
|
282
|
+
// `search_operation` may only stand in as the permission subject for the operations that actually
|
|
283
|
+
// run it (dataLayer/export.ts is its only consumer). For anything else it would check the nested
|
|
284
|
+
// tables while the handler runs against the top-level ones.
|
|
285
|
+
const nestedSearch =
|
|
286
|
+
json.operation === terms.OPERATIONS_ENUM.EXPORT_LOCAL || json.operation === terms.OPERATIONS_ENUM.EXPORT_TO_S3
|
|
287
|
+
? json.search_operation
|
|
288
|
+
: undefined;
|
|
289
|
+
if (nestedSearch !== undefined) {
|
|
290
|
+
// It becomes the permission subject below, and a primitive would throw on the principal
|
|
291
|
+
// assignment — a 400 rather than a wrapped 500.
|
|
292
|
+
if (nestedSearch === null || typeof nestedSearch !== 'object' || Array.isArray(nestedSearch)) {
|
|
293
|
+
throw handleHDBError(
|
|
294
|
+
new Error(),
|
|
295
|
+
`'search_operation' must be an object`,
|
|
296
|
+
hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
|
|
297
|
+
undefined,
|
|
298
|
+
undefined,
|
|
299
|
+
true
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
// The object check alone lets `{}` and unsupported operations through — they would then fail
|
|
303
|
+
// asynchronously in the export worker (or dereference `search_operation.operation`). Require a
|
|
304
|
+
// supported operation at request time; the nested authorization below relies on it resolving.
|
|
305
|
+
if (typeof nestedSearch.operation !== 'string' || !EXPORT_SEARCH_OPERATIONS.has(nestedSearch.operation)) {
|
|
306
|
+
throw handleHDBError(
|
|
307
|
+
new Error(),
|
|
308
|
+
`'search_operation.operation' must be one of: ${[...EXPORT_SEARCH_OPERATIONS].join(', ')}`,
|
|
309
|
+
hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
|
|
310
|
+
undefined,
|
|
311
|
+
undefined,
|
|
312
|
+
true
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// Dispatch state, never client input: the export worker re-reads `parsed_sql_object` off this
|
|
316
|
+
// same object (evaluateSQL), and it carries `permissions_checked`, so a body-supplied one would
|
|
317
|
+
// execute an AST no check ever saw. Deleting it rather than replacing it with the authorized
|
|
318
|
+
// parse is deliberate — the worker then re-parses and re-runs the check itself.
|
|
319
|
+
delete nestedSearch.parsed_sql_object;
|
|
320
|
+
}
|
|
321
|
+
// The AST check below covers only the statement's tables, never the caller's right to invoke the
|
|
322
|
+
// job operation, so it is additive to verifyPerms rather than an alternative to it.
|
|
323
|
+
const hasNestedSqlSearch = nestedSearch?.operation === terms.OPERATIONS_ENUM.SQL;
|
|
324
|
+
|
|
260
325
|
try {
|
|
261
|
-
if (
|
|
326
|
+
if (isSqlOperation || hasNestedSqlSearch) {
|
|
262
327
|
const sql = require('../../sqlTranslator/index');
|
|
263
|
-
const sqlStatement =
|
|
328
|
+
const sqlStatement = isSqlOperation ? json.sql : nestedSearch.sql;
|
|
264
329
|
// Before this dispatch's own parse is assigned, so a body-supplied object cannot survive it.
|
|
265
330
|
stripSuppliedParsedSqlObject(json);
|
|
266
331
|
const parsedSqlObject = sql.convertSQLToAST(sqlStatement);
|
|
267
|
-
|
|
332
|
+
if (hasNestedSqlSearch && parsedSqlObject.variant !== terms.VALID_SQL_OPS_ENUM.SELECT) {
|
|
333
|
+
throw handleHDBError(
|
|
334
|
+
new Error(),
|
|
335
|
+
`'search_operation.sql' must be a SELECT statement`,
|
|
336
|
+
hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
|
|
337
|
+
undefined,
|
|
338
|
+
undefined,
|
|
339
|
+
true
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
// Only the direct-SQL path consumes `json.parsed_sql_object` (evaluateSQL). A nested export
|
|
343
|
+
// re-parses off `search_operation` — whose `parsed_sql_object` was deleted above — so setting
|
|
344
|
+
// it here for a job would be inert.
|
|
345
|
+
if (isSqlOperation) {
|
|
346
|
+
json.parsed_sql_object = parsedSqlObject;
|
|
347
|
+
}
|
|
268
348
|
if (!bypassAuth) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
349
|
+
if (hasNestedSqlSearch) {
|
|
350
|
+
// A nested-SQL export's outer op (export_local/export_to_s3) is `requires_su`, which the
|
|
351
|
+
// AST table check below does not enforce. Authorize invoking it through verifyPerms
|
|
352
|
+
// exactly as the non-SQL job path does — otherwise SQL is a way around the requires_su
|
|
353
|
+
// gate that path enforces. The nested table reads are then covered additively by the AST
|
|
354
|
+
// check. verifyPerms also runs the allowlist (gate 1) and the export token scope.
|
|
355
|
+
const functionToCheck = job_operation_function === undefined ? operation_function : job_operation_function;
|
|
356
|
+
const outerDenial = opAuth.verifyPerms(json, functionToCheck, { apiOperation: json.operation });
|
|
357
|
+
if (outerDenial) {
|
|
358
|
+
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
359
|
+
operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
|
|
360
|
+
throw handleHDBError(
|
|
361
|
+
new Error(),
|
|
362
|
+
outerDenial,
|
|
363
|
+
hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
|
|
364
|
+
undefined,
|
|
365
|
+
false,
|
|
366
|
+
true
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
// Direct SQL: the invoked operation IS `sql`, so the role `operations` allowlist is the
|
|
371
|
+
// operation-invocation check — otherwise an allowlisted role reaches unlisted operations
|
|
372
|
+
// via `sql`. json.operation is already the API name.
|
|
373
|
+
const allowlistDenial = opAuth.verifyOperationsAllowlist(json, json.operation);
|
|
374
|
+
if (allowlistDenial) {
|
|
375
|
+
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
376
|
+
operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
|
|
377
|
+
throw handleHDBError(
|
|
378
|
+
new Error(),
|
|
379
|
+
allowlistDenial,
|
|
380
|
+
hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
|
|
381
|
+
undefined,
|
|
382
|
+
undefined,
|
|
383
|
+
true
|
|
384
|
+
);
|
|
385
|
+
}
|
|
284
386
|
}
|
|
285
387
|
// `json.operation` explicitly — the operation this dispatch already resolved, not a
|
|
286
388
|
// field read back off the request body.
|
|
@@ -309,10 +411,9 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
|
|
|
309
411
|
json.operation !== terms.OPERATIONS_ENUM.EXCHANGE_OIDC_TOKEN
|
|
310
412
|
) {
|
|
311
413
|
const functionToCheck = job_operation_function === undefined ? operation_function : job_operation_function;
|
|
312
|
-
const operation_json =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
414
|
+
const operation_json = nestedSearch ?? json;
|
|
415
|
+
// Authentication is the only source of the principal; a nested one is overwritten, not honored.
|
|
416
|
+
operation_json.hdb_user = json.hdb_user;
|
|
316
417
|
|
|
317
418
|
// Pass the top-level operation for the token-scope check: for an export job, operation_json
|
|
318
419
|
// is the nested search_operation, so json.operation (export_local/export_to_s3) is the op the
|
|
@@ -335,6 +436,35 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
|
|
|
335
436
|
true
|
|
336
437
|
);
|
|
337
438
|
}
|
|
439
|
+
|
|
440
|
+
// The check above authorizes invoking the operation, but for a non-SQL export it returns
|
|
441
|
+
// before any table check — a requires_su export is granted the moment its `operations`
|
|
442
|
+
// allowlist lists it (verifyPerms gate 2) — and the job worker then runs `search_operation`
|
|
443
|
+
// through searchByValue/Hash/Conditions, none of which check permissions. So the nested read
|
|
444
|
+
// is only enforceable here: authorize it additively against its real search handler and the
|
|
445
|
+
// authenticated principal (assigned above via operation_json), so a role that may invoke the
|
|
446
|
+
// export but lacks read on the table is denied. A nested SQL search is covered by the AST
|
|
447
|
+
// branch above instead.
|
|
448
|
+
if (nestedSearch) {
|
|
449
|
+
const nestedFunction = getOperationFunction(nestedSearch).operation_function;
|
|
450
|
+
const nestedPermsResult = opAuth.verifyPerms(nestedSearch, nestedFunction, {
|
|
451
|
+
apiOperation: json.operation,
|
|
452
|
+
});
|
|
453
|
+
if (nestedPermsResult) {
|
|
454
|
+
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
455
|
+
operationLog.warn(
|
|
456
|
+
`User '${operation_json.hdb_user?.username}' is not permitted to ${nestedSearch.operation} within ${json.operation}`
|
|
457
|
+
);
|
|
458
|
+
throw handleHDBError(
|
|
459
|
+
new Error(),
|
|
460
|
+
nestedPermsResult,
|
|
461
|
+
hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
|
|
462
|
+
undefined,
|
|
463
|
+
false,
|
|
464
|
+
true
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
338
468
|
}
|
|
339
469
|
} catch (err) {
|
|
340
470
|
throw handleHDBError(err, `There was an error when trying to choose an operation path`, 500);
|
|
@@ -24,6 +24,7 @@ import { UwsRequest, UwsRequestBody } from './Request.ts';
|
|
|
24
24
|
import { Headers } from './Headers.ts';
|
|
25
25
|
import { when } from '../../utility/when.ts';
|
|
26
26
|
import { ClientError } from '../../utility/errors/hdbError.ts';
|
|
27
|
+
import { errorToString } from '../../utility/logging/harper_logger.ts';
|
|
27
28
|
|
|
28
29
|
// uWS has no npm package; it's installed from a GitHub tag and is platform/ABI-specific.
|
|
29
30
|
// Imported lazily so harper builds/loads on platforms without a uWS binary.
|
|
@@ -160,7 +161,9 @@ export async function createUwsServer(options: UwsServerOptions): Promise<{ app:
|
|
|
160
161
|
res.cork(() => {
|
|
161
162
|
res.writeStatus(statusText(status));
|
|
162
163
|
res.writeHeader('content-type', 'text/plain');
|
|
163
|
-
|
|
164
|
+
// the same renderer as the Node and Bun terminal handlers: the class name is the
|
|
165
|
+
// error code a client branches on, and it must not depend on the runtime
|
|
166
|
+
res.end(errorToString(error));
|
|
164
167
|
});
|
|
165
168
|
}
|
|
166
169
|
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A close frame's payload is at most 125 bytes, two of which are the status code, and `ws` throws a
|
|
2
|
+
// RangeError past that (node_modules/ws/lib/sender.js). Both close sites below are reached from a
|
|
3
|
+
// rejection handler, where such a throw would surface as an unhandled rejection rather than a failed
|
|
4
|
+
// close, so a message Harper does not control — a `server.getUser` override's, for instance — must be
|
|
5
|
+
// bounded before it gets there.
|
|
6
|
+
const MAX_CLOSE_REASON_BYTES = 123;
|
|
7
|
+
|
|
8
|
+
/** Bounds `text` to what a close frame accepts, truncating on a code-point boundary. */
|
|
9
|
+
export function toCloseReason(text: string | undefined): string {
|
|
10
|
+
const reason = text ?? '';
|
|
11
|
+
// a UTF-16 code unit never encodes to fewer than one byte, so an over-long string is over the byte
|
|
12
|
+
// limit too — testing that first keeps an unbounded message off byteLength's whole-string scan
|
|
13
|
+
if (reason.length <= MAX_CLOSE_REASON_BYTES && Buffer.byteLength(reason, 'utf8') <= MAX_CLOSE_REASON_BYTES)
|
|
14
|
+
return reason;
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
let end = 0;
|
|
17
|
+
// iterating the string yields whole code points, so a surrogate pair is never split
|
|
18
|
+
for (const character of reason) {
|
|
19
|
+
const size = Buffer.byteLength(character, 'utf8');
|
|
20
|
+
if (bytes + size > MAX_CLOSE_REASON_BYTES) break;
|
|
21
|
+
bytes += size;
|
|
22
|
+
end += character.length;
|
|
23
|
+
}
|
|
24
|
+
return reason.slice(0, end);
|
|
25
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { statfs } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { ownsStoreMaintenance } from '../server/threads/manageThreads.js';
|
|
5
5
|
import { logger } from '../utility/logging/logger.ts';
|
|
6
6
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
7
7
|
import * as envMgr from '../utility/environment/environmentManager.ts';
|
|
@@ -106,7 +106,7 @@ export function onStorageReclamation(
|
|
|
106
106
|
handler: (priority: number) => Promise<void> | void,
|
|
107
107
|
skipThreadCheck?: boolean
|
|
108
108
|
) {
|
|
109
|
-
if (skipThreadCheck ||
|
|
109
|
+
if (skipThreadCheck || ownsStoreMaintenance(path)) {
|
|
110
110
|
// only run on one thread (last one)
|
|
111
111
|
if (!path) {
|
|
112
112
|
throw new Error('Storage reclamation path cannot be empty');
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated applications: an application whose root-config entry carries `isolated: true` runs in a
|
|
3
|
+
* worker thread of its own that loads no other application (harper#642, tier 2).
|
|
4
|
+
*
|
|
5
|
+
* The invariant every helper here serves: an isolated application is loaded by exactly one thread,
|
|
6
|
+
* and that thread loads nothing else. The dedicated worker is an ordinary `http` worker by type --
|
|
7
|
+
* so restarts, overlap rules and shutdown treat it as one -- distinguished only by
|
|
8
|
+
* `workerData.isolatedApplication`. It binds none of the public ports (the kernel would hand it
|
|
9
|
+
* connections for every other application) and is reachable only through its own UDS mirror, whose
|
|
10
|
+
* metadata names the application and its hosts for the fronting proxy to route by.
|
|
11
|
+
*/
|
|
12
|
+
import { isMainThread, workerData } from 'node:worker_threads';
|
|
13
|
+
import { getWorkerIndex } from './manageThreads.js';
|
|
14
|
+
import { getConfigObj, getConfigPath } from '../../config/configUtils.ts';
|
|
15
|
+
import * as env from '../../utility/environment/environmentManager.ts';
|
|
16
|
+
import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
|
|
17
|
+
import { isDomainSocketPathTooLong } from '../../utility/domainSocket.ts';
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { basename, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_MAX_ISOLATED_APPLICATIONS = 8;
|
|
22
|
+
|
|
23
|
+
/** The application this thread is dedicated to, if it is a dedicated worker. */
|
|
24
|
+
export function thisThreadsIsolatedApplication(): string | undefined {
|
|
25
|
+
return (workerData as any)?.isolatedApplication;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function thisThreadOwnsApplication(
|
|
29
|
+
appName: string | undefined,
|
|
30
|
+
owner: string | undefined = thisThreadsIsolatedApplication()
|
|
31
|
+
): boolean {
|
|
32
|
+
return owner !== undefined && appName === owner;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isIsolatedApplication(
|
|
36
|
+
appName: string,
|
|
37
|
+
config: Record<string, any> | undefined = getConfigObj()
|
|
38
|
+
): boolean {
|
|
39
|
+
const entry = config?.[appName];
|
|
40
|
+
return typeof entry === 'object' && entry !== null && entry.isolated === true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Every application the root config marks isolated, in config order. */
|
|
44
|
+
export function isolatedApplicationNames(config: Record<string, any> | undefined = getConfigObj()): string[] {
|
|
45
|
+
if (!config) return [];
|
|
46
|
+
return Object.keys(config).filter((name) => isIsolatedApplication(name, config));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function presentIsolatedApplicationNames(
|
|
50
|
+
config: Record<string, any> | undefined = getConfigObj(),
|
|
51
|
+
componentsRoot: string = getConfigPath(CONFIG_PARAMS.COMPONENTSROOT) as string,
|
|
52
|
+
installedRoot: string = join(env.get(CONFIG_PARAMS.ROOTPATH), 'components'),
|
|
53
|
+
runApplicationPath: string | undefined = process.env.RUN_HDB_APP
|
|
54
|
+
): string[] {
|
|
55
|
+
return isolatedApplicationNames(config).filter(
|
|
56
|
+
(application) =>
|
|
57
|
+
existsSync(join(componentsRoot, application)) ||
|
|
58
|
+
existsSync(join(installedRoot, application)) ||
|
|
59
|
+
(runApplicationPath && basename(runApplicationPath) === application)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function maxIsolatedApplications(): number {
|
|
64
|
+
const configured = Number(env.get(CONFIG_PARAMS.THREADS_MAXISOLATED));
|
|
65
|
+
return Number.isInteger(configured) && configured >= 0 ? configured : DEFAULT_MAX_ISOLATED_APPLICATIONS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Why `appName` cannot claim a place in the dedicated-worker budget, if the budget is full. */
|
|
69
|
+
export function isolatedApplicationCapacityRefusal(
|
|
70
|
+
appName: string,
|
|
71
|
+
runningApplications: Iterable<string>,
|
|
72
|
+
max = maxIsolatedApplications()
|
|
73
|
+
): string | undefined {
|
|
74
|
+
const running = new Set(runningApplications);
|
|
75
|
+
if (running.has(appName) || running.size < max) return undefined;
|
|
76
|
+
return `the instance already runs ${max} isolated application(s) (threads.maxIsolated)`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether the calling thread is the one that loads the application `appName` (an application: a
|
|
81
|
+
* directory under componentsRoot, or a root-config entry with `package`). A dedicated worker loads
|
|
82
|
+
* only its own application; every other thread -- pool workers and the main thread alike -- loads only
|
|
83
|
+
* the applications that are not isolated. Decided before any of the application's modules are
|
|
84
|
+
* imported, so a skipped application has no side effects on the thread that skipped it.
|
|
85
|
+
*/
|
|
86
|
+
export function shouldLoadApplicationHere(
|
|
87
|
+
appName: string,
|
|
88
|
+
owner: string | undefined = thisThreadsIsolatedApplication(),
|
|
89
|
+
config: Record<string, any> | undefined = getConfigObj()
|
|
90
|
+
): boolean {
|
|
91
|
+
if (owner !== undefined) return appName === owner;
|
|
92
|
+
return !isIsolatedApplication(appName, config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether this thread starts the uWS listener for one `uwsServeConfigs` entry (see listenOnPorts()).
|
|
97
|
+
* A dedicated worker starts only its own per-application UDS mirrors -- the entries that carry a
|
|
98
|
+
* `socketPath` -- and never a global port listener, which the kernel would hand connections for every
|
|
99
|
+
* other application.
|
|
100
|
+
*/
|
|
101
|
+
export function shouldStartUwsListenerHere(
|
|
102
|
+
serveConfig: { socketPath?: string },
|
|
103
|
+
owner: string | undefined = thisThreadsIsolatedApplication()
|
|
104
|
+
): boolean {
|
|
105
|
+
return owner === undefined || Boolean(serveConfig.socketPath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether a dedicated worker can be reached at all: it binds only UDS mirrors of the secure port, so
|
|
110
|
+
* without `tls.unixDomainSockets` and a secure port an isolated application would load and answer
|
|
111
|
+
* nothing, silently. Refused at admission instead. Returns the reason, or undefined when reachable.
|
|
112
|
+
*/
|
|
113
|
+
export function isolatedApplicationsUnreachableReason(platform = process.platform): string | undefined {
|
|
114
|
+
if (platform === 'win32') return 'Windows does not support per-application UDS mirrors';
|
|
115
|
+
// the main thread standing in as the only worker (threads.count: 0) has no thread to give
|
|
116
|
+
if (isMainThread && getWorkerIndex() === 0) return 'threads.count is 0, so there is no worker thread to dedicate';
|
|
117
|
+
if (!env.get(CONFIG_PARAMS.HTTP_SECUREPORT)) return 'no http.securePort is configured';
|
|
118
|
+
if (!env.get(CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) return 'tls.unixDomainSockets is not enabled';
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Why `appName` cannot get a dedicated worker on this instance, or undefined when it can. */
|
|
123
|
+
export function isolatedApplicationRefusal(appName: string): string | undefined {
|
|
124
|
+
const unreachable = isolatedApplicationsUnreachableReason();
|
|
125
|
+
if (unreachable) return `its worker would be unreachable: ${unreachable}`;
|
|
126
|
+
const socketPath = join(
|
|
127
|
+
env.getHdbBasePath(),
|
|
128
|
+
'sockets',
|
|
129
|
+
`${applicationSocketName(appName, env.get(CONFIG_PARAMS.HTTP_SECUREPORT))}.sock`
|
|
130
|
+
);
|
|
131
|
+
if (isDomainSocketPathTooLong(socketPath)) return `its UDS mirror path ${socketPath} exceeds the platform limit`;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The filesystem name of an isolated worker's UDS mirror for `port`: every UTF-8 byte outside
|
|
137
|
+
* `[A-Za-z0-9._]` becomes a fixed-width `%XX`, which is injective (two application names never share
|
|
138
|
+
* a socket), and the `app-` prefix keeps it apart from the pool's `<workerIndex>-<port>` names.
|
|
139
|
+
*/
|
|
140
|
+
export function applicationSocketName(appName: string, port: number | string): string {
|
|
141
|
+
let encoded = '';
|
|
142
|
+
for (const byte of Buffer.from(appName, 'utf8')) {
|
|
143
|
+
const c = String.fromCharCode(byte);
|
|
144
|
+
encoded += byte < 0x80 && /[A-Za-z0-9._]/.test(c) ? c : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
145
|
+
}
|
|
146
|
+
return `app-${encoded}-${port}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What a dedicated worker publishes in its mirror metadata for the proxy to route by. */
|
|
150
|
+
export function isolatedApplicationRoute(
|
|
151
|
+
appName: string | undefined = thisThreadsIsolatedApplication(),
|
|
152
|
+
config: Record<string, any> | undefined = getConfigObj()
|
|
153
|
+
): { application: string; hosts: string[] } | undefined {
|
|
154
|
+
if (!appName) return undefined;
|
|
155
|
+
const host = config?.[appName]?.host; // a string, as the mount parser requires
|
|
156
|
+
return { application: appName, hosts: typeof host === 'string' && host ? [host] : [] };
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Hands the log-rotation coordinator the thread mesh it needs, from this side of the dependency
|
|
4
|
+
// edge: manageThreads already imports harper_logger, so logging can never import manageThreads.
|
|
5
|
+
|
|
6
|
+
import { threadId } from 'node:worker_threads';
|
|
7
|
+
import { setRotationTransport } from '../../utility/logging/logGenerationCoordinator.ts';
|
|
8
|
+
import { broadcast, onMessageByType, onThreadExit } from './manageThreads.js';
|
|
9
|
+
|
|
10
|
+
// Assigned to the `threads` global by manageThreads.
|
|
11
|
+
declare const threads: {
|
|
12
|
+
sendToThread(threadId: number, message: any): boolean;
|
|
13
|
+
[index: number]: { threadId?: number };
|
|
14
|
+
length: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
setRotationTransport({
|
|
18
|
+
threadId,
|
|
19
|
+
broadcast(message) {
|
|
20
|
+
broadcast(message);
|
|
21
|
+
},
|
|
22
|
+
sendToThread(target, message) {
|
|
23
|
+
threads.sendToThread(target, message);
|
|
24
|
+
},
|
|
25
|
+
onMessage(type, handler) {
|
|
26
|
+
onMessageByType(type, handler);
|
|
27
|
+
},
|
|
28
|
+
onThreadExit,
|
|
29
|
+
peerThreadIds() {
|
|
30
|
+
// Job workers included: they hold log descriptors whether or not they take part in ITC gossip,
|
|
31
|
+
// and the deadlock that excludes them from the schema broadcast cannot happen here — the
|
|
32
|
+
// handler only closes a descriptor, and no caller blocks its event loop on the answer.
|
|
33
|
+
const ids = new Set<number>();
|
|
34
|
+
for (let i = 0; i < threads.length; i++) {
|
|
35
|
+
const peer = threads[i].threadId;
|
|
36
|
+
if (peer !== undefined && peer !== threadId) ids.add(peer);
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
},
|
|
40
|
+
});
|