harper 5.1.20 → 5.2.0-alpha.5
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/agent/operations.ts +13 -16
- package/components/Application.ts +188 -7
- package/components/ApplicationScope.ts +3 -0
- package/components/Scope.ts +12 -0
- package/components/anthropic/index.ts +9 -2
- package/components/bedrock/index.ts +25 -2
- package/components/componentLoader.ts +56 -7
- package/components/componentSecrets.ts +451 -0
- package/components/deploymentRecorder.ts +7 -1
- package/components/mcp/adapters/fastify.ts +3 -0
- package/components/mcp/adapters/harperHttp.ts +3 -0
- package/components/mcp/audit.ts +21 -6
- package/components/mcp/customResourceRegistry.ts +17 -38
- package/components/mcp/listChanged.ts +37 -20
- package/components/mcp/quota.ts +145 -0
- package/components/mcp/rateLimit.ts +152 -14
- package/components/mcp/resources.ts +2 -4
- package/components/mcp/toolRegistry.ts +73 -4
- package/components/mcp/tools/application.ts +77 -8
- package/components/mcp/tools/operations.ts +106 -36
- package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
- package/components/mcp/tools/schemas/operations.ts +5 -0
- package/components/mcp/transport.ts +55 -4
- package/components/ollama/index.ts +6 -2
- package/components/openai/index.ts +9 -2
- package/components/operations.js +204 -10
- package/components/operationsValidation.js +173 -0
- package/components/secretOperations.ts +498 -0
- package/config/configUtils.ts +25 -1
- package/config-app.schema.json +81 -0
- package/config-root.schema.json +8 -1
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dataLayer/readAuditLog.ts +14 -0
- package/dataLayer/schemaDescribe.ts +1 -0
- package/dist/agent/operations.d.ts +7 -4
- package/dist/agent/operations.js +13 -15
- package/dist/agent/operations.js.map +1 -1
- package/dist/components/Application.d.ts +25 -2
- package/dist/components/Application.js +166 -7
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +2 -0
- package/dist/components/ApplicationScope.js +3 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/Scope.d.ts +8 -0
- package/dist/components/Scope.js +11 -0
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +4 -1
- package/dist/components/anthropic/index.js +4 -2
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +4 -1
- package/dist/components/bedrock/index.js +22 -2
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -0
- package/dist/components/componentLoader.js +52 -10
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentSecrets.d.ts +63 -0
- package/dist/components/componentSecrets.js +405 -0
- package/dist/components/componentSecrets.js.map +1 -0
- package/dist/components/deploymentRecorder.d.ts +3 -0
- package/dist/components/deploymentRecorder.js +4 -3
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/fastify.d.ts +2 -0
- package/dist/components/mcp/adapters/fastify.js +1 -0
- package/dist/components/mcp/adapters/fastify.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
- package/dist/components/mcp/adapters/harperHttp.js +1 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/audit.d.ts +2 -2
- package/dist/components/mcp/audit.js +20 -5
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
- package/dist/components/mcp/customResourceRegistry.js +15 -30
- package/dist/components/mcp/customResourceRegistry.js.map +1 -1
- package/dist/components/mcp/listChanged.d.ts +7 -2
- package/dist/components/mcp/listChanged.js +28 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/quota.d.ts +31 -0
- package/dist/components/mcp/quota.js +155 -0
- package/dist/components/mcp/quota.js.map +1 -0
- package/dist/components/mcp/rateLimit.d.ts +19 -3
- package/dist/components/mcp/rateLimit.js +129 -13
- package/dist/components/mcp/rateLimit.js.map +1 -1
- package/dist/components/mcp/resources.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +39 -1
- package/dist/components/mcp/toolRegistry.js +60 -5
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.d.ts +8 -1
- package/dist/components/mcp/tools/application.js +65 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +15 -3
- package/dist/components/mcp/tools/operations.js +99 -38
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +4 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/mcp/transport.d.ts +6 -0
- package/dist/components/mcp/transport.js +47 -3
- package/dist/components/mcp/transport.js.map +1 -1
- package/dist/components/ollama/index.d.ts +4 -1
- package/dist/components/ollama/index.js +4 -2
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +4 -1
- package/dist/components/openai/index.js +4 -2
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +39 -0
- package/dist/components/operations.js +194 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.d.ts +38 -0
- package/dist/components/operationsValidation.js +155 -0
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/secretOperations.d.ts +113 -0
- package/dist/components/secretOperations.js +475 -0
- package/dist/components/secretOperations.js.map +1 -0
- package/dist/config/configUtils.d.ts +4 -1
- package/dist/config/configUtils.js +18 -1
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/readAuditLog.js +11 -0
- package/dist/dataLayer/readAuditLog.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/globals.d.ts +1 -1
- package/dist/globals.js +1 -0
- package/dist/globals.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +38 -0
- package/dist/resources/DatabaseTransaction.d.ts +31 -3
- package/dist/resources/DatabaseTransaction.js +190 -29
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +1 -1
- package/dist/resources/LMDBTransaction.js +43 -5
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
- package/dist/resources/PrimaryRocksDatabase.js +194 -0
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
- package/dist/resources/RecordEncoder.d.ts +1 -0
- package/dist/resources/RecordEncoder.js +81 -75
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +19 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.d.ts +0 -1
- package/dist/resources/Resource.js +103 -16
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +6 -2
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -4
- package/dist/resources/Table.js +247 -24
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/read.js +25 -28
- package/dist/resources/analytics/read.js.map +1 -1
- package/dist/resources/blob.js +165 -22
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/crdt.d.ts +2 -0
- package/dist/resources/crdt.js +45 -36
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.d.ts +1 -0
- package/dist/resources/databases.js +134 -7
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/graphql.js +8 -0
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/jsResource.js +6 -0
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/loadEnv.js +30 -3
- package/dist/resources/loadEnv.js.map +1 -1
- package/dist/resources/login.js +2 -2
- package/dist/resources/login.js.map +1 -1
- package/dist/resources/models/Models.js +14 -3
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/embedHook.js +10 -2
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +56 -0
- package/dist/resources/models/openaiStream.js +94 -0
- package/dist/resources/models/openaiStream.js.map +1 -0
- package/dist/resources/replayLogs.js +4 -3
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +104 -11
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/secretDecryptor.d.ts +54 -0
- package/dist/resources/secretDecryptor.js +131 -0
- package/dist/resources/secretDecryptor.js.map +1 -0
- package/dist/resources/tracked.js +15 -5
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/auth.js +71 -8
- package/dist/security/auth.js.map +1 -1
- package/dist/security/jsLoader.js +11 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +9 -1
- package/dist/security/tokenAuthentication.js +31 -1
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
- package/dist/server/DurableSubscriptionsSession.js +8 -4
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +28 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +11 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -3
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +40 -2
- package/dist/server/http.js +560 -77
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +64 -0
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +12 -0
- package/dist/server/liveSubscriptionAuth.js +140 -0
- package/dist/server/liveSubscriptionAuth.js.map +1 -0
- package/dist/server/loadRootComponents.js +2 -1
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +44 -1
- package/dist/server/middlewareChain.js +86 -10
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/mqtt.js +2 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/operationsServer.js +1 -1
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +9 -0
- package/dist/server/serverHelpers/Headers.js +22 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/JSONStream.js +3 -3
- package/dist/server/serverHelpers/JSONStream.js.map +1 -1
- package/dist/server/serverHelpers/Request.d.ts +52 -1
- package/dist/server/serverHelpers/Request.js +122 -1
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +3 -8
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
- package/dist/server/serverHelpers/progressEmitter.js +52 -4
- package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
- package/dist/server/serverHelpers/registeredOperations.js +286 -0
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +11 -5
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
- package/dist/server/serverHelpers/serverUtilities.js +67 -5
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
- package/dist/server/serverHelpers/uwsServer.js +433 -0
- package/dist/server/serverHelpers/uwsServer.js.map +1 -0
- package/dist/server/static.d.ts +0 -15
- package/dist/server/static.js +161 -2
- package/dist/server/static.js.map +1 -1
- package/dist/server/status/index.d.ts +4 -1
- package/dist/server/status/index.js +64 -4
- package/dist/server/status/index.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +10 -0
- package/dist/server/threads/manageThreads.js +107 -4
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +5 -1
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +92 -14
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-2-0.d.ts +7 -0
- package/dist/upgrade/directives/5-2-0.js +108 -0
- package/dist/upgrade/directives/5-2-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/common_utils.js +5 -0
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/envFile.d.ts +41 -0
- package/dist/utility/envFile.js +221 -0
- package/dist/utility/envFile.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +25 -0
- package/dist/utility/hdbTerms.js +31 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +21 -0
- package/dist/utility/logging/harper_logger.js +160 -8
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logRotator.js +29 -17
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/logging/readLog.d.ts +1 -1
- package/dist/utility/logging/readLog.js +305 -2
- package/dist/utility/logging/readLog.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +18 -0
- package/dist/utility/operationPermissions.js +35 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +17 -0
- package/dist/utility/operation_authorization.js +38 -0
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/secretEnvelope.d.ts +30 -0
- package/dist/utility/secretEnvelope.js +94 -0
- package/dist/utility/secretEnvelope.js.map +1 -0
- package/dist/utility/signalling.d.ts +7 -0
- package/dist/utility/signalling.js +16 -0
- package/dist/utility/signalling.js.map +1 -1
- package/dist/validation/configValidator.js +70 -24
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +4 -0
- package/json/systemSchema.json +38 -0
- package/npm-shrinkwrap.json +15066 -9347
- package/package.json +13 -8
- package/resources/DESIGN.md +15 -12
- package/resources/DatabaseTransaction.ts +206 -37
- package/resources/LMDBTransaction.ts +43 -9
- package/resources/PrimaryRocksDatabase.ts +201 -0
- package/resources/RecordEncoder.ts +81 -69
- package/resources/RequestTarget.ts +21 -0
- package/resources/Resource.ts +109 -25
- package/resources/ResourceInterface.ts +6 -2
- package/resources/Resources.ts +1 -3
- package/resources/Table.ts +238 -29
- package/resources/analytics/read.ts +25 -30
- package/resources/blob.ts +159 -23
- package/resources/crdt.ts +37 -31
- package/resources/databases.ts +143 -6
- package/resources/graphql.ts +8 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
- package/resources/jsResource.ts +6 -0
- package/resources/loadEnv.ts +34 -3
- package/resources/login.ts +2 -2
- package/resources/models/Models.ts +19 -3
- package/resources/models/embedHook.ts +13 -2
- package/resources/models/openaiStream.ts +133 -0
- package/resources/replayLogs.ts +4 -3
- package/resources/search.ts +113 -11
- package/resources/secretDecryptor.ts +159 -0
- package/resources/tracked.ts +24 -17
- package/security/auth.ts +70 -10
- package/security/jsLoader.ts +12 -0
- package/security/tokenAuthentication.ts +42 -1
- package/server/DESIGN.md +10 -0
- package/server/DurableSubscriptionsSession.ts +8 -4
- package/server/REST.ts +26 -9
- package/server/fastifyRoutes.ts +10 -1
- package/server/graphqlQuerying.ts +4 -3
- package/server/http.ts +563 -77
- package/server/itc/serverHandlers.js +70 -0
- package/server/liveSubscriptionAuth.ts +152 -0
- package/server/loadRootComponents.js +2 -1
- package/server/middlewareChain.ts +107 -17
- package/server/mqtt.ts +2 -1
- package/server/operationsServer.ts +2 -2
- package/server/serverHelpers/Headers.ts +24 -0
- package/server/serverHelpers/JSONStream.ts +3 -3
- package/server/serverHelpers/Request.ts +127 -0
- package/server/serverHelpers/contentTypes.ts +3 -8
- package/server/serverHelpers/progressEmitter.ts +55 -5
- package/server/serverHelpers/registeredOperations.ts +259 -0
- package/server/serverHelpers/serverHandlers.js +11 -5
- package/server/serverHelpers/serverUtilities.ts +88 -5
- package/server/serverHelpers/uwsServer.ts +488 -0
- package/server/static.ts +180 -2
- package/server/status/index.ts +72 -4
- package/server/threads/manageThreads.js +102 -4
- package/server/threads/socketRouter.ts +5 -1
- package/server/threads/threadServer.js +93 -10
- package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
- package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
- package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
- package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
- package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
- package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
- package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
- package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
- package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
- package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
- package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
- package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
- package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
- package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
- package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
- package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
- package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
- package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-2-0.ts +82 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/common_utils.ts +5 -0
- package/utility/envFile.ts +218 -0
- package/utility/hdbTerms.ts +31 -0
- package/utility/logging/harper_logger.ts +156 -8
- package/utility/logging/logRotator.ts +24 -14
- package/utility/logging/readLog.ts +323 -2
- package/utility/operationPermissions.ts +35 -1
- package/utility/operation_authorization.ts +74 -1
- package/utility/secretEnvelope.ts +113 -0
- package/utility/signalling.ts +15 -0
- package/validation/configValidator.ts +78 -25
- package/studio/web/assets/index-VFcpNWgq.js.map +0 -1
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Operations against system.hdb_secret — the envelope-encrypted secrets store (#715).
|
|
4
|
+
//
|
|
5
|
+
// Only ciphertext is ever stored: a plaintext `value` submitted to set_secret is encrypted
|
|
6
|
+
// immediately with the cluster public key (custody must be registered) and the plaintext is
|
|
7
|
+
// discarded; processLocalTransaction strips `value`/`envelope` from the operations log before
|
|
8
|
+
// dispatch. Rows reach peers via normal system-table replication — these operations deliberately
|
|
9
|
+
// do NOT call replicateOperation (op replication on top of table replication would double-apply).
|
|
10
|
+
//
|
|
11
|
+
// All operations are super_user only, enforced in-handler in addition to the requiredPermissions
|
|
12
|
+
// registration: an `operations` allowlist on a role can otherwise delegate SU-only ops (see the
|
|
13
|
+
// gate-2 bypass in utility/operation_authorization.ts), and secret custody must not be delegable.
|
|
14
|
+
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { databases } from '../resources/databases.ts';
|
|
18
|
+
import * as configUtils from '../config/configUtils.ts';
|
|
19
|
+
import * as terms from '../utility/hdbTerms.ts';
|
|
20
|
+
import { ClientError, handleHDBError, hdbErrors } from '../utility/errors/hdbError.ts';
|
|
21
|
+
import logger from '../utility/logging/harper_logger.ts';
|
|
22
|
+
import * as validator from './operationsValidation.js';
|
|
23
|
+
import { getSecretCustody } from '../resources/secretDecryptor.ts';
|
|
24
|
+
import { encryptEnvelope, parseEnvelopeFields } from '../utility/secretEnvelope.ts';
|
|
25
|
+
import { ENV_ENCRYPTED_PREFIX } from '../utility/envFile.ts';
|
|
26
|
+
|
|
27
|
+
const { HTTP_STATUS_CODES } = hdbErrors;
|
|
28
|
+
const SECRET_TABLE = terms.SYSTEM_TABLE_NAMES.SECRET_TABLE_NAME;
|
|
29
|
+
|
|
30
|
+
function requireSuperUser(req: any): void {
|
|
31
|
+
if (!req?.hdb_user?.role?.permission?.super_user) {
|
|
32
|
+
throw new ClientError(
|
|
33
|
+
`Operation '${req?.operation}' is restricted to super_user roles`,
|
|
34
|
+
HTTP_STATUS_CODES.FORBIDDEN
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validate(validation: any): void {
|
|
40
|
+
if (validation) {
|
|
41
|
+
throw handleHDBError(validation, validation.message, HTTP_STATUS_CODES.BAD_REQUEST);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function secretTable() {
|
|
46
|
+
const table = (databases as any).system?.[SECRET_TABLE];
|
|
47
|
+
if (!table) {
|
|
48
|
+
throw new ClientError(
|
|
49
|
+
`The secrets store is not initialized on this node (system.${SECRET_TABLE} missing). ` +
|
|
50
|
+
`Run upgrade or restart the server to provision the table.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return table;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Grants are a set: duplicates would let one revoke leave a residual grant behind, so every write
|
|
57
|
+
// path normalizes through this (including rows that arrived dirty via replication).
|
|
58
|
+
function dedupeGrants(grants: any): string[] {
|
|
59
|
+
return Array.isArray(grants) ? [...new Set(grants)] : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Rebuild a plain record from a stored row's known attributes (never spread rows — RecordObject
|
|
63
|
+
// prototype fields don't survive a spread reliably; see DESIGN.md).
|
|
64
|
+
function toRecord(row: any) {
|
|
65
|
+
return {
|
|
66
|
+
name: row.name,
|
|
67
|
+
envelope: row.envelope,
|
|
68
|
+
kid: row.kid ?? null,
|
|
69
|
+
grants: dedupeGrants(row.grants),
|
|
70
|
+
processEnv: !!row.processEnv,
|
|
71
|
+
metadata: row.metadata ?? {},
|
|
72
|
+
unverified: !!row.unverified,
|
|
73
|
+
updated_by: row.updated_by ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// True when a directory with the component's name exists under the components root. Advisory
|
|
78
|
+
// only (audit breadcrumb) — must never block or throw.
|
|
79
|
+
function componentIsKnown(component: string): boolean {
|
|
80
|
+
try {
|
|
81
|
+
return existsSync(join(configUtils.getConfigPath(terms.CONFIG_PARAMS.COMPONENTSROOT), component));
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Serialize read-modify-write mutations per secret name. The operations API dispatches these
|
|
88
|
+
// handlers on the MAIN thread only, so an in-process promise chain is sufficient to make the
|
|
89
|
+
// get→mutate→put of set_secret/grant_secret/revoke_secret/delete_secret atomic against local
|
|
90
|
+
// concurrency (without it, overlapping grant/revoke calls can silently drop each other's
|
|
91
|
+
// mutation). Cross-node concurrency remains last-write-wins on the whole row — the known
|
|
92
|
+
// system-table replication semantic.
|
|
93
|
+
const nameLocks = new Map<string, Promise<void>>();
|
|
94
|
+
async function withSecretLock<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
|
95
|
+
const prior = nameLocks.get(name) ?? Promise.resolve();
|
|
96
|
+
const run = prior.then(fn);
|
|
97
|
+
// The stored tail never rejects, so a failed mutation doesn't poison the chain.
|
|
98
|
+
const tail = run.then(
|
|
99
|
+
() => undefined,
|
|
100
|
+
() => undefined
|
|
101
|
+
);
|
|
102
|
+
nameLocks.set(name, tail);
|
|
103
|
+
try {
|
|
104
|
+
return await run;
|
|
105
|
+
} finally {
|
|
106
|
+
if (nameLocks.get(name) === tail) nameLocks.delete(name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Structured audit event for every mutation — operation, secret name, and user, never values.
|
|
111
|
+
function notifyMutation(operation: string, name: string, req: any, detail?: string) {
|
|
112
|
+
logger.notify(
|
|
113
|
+
`secret mutation: op=${operation} name=${name} user=${req?.hdb_user?.username ?? 'unknown'}${detail ? ` ${detail}` : ''}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Upsert a secret. Exactly one of `value` (plaintext, encrypted immediately — requires custody)
|
|
119
|
+
* or `envelope` (`enc:v1:...` ciphertext, encrypted client-side against get_secrets_public_key).
|
|
120
|
+
* `kid` is derived from the envelope server-side; a client-supplied kid field is never trusted.
|
|
121
|
+
* Never returns or logs plaintext.
|
|
122
|
+
*
|
|
123
|
+
* The delivery tier is explicit and mutually exclusive (a process-env secret is already global, so
|
|
124
|
+
* scoping it is meaningless):
|
|
125
|
+
* - `processEnv: true` — materialized into the real `process.env` at component load, inherited by
|
|
126
|
+
* child processes (`.env` semantics). No isolation promise.
|
|
127
|
+
* - `grants: [...]` — scoped: never in `process.env`, exposed only to the listed components via the
|
|
128
|
+
* `secrets` accessor.
|
|
129
|
+
* Both default to the stored row so a value rotation preserves the tier without re-specifying it.
|
|
130
|
+
*/
|
|
131
|
+
export async function setSecret(req: any) {
|
|
132
|
+
requireSuperUser(req);
|
|
133
|
+
validate(validator.setSecretValidator(req));
|
|
134
|
+
|
|
135
|
+
const table = secretTable();
|
|
136
|
+
const { name } = req;
|
|
137
|
+
const custody = getSecretCustody();
|
|
138
|
+
let envelope: string;
|
|
139
|
+
let kid: string | null;
|
|
140
|
+
let unverified = false;
|
|
141
|
+
|
|
142
|
+
if (req.value !== undefined) {
|
|
143
|
+
if (!custody) {
|
|
144
|
+
throw new ClientError('secrets custody is not initialized on this node');
|
|
145
|
+
}
|
|
146
|
+
const { publicKey, fingerprint } = custody.getPublicKey();
|
|
147
|
+
envelope = ENV_ENCRYPTED_PREFIX + encryptEnvelope(req.value, publicKey, fingerprint);
|
|
148
|
+
kid = fingerprint;
|
|
149
|
+
delete req.value; // plaintext is now sealed; drop the reference
|
|
150
|
+
} else {
|
|
151
|
+
// Deliberately NEVER decrypted server-side: a decrypt-verify would put the plaintext in
|
|
152
|
+
// this thread's heap, defeating client-side encryption. Ingest checks are structural
|
|
153
|
+
// (marker, base64url JSON, k/iv/ct/tag base64 fields) plus a kid-vs-custody fingerprint
|
|
154
|
+
// match; a tampered-but-well-formed envelope is only caught at consumption time.
|
|
155
|
+
envelope = req.envelope;
|
|
156
|
+
if (!envelope.startsWith(ENV_ENCRYPTED_PREFIX)) {
|
|
157
|
+
throw new ClientError(`'envelope' must start with '${ENV_ENCRYPTED_PREFIX}'`);
|
|
158
|
+
}
|
|
159
|
+
let fields;
|
|
160
|
+
try {
|
|
161
|
+
fields = parseEnvelopeFields(envelope.slice(ENV_ENCRYPTED_PREFIX.length));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw new ClientError(`Invalid secret envelope: ${(error as Error).message}`);
|
|
164
|
+
}
|
|
165
|
+
kid = fields.kid ?? null;
|
|
166
|
+
if (custody) {
|
|
167
|
+
const { fingerprint } = custody.getPublicKey();
|
|
168
|
+
if (kid !== null && kid !== fingerprint) {
|
|
169
|
+
throw new ClientError(
|
|
170
|
+
`Secret envelope kid '${kid}' does not match this cluster's secrets key ` +
|
|
171
|
+
`(expected '${fingerprint}'; fetch it via get_secrets_public_key)`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (kid === null) {
|
|
175
|
+
// No sealed kid to check against custody — accept, but surface it in list_secrets.
|
|
176
|
+
unverified = true;
|
|
177
|
+
logger.warn(`set_secret '${name}': envelope carries no kid; accepted without key-identity verification`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// No custody on this node: accept, but mark the row so a bad envelope is visible in
|
|
181
|
+
// list_secrets instead of silently failing later.
|
|
182
|
+
unverified = true;
|
|
183
|
+
logger.warn(
|
|
184
|
+
`set_secret '${name}': envelope accepted without verification (no secrets custody registered on this node)`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return withSecretLock(name, async () => {
|
|
190
|
+
const existing = await table.get(name);
|
|
191
|
+
const processEnv = req.processEnv ?? existing?.processEnv ?? false;
|
|
192
|
+
const grants = dedupeGrants(req.grants ?? existing?.grants);
|
|
193
|
+
if (processEnv && grants.length > 0) {
|
|
194
|
+
throw new ClientError(
|
|
195
|
+
`Secret '${name}' cannot be both processEnv and grant-scoped — a processEnv secret is global. ` +
|
|
196
|
+
`Set processEnv:false to scope it with grants, or omit grants.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
await table.put({
|
|
200
|
+
name,
|
|
201
|
+
envelope,
|
|
202
|
+
kid,
|
|
203
|
+
grants: processEnv ? [] : grants,
|
|
204
|
+
processEnv,
|
|
205
|
+
metadata: req.metadata ?? existing?.metadata ?? {},
|
|
206
|
+
unverified,
|
|
207
|
+
updated_by: req.hdb_user?.username ?? null,
|
|
208
|
+
});
|
|
209
|
+
notifyMutation(terms.OPERATIONS_ENUM.SET_SECRET, name, req, unverified ? 'unverified=true' : undefined);
|
|
210
|
+
return { name, kid, created: existing == null };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function mutateGrants(req: any, grant: boolean) {
|
|
215
|
+
requireSuperUser(req);
|
|
216
|
+
validate(validator.grantSecretValidator(req));
|
|
217
|
+
|
|
218
|
+
const table = secretTable();
|
|
219
|
+
return withSecretLock(req.name, async () => {
|
|
220
|
+
const row = await table.get(req.name);
|
|
221
|
+
if (!row) {
|
|
222
|
+
throw new ClientError(`No secret found with name '${req.name}'`, HTTP_STATUS_CODES.NOT_FOUND);
|
|
223
|
+
}
|
|
224
|
+
const record = toRecord(row); // record.grants comes back deduped
|
|
225
|
+
// A processEnv secret is global; granting/revoking a component scope is contradictory. Revoke
|
|
226
|
+
// is a no-op on an already-empty grants list, so only the grant path needs to reject loudly.
|
|
227
|
+
if (grant && record.processEnv) {
|
|
228
|
+
throw new ClientError(
|
|
229
|
+
`Secret '${req.name}' is a processEnv (global) secret and cannot be scoped to a component. ` +
|
|
230
|
+
`Re-run set_secret with processEnv:false to convert it to a scoped secret first.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const present = record.grants.includes(req.component);
|
|
234
|
+
const storedLength = Array.isArray(row.grants) ? row.grants.length : 0;
|
|
235
|
+
// No-op short-circuit: nothing to write, nothing to audit, no updated_by/__updatedtime__
|
|
236
|
+
// bump. A stored row with duplicate grants (dirty state) is NOT a no-op — falling through
|
|
237
|
+
// persists the normalized set.
|
|
238
|
+
if ((grant ? present : !present) && record.grants.length === storedLength) {
|
|
239
|
+
return { name: req.name, grants: record.grants, changed: false };
|
|
240
|
+
}
|
|
241
|
+
if (grant && !present) record.grants.push(req.component);
|
|
242
|
+
// Remove every occurrence — with the dedupe this is belt-and-braces against dirty rows.
|
|
243
|
+
if (!grant) record.grants = record.grants.filter((component) => component !== req.component);
|
|
244
|
+
record.updated_by = req.hdb_user?.username ?? null;
|
|
245
|
+
await table.put(record);
|
|
246
|
+
// Granting to a not-yet-deployed component is legal (grants may precede the deploy), but an
|
|
247
|
+
// unknown name is worth a breadcrumb in the audit event — it is usually a typo.
|
|
248
|
+
let detail = `component=${req.component}`;
|
|
249
|
+
if (grant && !componentIsKnown(req.component)) detail += ' component_known=false';
|
|
250
|
+
notifyMutation(
|
|
251
|
+
grant ? terms.OPERATIONS_ENUM.GRANT_SECRET : terms.OPERATIONS_ENUM.REVOKE_SECRET,
|
|
252
|
+
req.name,
|
|
253
|
+
req,
|
|
254
|
+
detail
|
|
255
|
+
);
|
|
256
|
+
return { name: req.name, grants: record.grants, changed: true };
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Add a component to a secret's grants list (idempotent). */
|
|
261
|
+
export function grantSecret(req: any) {
|
|
262
|
+
return mutateGrants(req, true);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Remove a component from a secret's grants list (idempotent). */
|
|
266
|
+
export function revokeSecret(req: any) {
|
|
267
|
+
return mutateGrants(req, false);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* List secret metadata: never envelopes, never values. Includes the node's current custody
|
|
272
|
+
* fingerprint (null when custody is absent) and a per-row kid_matches_custody flag so stale rows
|
|
273
|
+
* on a cloned/rekeyed node are immediately visible.
|
|
274
|
+
*/
|
|
275
|
+
export async function listSecrets(req: any) {
|
|
276
|
+
requireSuperUser(req);
|
|
277
|
+
|
|
278
|
+
const table = secretTable();
|
|
279
|
+
const custody = getSecretCustody();
|
|
280
|
+
const custodyFingerprint = custody ? custody.getPublicKey().fingerprint : null;
|
|
281
|
+
const secrets: any[] = [];
|
|
282
|
+
for await (const row of table.search([])) {
|
|
283
|
+
secrets.push({
|
|
284
|
+
name: row.name,
|
|
285
|
+
kid: row.kid ?? null,
|
|
286
|
+
grants: Array.isArray(row.grants) ? [...row.grants] : [],
|
|
287
|
+
processEnv: !!row.processEnv,
|
|
288
|
+
metadata: row.metadata ?? {},
|
|
289
|
+
unverified: !!row.unverified,
|
|
290
|
+
updated_by: row.updated_by ?? null,
|
|
291
|
+
__createdtime__: row.__createdtime__,
|
|
292
|
+
__updatedtime__: row.__updatedtime__,
|
|
293
|
+
kid_matches_custody: custodyFingerprint !== null && row.kid === custodyFingerprint,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
secrets.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
297
|
+
return { secrets, custody_fingerprint: custodyFingerprint };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Delete a secret row. Not cryptographic erasure — audit/txn logs and backups retain envelopes. */
|
|
301
|
+
export async function deleteSecret(req: any) {
|
|
302
|
+
requireSuperUser(req);
|
|
303
|
+
validate(validator.deleteSecretValidator(req));
|
|
304
|
+
|
|
305
|
+
const table = secretTable();
|
|
306
|
+
return withSecretLock(req.name, async () => {
|
|
307
|
+
const row = await table.get(req.name);
|
|
308
|
+
if (!row) {
|
|
309
|
+
throw new ClientError(`No secret found with name '${req.name}'`, HTTP_STATUS_CODES.NOT_FOUND);
|
|
310
|
+
}
|
|
311
|
+
await table.delete(req.name);
|
|
312
|
+
notifyMutation(terms.OPERATIONS_ENUM.DELETE_SECRET, req.name, req);
|
|
313
|
+
return { message: `Successfully deleted secret '${req.name}'` };
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The cluster secrets public key, for client-side envelope encryption. */
|
|
318
|
+
export function getSecretsPublicKey(req: any) {
|
|
319
|
+
requireSuperUser(req);
|
|
320
|
+
const custody = getSecretCustody();
|
|
321
|
+
if (!custody) {
|
|
322
|
+
throw new ClientError('secrets custody is not initialized on this node');
|
|
323
|
+
}
|
|
324
|
+
const { publicKey, fingerprint } = custody.getPublicKey();
|
|
325
|
+
return { public_key: publicKey, fingerprint };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A resolved registry-auth entry as consumed by the transient .npmrc writer. */
|
|
329
|
+
export interface ResolvedRegistryAuthEntry {
|
|
330
|
+
registry: string;
|
|
331
|
+
token: string;
|
|
332
|
+
scope?: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A registry-auth entry after ingestion: a reference into hdb_secret, never a token. */
|
|
336
|
+
export interface RegistryAuthReference {
|
|
337
|
+
registry: string;
|
|
338
|
+
secret: string;
|
|
339
|
+
scope?: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Deterministic name for the auto-minted secret backing a literal registry token: keyed by the
|
|
344
|
+
* deploying component and the registry, so re-supplying (or rotating) the token on a later deploy
|
|
345
|
+
* overwrites the same row rather than accumulating one per deploy. Sanitized to the set_secret name
|
|
346
|
+
* grammar (`\w.-`), since a registry can carry a scheme, port, or path.
|
|
347
|
+
*/
|
|
348
|
+
export function deriveRegistrySecretName(component: string, registry: string): string {
|
|
349
|
+
const registryKey = registry
|
|
350
|
+
.trim()
|
|
351
|
+
.replace(/^https?:\/\//i, '')
|
|
352
|
+
.replace(/^\/\//, '')
|
|
353
|
+
.replace(/\/+$/, '')
|
|
354
|
+
.toLowerCase()
|
|
355
|
+
.replace(/[^\w.-]+/g, '_');
|
|
356
|
+
const componentKey = String(component).replace(/[^\w.-]+/g, '_');
|
|
357
|
+
return `deploy.${componentKey}.${registryKey}`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Ingest deploy_component `registryAuth` into the secrets store so a provided registry token lives
|
|
362
|
+
* as ciphertext in the replicated, audited `hdb_secret` store (reference, not embed) rather than
|
|
363
|
+
* travelling in the operation body. Returns the entries in reference form (`{ registry, secret }`).
|
|
364
|
+
*
|
|
365
|
+
* - A literal `{ registry, token }` entry is encrypted (via `set_secret`, custody required) under a
|
|
366
|
+
* derived name granted to `component`, and returned as a reference. Overwrites an existing
|
|
367
|
+
* derived row so token rotation is idempotent.
|
|
368
|
+
* - An already-reference `{ registry, secret }` entry passes through unchanged (peers re-running a
|
|
369
|
+
* replicated deploy already carry references — they never re-ingest).
|
|
370
|
+
* - With no custody on this node (OSS core, or key not held here), literal tokens CANNOT be sealed;
|
|
371
|
+
* they pass through untouched so the caller can fall back to the transient, this-node-only path.
|
|
372
|
+
* The reference-form result therefore contains only entries the caller may persist and replicate.
|
|
373
|
+
*
|
|
374
|
+
* Runs on the deploying main thread where custody is registered.
|
|
375
|
+
*/
|
|
376
|
+
export async function ingestRegistryAuth(req: any, registryAuth: any[] | undefined, component: string): Promise<any[]> {
|
|
377
|
+
if (!Array.isArray(registryAuth) || registryAuth.length === 0) return registryAuth ?? [];
|
|
378
|
+
const custody = getSecretCustody();
|
|
379
|
+
const out: any[] = [];
|
|
380
|
+
for (const entry of registryAuth) {
|
|
381
|
+
// Already a reference (or nothing to seal without custody): leave as-is.
|
|
382
|
+
if (entry.secret !== undefined || !custody) {
|
|
383
|
+
out.push(entry);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const name = deriveRegistrySecretName(component, entry.registry);
|
|
387
|
+
// Reuse set_secret's seal-and-store path (encrypt with custody, grant to the component, audit
|
|
388
|
+
// the mutation). The deploy request is already super_user, which set_secret requires.
|
|
389
|
+
await setSecret({
|
|
390
|
+
operation: terms.OPERATIONS_ENUM.SET_SECRET,
|
|
391
|
+
hdb_user: req?.hdb_user,
|
|
392
|
+
name,
|
|
393
|
+
value: entry.token,
|
|
394
|
+
grants: [component],
|
|
395
|
+
processEnv: false,
|
|
396
|
+
});
|
|
397
|
+
out.push(
|
|
398
|
+
entry.scope === undefined
|
|
399
|
+
? { registry: entry.registry, secret: name }
|
|
400
|
+
: { registry: entry.registry, secret: name, scope: entry.scope }
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Wait (bounded) for a secret row to appear — covers the replicated-deploy race where a peer runs
|
|
407
|
+
* the deploy before the origin's hdb_secret row has replicated in. */
|
|
408
|
+
async function waitForSecretRow(table: any, name: string, waitMs: number): Promise<any> {
|
|
409
|
+
const deadline = Date.now() + waitMs;
|
|
410
|
+
let row = await table.get(name);
|
|
411
|
+
while (!row && Date.now() < deadline) {
|
|
412
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(200, Math.max(1, waitMs))));
|
|
413
|
+
row = await table.get(name);
|
|
414
|
+
}
|
|
415
|
+
return row;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Resolve deploy_component `registryAuth` entries that reference a stored secret
|
|
420
|
+
* (`{ registry, secret }`) into concrete token entries (`{ registry, token }`) by decrypting the
|
|
421
|
+
* named hdb_secret row on this thread. Entries that already carry a literal `token` pass through
|
|
422
|
+
* unchanged, so this is a no-op for the token-only fallback form.
|
|
423
|
+
*
|
|
424
|
+
* This is intentionally NOT a `get_secret` operation — the store never returns plaintext across the
|
|
425
|
+
* API boundary. The resolved token is handed back to the install path in-memory, fed to the
|
|
426
|
+
* transient .npmrc, and never written back to the request body, replicated, or logged.
|
|
427
|
+
*
|
|
428
|
+
* Authority mirrors the accessor model (#1550): the referenced secret must be usable by the
|
|
429
|
+
* component being deployed — either a `processEnv` (global) secret or a scoped secret granted to
|
|
430
|
+
* `component`. Without that, the deploy path would let a super_user pull an arbitrary scoped secret
|
|
431
|
+
* into an .npmrc, sidestepping the grant that is the store's authority. A missing row, missing
|
|
432
|
+
* grant, absent custody, or decrypt failure throws a ClientError naming the precise reason.
|
|
433
|
+
*
|
|
434
|
+
* `options.waitMs` gives a bounded grace period for a referenced row to replicate in (used on the
|
|
435
|
+
* peer side of a replicated deploy, where the deploy op can arrive just ahead of the hdb_secret
|
|
436
|
+
* row); it defaults to 0 (the origin wrote the row before it resolves, so no wait is needed).
|
|
437
|
+
*
|
|
438
|
+
* Runs on the main thread, where the operations API dispatches deploys and the Pro secrets
|
|
439
|
+
* component registers custody — the same place set_secret decrypts. On a node without custody
|
|
440
|
+
* (OSS core, or a custody key not held here) a referenced secret cannot be resolved and the deploy
|
|
441
|
+
* fails loudly rather than silently installing without auth.
|
|
442
|
+
*/
|
|
443
|
+
export async function resolveRegistryAuth(
|
|
444
|
+
registryAuth: any[] | undefined,
|
|
445
|
+
component: string,
|
|
446
|
+
options: { waitMs?: number } = {}
|
|
447
|
+
): Promise<ResolvedRegistryAuthEntry[] | undefined> {
|
|
448
|
+
if (!Array.isArray(registryAuth) || registryAuth.length === 0) return registryAuth;
|
|
449
|
+
// Token-only requests must not require custody or a provisioned store — keep the fast path pure.
|
|
450
|
+
if (!registryAuth.some((entry) => entry && entry.secret !== undefined)) return registryAuth;
|
|
451
|
+
|
|
452
|
+
const waitMs = options.waitMs ?? 0;
|
|
453
|
+
const table = secretTable();
|
|
454
|
+
const resolved: ResolvedRegistryAuthEntry[] = [];
|
|
455
|
+
for (const entry of registryAuth) {
|
|
456
|
+
if (!entry) continue; // parity with the fast-path guard above; validated entries are never null
|
|
457
|
+
if (entry.secret === undefined) {
|
|
458
|
+
resolved.push({ registry: entry.registry, token: entry.token, scope: entry.scope });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const name: string = entry.secret;
|
|
462
|
+
const row = waitMs > 0 ? await waitForSecretRow(table, name, waitMs) : await table.get(name);
|
|
463
|
+
if (!row) {
|
|
464
|
+
throw new ClientError(
|
|
465
|
+
`registryAuth references secret '${name}', which does not exist`,
|
|
466
|
+
HTTP_STATUS_CODES.NOT_FOUND
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const grants = Array.isArray(row.grants) ? row.grants : [];
|
|
470
|
+
if (!row.processEnv && !grants.includes(component)) {
|
|
471
|
+
throw new ClientError(
|
|
472
|
+
`registryAuth secret '${name}' is not granted to component '${component}' ` +
|
|
473
|
+
`(grant it with grant_secret, or set it processEnv:true)`,
|
|
474
|
+
HTTP_STATUS_CODES.FORBIDDEN
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const custody = getSecretCustody();
|
|
478
|
+
if (!custody) {
|
|
479
|
+
// Server-state condition, not a client-fixable request: the same body would resolve once
|
|
480
|
+
// custody comes up, so report it retryable (503) rather than the ClientError default 400.
|
|
481
|
+
throw new ClientError(
|
|
482
|
+
`secrets custody is not initialized on this node; cannot resolve registryAuth secret '${name}'`,
|
|
483
|
+
HTTP_STATUS_CODES.SERVICE_UNAVAILABLE
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
let token: string;
|
|
487
|
+
try {
|
|
488
|
+
token = custody.decrypt(row.envelope);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
throw new ClientError(
|
|
491
|
+
`Failed to decrypt registryAuth secret '${name}': ${(error as Error).message}`,
|
|
492
|
+
HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
resolved.push({ registry: entry.registry, token, scope: entry.scope });
|
|
496
|
+
}
|
|
497
|
+
return resolved;
|
|
498
|
+
}
|
package/config/configUtils.ts
CHANGED
|
@@ -851,9 +851,33 @@ export function getConfiguration() {
|
|
|
851
851
|
*/
|
|
852
852
|
export async function setConfiguration(setConfigJson) {
|
|
853
853
|
// eslint-disable-next-line no-unused-vars
|
|
854
|
-
const { operation, hdb_user, hdbAuthHeader, ...configFields } = setConfigJson;
|
|
854
|
+
const { operation, hdb_user, hdbAuthHeader, replicated, ...configFields } = setConfigJson;
|
|
855
|
+
// Operation-control field, not a config param: enforce boolean (matching other
|
|
856
|
+
// `replicated` surfaces, e.g. analyticsValidator) before any local write so a
|
|
857
|
+
// malformed value like the string "false" — which is truthy — can't apply config
|
|
858
|
+
// locally or trigger an unintended fan-out.
|
|
859
|
+
if (replicated !== undefined && typeof replicated !== 'boolean') {
|
|
860
|
+
throw handleHDBError(
|
|
861
|
+
new Error(),
|
|
862
|
+
`'replicated' must be a boolean`,
|
|
863
|
+
HTTP_STATUS_CODES.BAD_REQUEST,
|
|
864
|
+
undefined,
|
|
865
|
+
undefined,
|
|
866
|
+
true
|
|
867
|
+
);
|
|
868
|
+
}
|
|
855
869
|
try {
|
|
856
870
|
updateConfigValue(undefined, undefined, configFields, true);
|
|
871
|
+
if (replicated) {
|
|
872
|
+
// Opt-in fan-out to all cluster nodes (#660). replicateOperation forwards the
|
|
873
|
+
// body with `replicated: false`, so peers apply locally without re-replicating;
|
|
874
|
+
// per-node outcomes are returned on `response.replicated`. `replicated` must
|
|
875
|
+
// stay out of configFields on both origin and peers, or it would be written to
|
|
876
|
+
// the config file as a config param.
|
|
877
|
+
const response = await server.replication.replicateOperation(setConfigJson);
|
|
878
|
+
response.message = CONFIGURE_SUCCESS_RESPONSE;
|
|
879
|
+
return response;
|
|
880
|
+
}
|
|
857
881
|
return CONFIGURE_SUCCESS_RESPONSE;
|
|
858
882
|
} catch (err) {
|
|
859
883
|
if (typeof err === 'string' || err instanceof String) {
|
package/config-app.schema.json
CHANGED
|
@@ -51,6 +51,87 @@
|
|
|
51
51
|
},
|
|
52
52
|
"required": ["files"]
|
|
53
53
|
},
|
|
54
|
+
"static": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"description": "Static file serving configuration.",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"files": {
|
|
60
|
+
"description": "Glob(s) or path(s) to the files to serve.",
|
|
61
|
+
"oneOf": [
|
|
62
|
+
{
|
|
63
|
+
"type": "string"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": {
|
|
68
|
+
"type": "string"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"urlPath": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "URL path prefix the files are served under."
|
|
76
|
+
},
|
|
77
|
+
"index": {
|
|
78
|
+
"type": "boolean",
|
|
79
|
+
"description": "Serve index.html files for directory requests.",
|
|
80
|
+
"default": true
|
|
81
|
+
},
|
|
82
|
+
"extensions": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"items": {
|
|
85
|
+
"type": "string"
|
|
86
|
+
},
|
|
87
|
+
"description": "File extensions to try appending when a requested path is not found (e.g. ['html'] serves /page.html for /page)."
|
|
88
|
+
},
|
|
89
|
+
"fallthrough": {
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"description": "Pass unmatched requests to the next handler. When false, this handler answers unmatched GETs itself — note it runs before the REST handler by default, so also set `after: 'rest'` if the application serves an API.",
|
|
92
|
+
"default": true
|
|
93
|
+
},
|
|
94
|
+
"notFound": {
|
|
95
|
+
"description": "File to serve when a path is not found (requires `fallthrough: false`). Either a path, or an object with `file` and `statusCode` (e.g. index.html with 200 for history-mode SPAs — combine with `after: 'rest'`).",
|
|
96
|
+
"oneOf": [
|
|
97
|
+
{
|
|
98
|
+
"type": "string"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"properties": {
|
|
104
|
+
"file": {
|
|
105
|
+
"type": "string"
|
|
106
|
+
},
|
|
107
|
+
"statusCode": {
|
|
108
|
+
"type": "number"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"required": ["file", "statusCode"]
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"before": {
|
|
116
|
+
"description": "Run this handler before the named handler (the config key as registered, e.g. 'rest'). Defaults to 'authentication' so file requests skip credential parsing; false clears the default so registration order applies.",
|
|
117
|
+
"oneOf": [
|
|
118
|
+
{
|
|
119
|
+
"type": "string",
|
|
120
|
+
"minLength": 1
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"const": false
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"after": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"minLength": 1,
|
|
130
|
+
"description": "Run this handler after the named handler (the config key as registered, e.g. 'rest' to let API routes match before static fallbacks). Overrides the default `before: 'authentication'` hoist."
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"required": ["files"]
|
|
134
|
+
},
|
|
54
135
|
"package": {
|
|
55
136
|
"type": "string",
|
|
56
137
|
"description": "Package identifier for the application (npm-compatible or git URL)."
|
package/config-root.schema.json
CHANGED
|
@@ -349,8 +349,15 @@
|
|
|
349
349
|
"properties": {
|
|
350
350
|
"enabled": { "type": "boolean", "description": "Enable log rotation. Default: true" },
|
|
351
351
|
"compress": { "type": "boolean", "description": "Compress rotated logs with gzip. Default: false" },
|
|
352
|
-
"interval": {
|
|
352
|
+
"interval": {
|
|
353
|
+
"type": ["string", "null"],
|
|
354
|
+
"description": "Time between rotations (e.g. '1D', '6H', '30m'). Units: D (days), H (hours), M (months), m (minutes)."
|
|
355
|
+
},
|
|
353
356
|
"maxSize": { "type": ["string", "null"], "description": "Max size before rotation (e.g. '100M', '1G')." },
|
|
357
|
+
"retention": {
|
|
358
|
+
"type": ["string", "null"],
|
|
359
|
+
"description": "Delete rotated logs older than this age (e.g. '30D', '12H', '1M' for one month). Units: D (days), H (hours), M (months), m (minutes). When unset, rotated logs are kept indefinitely."
|
|
360
|
+
},
|
|
354
361
|
"path": { "type": "string", "description": "Directory to store rotated logs. Default: <ROOTPATH>/log" }
|
|
355
362
|
}
|
|
356
363
|
},
|
|
@@ -86,9 +86,10 @@ function initSystemSchemaPaths(schema, table) {
|
|
|
86
86
|
schema = schema.toString();
|
|
87
87
|
table = table.toString();
|
|
88
88
|
|
|
89
|
-
// Check to see if there are any CLI or env args related to schema/table path
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
// Check to see if there are any CLI or env args related to schema/table path.
|
|
90
|
+
// Merge CLI args over env vars WITHOUT mutating process.env: assigning into process.env
|
|
91
|
+
// permanently clobbered real env vars (e.g. AUTHENTICATION_AUTHORIZELOCAL) with CLI values.
|
|
92
|
+
const args = Object.assign({}, process.env, minimist(process.argv));
|
|
92
93
|
|
|
93
94
|
const schemaConfJson = args[CONFIG_PARAMS.DATABASES.toUpperCase()];
|
|
94
95
|
if (schemaConfJson) {
|
|
@@ -27,6 +27,20 @@ export default async function readAuditLog(readAuditLogObject: any) {
|
|
|
27
27
|
throw new Error(HDB_ERROR_MSGS.TABLE_REQUIRED_ERR);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// system.hdb_secret audit rows carry full record images — i.e. secret envelopes — and
|
|
31
|
+
// read_audit_log is delegable to non-super_user roles via the `operations` allowlist. Table
|
|
32
|
+
// audit itself must stay on (mount_hdb forces audit:true for every system table; on RocksDB it
|
|
33
|
+
// is load-bearing for data retention), so the read surface is blocked instead, regardless of
|
|
34
|
+
// config or role. Secret mutations are separately audited via value-free logger.notify events
|
|
35
|
+
// (components/secretOperations.ts).
|
|
36
|
+
if (
|
|
37
|
+
database === hdbTerms.SYSTEM_SCHEMA_NAME &&
|
|
38
|
+
readAuditLogObject.table === hdbTerms.SYSTEM_TABLE_NAMES.SECRET_TABLE_NAME
|
|
39
|
+
) {
|
|
40
|
+
const msg = `read_audit_log is not supported on ${database}.${readAuditLogObject.table}; secret mutations are audited via structured log events that never contain values`;
|
|
41
|
+
throw handleHDBError(new Error(), msg, HTTP_STATUS_CODES.FORBIDDEN, undefined, undefined, true);
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
if (!envMgr.get(hdbTerms.CONFIG_PARAMS.LOGGING_AUDITLOG)) {
|
|
31
45
|
throw handleHDBError(
|
|
32
46
|
new Error(),
|
|
@@ -204,6 +204,7 @@ async function descTable(describeTableObject: any, attrPerms?: any) {
|
|
|
204
204
|
if (tableObj.replicate !== undefined) tableResult.replicate = tableObj.replicate;
|
|
205
205
|
if (tableObj.expirationMS !== undefined) tableResult.expiration = tableObj.expirationMS / 1000 + 's';
|
|
206
206
|
if (tableObj.sealed !== undefined) tableResult.sealed = tableObj.sealed;
|
|
207
|
+
if (tableObj.cacheControl != null) tableResult.cacheControl = tableObj.cacheControl;
|
|
207
208
|
if ((tableObj as any).sources?.length > 0)
|
|
208
209
|
tableResult.sources = (tableObj as any).sources
|
|
209
210
|
.map((source: any) => source.name)
|