@harperfast/harper 5.2.0-beta.4 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/backup.ts +169 -0
- package/bin/cliOperations.ts +92 -73
- package/bin/copyDb.ts +21 -4
- package/bin/harper.ts +43 -55
- package/bin/help.ts +216 -0
- package/components/Application.ts +236 -46
- package/components/ApplicationScope.ts +26 -0
- package/components/EntryHandler.ts +410 -105
- package/components/RuntimeModuleTracker.ts +189 -0
- package/components/Scope.ts +120 -30
- package/components/componentLoader.ts +177 -25
- package/components/deployLifecycle.ts +119 -33
- package/components/mcp/toolRegistry.ts +10 -0
- package/components/mcp/tools/application.ts +12 -5
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
- package/components/mcp/tools/schemas/operations.ts +9 -0
- package/components/operations.js +5 -6
- package/components/operationsValidation.js +32 -2
- package/components/scopeMount.ts +150 -0
- package/config/configUtils.ts +12 -9
- package/config-root.schema.json +14 -0
- package/dataLayer/backupManifest.ts +102 -0
- package/dataLayer/blobBackup.ts +286 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/restoreMarker.ts +276 -0
- package/dataLayer/rocksdbBackup.ts +1100 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/backup.d.ts +9 -0
- package/dist/bin/backup.js +192 -0
- package/dist/bin/backup.js.map +1 -0
- package/dist/bin/cliOperations.d.ts +13 -0
- package/dist/bin/cliOperations.js +89 -70
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.js +13 -2
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +13 -0
- package/dist/bin/harper.js +45 -56
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.d.ts +8 -0
- package/dist/bin/help.js +192 -0
- package/dist/bin/help.js.map +1 -0
- package/dist/components/Application.d.ts +16 -1
- package/dist/components/Application.js +210 -38
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +7 -0
- package/dist/components/ApplicationScope.js +22 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.d.ts +4 -4
- package/dist/components/EntryHandler.js +386 -95
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.d.ts +11 -0
- package/dist/components/RuntimeModuleTracker.js +189 -0
- package/dist/components/RuntimeModuleTracker.js.map +1 -0
- package/dist/components/Scope.d.ts +38 -1
- package/dist/components/Scope.js +117 -28
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -6
- package/dist/components/componentLoader.js +160 -18
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deployLifecycle.d.ts +6 -2
- package/dist/components/deployLifecycle.js +109 -31
- package/dist/components/deployLifecycle.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +7 -0
- package/dist/components/mcp/toolRegistry.js +10 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.js +11 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +9 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/operations.js +6 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +34 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/scopeMount.d.ts +86 -0
- package/dist/components/scopeMount.js +131 -0
- package/dist/components/scopeMount.js.map +1 -0
- package/dist/config/configUtils.js +13 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/backupManifest.d.ts +26 -0
- package/dist/dataLayer/backupManifest.js +97 -0
- package/dist/dataLayer/backupManifest.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +87 -0
- package/dist/dataLayer/blobBackup.js +282 -0
- package/dist/dataLayer/blobBackup.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +122 -0
- package/dist/dataLayer/restoreMarker.js +261 -0
- package/dist/dataLayer/restoreMarker.js.map +1 -0
- package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
- package/dist/dataLayer/rocksdbBackup.js +1039 -0
- package/dist/dataLayer/rocksdbBackup.js.map +1 -0
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +55 -0
- package/dist/resources/DatabaseTransaction.js +282 -109
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -5
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js +22 -4
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -5
- package/dist/resources/Table.js +114 -37
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -6
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +8 -1
- package/dist/resources/blob.js +31 -14
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +59 -1
- package/dist/resources/databases.js +386 -41
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/jsResource.d.ts +4 -26
- package/dist/resources/jsResource.js +5 -59
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -1
- package/dist/resources/models/Models.js +10 -1
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +9 -0
- package/dist/resources/models/backendRegistry.js +10 -0
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +16 -1
- package/dist/resources/models/openaiStream.js +113 -21
- package/dist/resources/models/openaiStream.js.map +1 -1
- package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
- package/dist/resources/models/v1/chatCompletions.js +115 -0
- package/dist/resources/models/v1/chatCompletions.js.map +1 -0
- package/dist/resources/models/v1/embeddings.d.ts +11 -0
- package/dist/resources/models/v1/embeddings.js +71 -0
- package/dist/resources/models/v1/embeddings.js.map +1 -0
- package/dist/resources/models/v1/errors.d.ts +54 -0
- package/dist/resources/models/v1/errors.js +130 -0
- package/dist/resources/models/v1/errors.js.map +1 -0
- package/dist/resources/models/v1/index.d.ts +36 -0
- package/dist/resources/models/v1/index.js +75 -0
- package/dist/resources/models/v1/index.js.map +1 -0
- package/dist/resources/models/v1/models.d.ts +26 -0
- package/dist/resources/models/v1/models.js +44 -0
- package/dist/resources/models/v1/models.js.map +1 -0
- package/dist/resources/models/v1/translation.d.ts +133 -0
- package/dist/resources/models/v1/translation.js +298 -0
- package/dist/resources/models/v1/translation.js.map +1 -0
- package/dist/resources/roles.d.ts +1 -1
- package/dist/resources/roles.js +54 -7
- package/dist/resources/roles.js.map +1 -1
- package/dist/resources/transaction.js +0 -3
- package/dist/resources/transaction.js.map +1 -1
- package/dist/security/jsLoader.js +84 -33
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/role.js +4 -0
- package/dist/security/role.js.map +1 -1
- package/dist/security/superUserGuard.d.ts +7 -0
- package/dist/security/superUserGuard.js +23 -0
- package/dist/security/superUserGuard.js.map +1 -0
- package/dist/security/tokenAuthentication.d.ts +0 -1
- package/dist/security/tokenAuthentication.js +6 -2
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.d.ts +6 -1
- package/dist/security/user.js +23 -1
- package/dist/security/user.js.map +1 -1
- package/dist/server/REST.js +25 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +15 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +7 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +20 -1
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobRunner.js +10 -0
- package/dist/server/jobs/jobRunner.js.map +1 -1
- package/dist/server/jobs/jobs.js +11 -0
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +10 -1
- package/dist/server/middlewareChain.js +81 -21
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/operationsServer.d.ts +0 -2
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/multipartParser.js +9 -0
- package/dist/server/serverHelpers/multipartParser.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
- package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
- package/dist/server/serverHelpers/registeredOperations.js +14 -13
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +27 -7
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +21 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +68 -21
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +17 -0
- package/dist/server/storageReclamation.js +90 -6
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/socketRouter.js +20 -0
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +12 -0
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/diff/differential.js +7 -3
- package/dist/sqlEngine/diff/differential.js.map +1 -1
- package/dist/sqlTranslator/index.js +6 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/upgradePrompt.d.ts +2 -2
- package/dist/upgrade/upgradePrompt.js +22 -3
- package/dist/upgrade/upgradePrompt.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +25 -6
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +1 -0
- package/dist/utility/errors/commonErrors.js +1 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +13 -1
- package/dist/utility/hdbTerms.js +13 -1
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +32 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +47 -0
- package/dist/utility/logging/harper_logger.js +803 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/operation_authorization.js +13 -3
- package/dist/utility/operation_authorization.js.map +1 -1
- package/npm-shrinkwrap.json +198 -193
- package/package.json +12 -7
- package/resources/DESIGN.md +2 -0
- package/resources/DatabaseTransaction.ts +310 -104
- package/resources/ResourceInterface.ts +8 -5
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +341 -263
- package/resources/analytics/write.ts +22 -20
- package/resources/blob.ts +31 -14
- package/resources/databases.ts +387 -46
- package/resources/jsResource.ts +5 -62
- package/resources/models/Models.ts +14 -1
- package/resources/models/backendRegistry.ts +10 -0
- package/resources/models/openaiStream.ts +131 -19
- package/resources/models/v1/chatCompletions.ts +128 -0
- package/resources/models/v1/embeddings.ts +70 -0
- package/resources/models/v1/errors.ts +141 -0
- package/resources/models/v1/index.ts +72 -0
- package/resources/models/v1/models.ts +53 -0
- package/resources/models/v1/translation.ts +362 -0
- package/resources/roles.ts +67 -7
- package/resources/transaction.ts +0 -3
- package/security/jsLoader.ts +84 -30
- package/security/role.ts +7 -0
- package/security/superUserGuard.ts +20 -0
- package/security/tokenAuthentication.ts +6 -3
- package/security/user.ts +26 -1
- package/server/DESIGN.md +61 -34
- package/server/REST.ts +25 -9
- package/server/fastifyRoutes.ts +20 -1
- package/server/http.ts +100 -13
- package/server/itc/serverHandlers.js +7 -1
- package/server/jobs/jobProcess.ts +18 -1
- package/server/jobs/jobRunner.ts +10 -0
- package/server/jobs/jobs.ts +11 -0
- package/server/middlewareChain.ts +79 -20
- package/server/operationsServer.ts +0 -2
- package/server/serverHelpers/multipartParser.ts +9 -0
- package/server/serverHelpers/operationAuthorizationState.ts +11 -0
- package/server/serverHelpers/registeredOperations.ts +19 -15
- package/server/serverHelpers/serverHandlers.js +28 -7
- package/server/serverHelpers/serverUtilities.ts +29 -7
- package/server/static.ts +87 -23
- package/server/storageReclamation.ts +104 -8
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +3 -0
- package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
- package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
- package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
- package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
- package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
- package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
- package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
- package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
- package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
- package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
- package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
- package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
- package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
- package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
- package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
- package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
- package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
- package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
- package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
- package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
- package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
- package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
- package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/OperationFunctionCaller.ts +24 -3
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +13 -1
- package/utility/install/installer.ts +37 -0
- package/utility/logging/harper_logger.ts +786 -41
- package/utility/operation_authorization.ts +31 -3
- package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
|
@@ -29,6 +29,8 @@ import * as scheduler from '../resources/scheduler/scheduler.ts';
|
|
|
29
29
|
import { restartWorkers, getWorkerIndex } from '../server/threads/manageThreads.js';
|
|
30
30
|
import { resetRestartNeeded, subscribeToRestartRequests } from './requestRestart.ts';
|
|
31
31
|
import { trackScopeClose } from './scopeShutdown.ts';
|
|
32
|
+
import { deployLifecycle } from './deployLifecycle.ts';
|
|
33
|
+
import { toScopeMount, nestScopeMount, type ScopeMount } from './scopeMount.ts';
|
|
32
34
|
import { scopedImport } from '../security/jsLoader.ts';
|
|
33
35
|
import { server } from '../server/Server.ts';
|
|
34
36
|
import { Resources } from '../resources/Resources.ts';
|
|
@@ -63,6 +65,51 @@ let resources;
|
|
|
63
65
|
* @param loadedPluginModules
|
|
64
66
|
* @param loadedResources
|
|
65
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* The application mount an operator declared for `appName` in the root config.
|
|
70
|
+
*
|
|
71
|
+
* Applications in the components root are loaded by directory scan, not from a root-config
|
|
72
|
+
* entry, so this is what makes a root-config entry authoritative for where an app is served:
|
|
73
|
+
*
|
|
74
|
+
* ```yaml
|
|
75
|
+
* my-app:
|
|
76
|
+
* host: api.example.com
|
|
77
|
+
* urlPath: /v1
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Works whether or not the entry also carries `package` — a payload-deployed app is mounted the
|
|
81
|
+
* same way as an installed one. A built-in plugin's config block is never an application mount;
|
|
82
|
+
* those keys (`http`, `mqtt`, …) configure the plugin itself.
|
|
83
|
+
*/
|
|
84
|
+
function rootConfigMount(appName: string): ScopeMount | undefined {
|
|
85
|
+
if (Object.hasOwn(TRUSTED_RESOURCE_PLUGINS, appName)) return undefined;
|
|
86
|
+
return toScopeMount(getConfigObj()?.[appName]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the mount for `appName`, or reports the failure and returns `undefined` if the
|
|
91
|
+
* configured `host`/`urlPath` is invalid. The caller must skip loading the application in that
|
|
92
|
+
* case rather than loading it anyway: an invalid mount is a request to CONSTRAIN where the app is
|
|
93
|
+
* served, so loading it unconstrained would silently drop the isolation the operator configured
|
|
94
|
+
* (review finding) — worse than not loading it at all. Isolated per-app so one bad mount doesn't
|
|
95
|
+
* take down every other application's load.
|
|
96
|
+
*/
|
|
97
|
+
function tryRootConfigMount(appName: string): { ok: true; mount: ScopeMount | undefined } | { ok: false } {
|
|
98
|
+
try {
|
|
99
|
+
return { ok: true, mount: rootConfigMount(appName) };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
(error as Error).message = `Not loading '${appName}': invalid routing configured: ${(error as Error).message}`;
|
|
102
|
+
errorReporter?.(error);
|
|
103
|
+
(getWorkerIndex() === 0 ? console : harperLogger).error(errorForLog(error as Error));
|
|
104
|
+
componentLifecycle.failed(
|
|
105
|
+
appName,
|
|
106
|
+
error as Error,
|
|
107
|
+
`Component '${appName}' failed to load due to invalid routing configuration`
|
|
108
|
+
);
|
|
109
|
+
return { ok: false };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
export async function loadComponentDirectories(loadedPluginModules?: Map<any, any>, loadedResources?: Resources) {
|
|
67
114
|
if (loadedResources) resources = loadedResources;
|
|
68
115
|
if (loadedPluginModules) loadedComponents = loadedPluginModules;
|
|
@@ -81,21 +128,32 @@ export async function loadComponentDirectories(loadedPluginModules?: Map<any, an
|
|
|
81
128
|
if (appEntry.name.startsWith('.')) continue;
|
|
82
129
|
const appName = appEntry.name;
|
|
83
130
|
const appFolder = join(CF_ROUTES_DIR, appName);
|
|
131
|
+
const mountResult = tryRootConfigMount(appName);
|
|
132
|
+
if (!mountResult.ok) continue;
|
|
84
133
|
cfsLoaded.push(
|
|
85
|
-
loadComponent(appFolder, resources, HDB_ROOT_DIR_NAME, {
|
|
134
|
+
loadComponent(appFolder, resources, HDB_ROOT_DIR_NAME, {
|
|
135
|
+
isRoot: false,
|
|
136
|
+
autoReload: false,
|
|
137
|
+
appName,
|
|
138
|
+
mount: mountResult.mount,
|
|
139
|
+
})
|
|
86
140
|
);
|
|
87
141
|
}
|
|
88
142
|
}
|
|
89
143
|
const hdbAppFolder = process.env.RUN_HDB_APP;
|
|
90
144
|
if (hdbAppFolder) {
|
|
91
145
|
if (getWorkerIndex() === 0) harperLogger.info?.('Loading application from ' + hdbAppFolder);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
const mountResult = tryRootConfigMount(basename(hdbAppFolder));
|
|
147
|
+
if (mountResult.ok) {
|
|
148
|
+
cfsLoaded.push(
|
|
149
|
+
loadComponent(hdbAppFolder, resources, hdbAppFolder, {
|
|
150
|
+
isRoot: false,
|
|
151
|
+
autoReload: Boolean(process.env.DEV_MODE),
|
|
152
|
+
appName: hdbAppFolder,
|
|
153
|
+
mount: mountResult.mount,
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
99
157
|
}
|
|
100
158
|
return Promise.all(cfsLoaded).then(() => {
|
|
101
159
|
watchesSetup = true;
|
|
@@ -113,6 +171,16 @@ export const TRUSTED_RESOURCE_PLUGINS: any = {
|
|
|
113
171
|
return require('../server/fastifyRoutes');
|
|
114
172
|
},
|
|
115
173
|
login,
|
|
174
|
+
// String entry: the loader `await import()`s these lazily when the component is actually
|
|
175
|
+
// processed, so the gateway's module graph is not pulled into componentLoader's own
|
|
176
|
+
// evaluation. `#src/*` is used rather than a relative path because it resolves under both
|
|
177
|
+
// conditions (source under --conditions=typestrip, dist otherwise); a relative extensionless
|
|
178
|
+
// require would only resolve against dist.
|
|
179
|
+
//
|
|
180
|
+
// The component is only *processed* when a `modelsGateway` key exists in the config, since
|
|
181
|
+
// the root loop skips absent keys (`if (!componentConfig) continue`). `defaultConfig.yaml`
|
|
182
|
+
// ships no such key, so an instance that never opts in pays nothing for this entry.
|
|
183
|
+
modelsGateway: '#src/resources/models/v1/index',
|
|
116
184
|
static: staticFiles,
|
|
117
185
|
customFunctions: {},
|
|
118
186
|
http: httpComponent,
|
|
@@ -255,6 +323,7 @@ function symlinkHarperModule(componentDirectory: string) {
|
|
|
255
323
|
*/
|
|
256
324
|
function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
|
|
257
325
|
return scope.ready.then(async () => {
|
|
326
|
+
await scope.waitForDeployCompletion();
|
|
258
327
|
// Timeout priority is user config, plugin default, finally 30 seconds
|
|
259
328
|
const timeout = scope.options.get(['timeout']) || plugin.defaultTimeout || 30_000; // default 30 second timeout
|
|
260
329
|
if (typeof timeout !== 'number') {
|
|
@@ -276,35 +345,76 @@ function sequentiallyHandleApplication(scope: Scope, plugin: PluginModule) {
|
|
|
276
345
|
}, timeout + 5_000); // extra time for lock acquisition
|
|
277
346
|
});
|
|
278
347
|
}
|
|
279
|
-
let loadTimeout: NodeJS.Timeout;
|
|
280
348
|
try {
|
|
281
349
|
// note that handleApplication can throw sync or async errors, need to run finally block for both
|
|
282
|
-
await
|
|
350
|
+
await withDeployAwareTimeout(
|
|
283
351
|
Promise.resolve(plugin.handleApplication(scope)).then(async () => {
|
|
284
352
|
// Wait for any initial entry handler loads to complete
|
|
285
353
|
// This ensures all async operations (like secureImport) finish before the component is marked as loaded
|
|
286
354
|
await scope.waitForInitialLoads();
|
|
287
355
|
}),
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
() =>
|
|
292
|
-
reject(
|
|
293
|
-
new Error(
|
|
294
|
-
`handleApplication timed out after ${timeout}ms for ${scope.pluginName} on behalf of ${scope.appName}`
|
|
295
|
-
)
|
|
296
|
-
),
|
|
297
|
-
timeout
|
|
298
|
-
))
|
|
299
|
-
),
|
|
300
|
-
]);
|
|
356
|
+
scope,
|
|
357
|
+
timeout
|
|
358
|
+
);
|
|
301
359
|
} finally {
|
|
302
360
|
Status.primaryStore.unlock(scope.pluginName);
|
|
303
|
-
clearTimeout(loadTimeout);
|
|
304
361
|
}
|
|
305
362
|
});
|
|
306
363
|
}
|
|
307
364
|
|
|
365
|
+
function withDeployAwareTimeout<T>(operation: Promise<T>, scope: Scope, timeout: number): Promise<T> {
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
const absoluteTimeout = timeout + 6 * 60 * 60 * 1000;
|
|
368
|
+
let remaining = timeout;
|
|
369
|
+
let activeSince = 0;
|
|
370
|
+
let timer: NodeJS.Timeout | undefined;
|
|
371
|
+
let absoluteTimer: NodeJS.Timeout;
|
|
372
|
+
const cleanup = () => {
|
|
373
|
+
if (timer) clearTimeout(timer);
|
|
374
|
+
clearTimeout(absoluteTimer);
|
|
375
|
+
deployLifecycle.off('deploy:start', handleDeployStart);
|
|
376
|
+
deployLifecycle.off('deploy:end', handleDeployEnd);
|
|
377
|
+
};
|
|
378
|
+
const rejectTimeout = (limit = timeout) => {
|
|
379
|
+
cleanup();
|
|
380
|
+
reject(
|
|
381
|
+
new Error(`handleApplication timed out after ${limit}ms for ${scope.pluginName} on behalf of ${scope.appName}`)
|
|
382
|
+
);
|
|
383
|
+
};
|
|
384
|
+
absoluteTimer = setTimeout(() => rejectTimeout(absoluteTimeout), absoluteTimeout);
|
|
385
|
+
absoluteTimer.unref?.();
|
|
386
|
+
const arm = () => {
|
|
387
|
+
if (deployLifecycle.isDeployInFlight(scope.appName)) return;
|
|
388
|
+
if (remaining <= 0) return rejectTimeout();
|
|
389
|
+
activeSince = Date.now();
|
|
390
|
+
timer = setTimeout(rejectTimeout, remaining);
|
|
391
|
+
};
|
|
392
|
+
function handleDeployStart(componentName: string) {
|
|
393
|
+
if (componentName !== scope.appName || !timer) return;
|
|
394
|
+
remaining -= Date.now() - activeSince;
|
|
395
|
+
clearTimeout(timer);
|
|
396
|
+
timer = undefined;
|
|
397
|
+
}
|
|
398
|
+
function handleDeployEnd(componentName: string) {
|
|
399
|
+
if (componentName === scope.appName) arm();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
deployLifecycle.on('deploy:start', handleDeployStart);
|
|
403
|
+
deployLifecycle.on('deploy:end', handleDeployEnd);
|
|
404
|
+
operation.then(
|
|
405
|
+
(value) => {
|
|
406
|
+
cleanup();
|
|
407
|
+
resolve(value);
|
|
408
|
+
},
|
|
409
|
+
(error) => {
|
|
410
|
+
cleanup();
|
|
411
|
+
reject(error);
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
arm();
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
308
418
|
export interface LoadComponentOptions {
|
|
309
419
|
isRoot?: boolean;
|
|
310
420
|
applicationScope?: ApplicationScope;
|
|
@@ -316,6 +426,10 @@ export interface LoadComponentOptions {
|
|
|
316
426
|
// (e.g. the deploy pre-flight validation) so their deploy-lifecycle listeners don't accumulate
|
|
317
427
|
// across deploys (#1462).
|
|
318
428
|
collectScopes?: Set<Scope>;
|
|
429
|
+
// Routing the operator declared for this application in the root config (`host`/`urlPath` on
|
|
430
|
+
// the application's entry). Applied to every plugin scope this load creates, and inherited by
|
|
431
|
+
// components the application itself declares, so the whole subtree moves together.
|
|
432
|
+
mount?: ScopeMount;
|
|
319
433
|
}
|
|
320
434
|
|
|
321
435
|
/**
|
|
@@ -342,7 +456,9 @@ export async function loadComponent(
|
|
|
342
456
|
isRoot,
|
|
343
457
|
autoReload,
|
|
344
458
|
appName,
|
|
459
|
+
mount,
|
|
345
460
|
} = options;
|
|
461
|
+
applicationScope.runtimeRoot ??= resolvedFolder;
|
|
346
462
|
applicationScope.allowedPath ??= realpathSync(componentDirectory);
|
|
347
463
|
if (providedLoadedComponents) loadedComponents = providedLoadedComponents;
|
|
348
464
|
try {
|
|
@@ -485,6 +601,13 @@ export async function loadComponent(
|
|
|
485
601
|
autoReload: false,
|
|
486
602
|
appName: appName || componentName,
|
|
487
603
|
collectScopes: options.collectScopes,
|
|
604
|
+
// `host`/`urlPath` on this entry route the component being loaded. For an
|
|
605
|
+
// application (no plugin module of its own) that entry is the only place an
|
|
606
|
+
// operator can say where the app is served — its own config.yaml declares the
|
|
607
|
+
// plugins, not the deployment. A nested component's own mount nests inside
|
|
608
|
+
// the parent's rather than replacing it, and the parent keeps hostname
|
|
609
|
+
// authority, so a child can't escape the host it is served on.
|
|
610
|
+
mount: nestScopeMount(mount, toScopeMount(componentConfig)),
|
|
488
611
|
});
|
|
489
612
|
componentFunctionality[componentName] = true;
|
|
490
613
|
}
|
|
@@ -554,7 +677,10 @@ export async function loadComponent(
|
|
|
554
677
|
// authoritative root-ness: only root-load scopes watch THE root config and
|
|
555
678
|
// get the runtime env-config overlay (#1618) — an app component that happens
|
|
556
679
|
// to ship a root-named config file does not
|
|
557
|
-
isRoot
|
|
680
|
+
isRoot,
|
|
681
|
+
// A root-declared plugin reads its own `host`/`urlPath` straight from this
|
|
682
|
+
// config, so only an inherited application mount applies here.
|
|
683
|
+
mount
|
|
558
684
|
);
|
|
559
685
|
|
|
560
686
|
if (options.collectScopes) {
|
|
@@ -597,6 +723,32 @@ export async function loadComponent(
|
|
|
597
723
|
);
|
|
598
724
|
}
|
|
599
725
|
|
|
726
|
+
// `start`/`startOnMainThread` hand the plugin the raw, unmounted `server` directly —
|
|
727
|
+
// unlike the new Plugin API's `handleApplication(scope)`, which receives the mount-aware
|
|
728
|
+
// scope — so routes registered there would silently escape a configured host/urlPath
|
|
729
|
+
// mount and stay reachable unconstrained (review finding). Same isolation gap
|
|
730
|
+
// fastifyRoutes.ts already closes for its own legacy fallback (host-only there, since
|
|
731
|
+
// urlPath composes into its route prefix; here neither composes, so both are rejected).
|
|
732
|
+
// Refuse rather than imply an isolation the hook can't honor; contained to this
|
|
733
|
+
// component by the loader's per-component try/catch.
|
|
734
|
+
if (
|
|
735
|
+
mount &&
|
|
736
|
+
(typeof extensionModule.start === 'function' || typeof extensionModule.startOnMainThread === 'function')
|
|
737
|
+
) {
|
|
738
|
+
const error = new Error(
|
|
739
|
+
`Component '${componentName}' is mounted (${[
|
|
740
|
+
mount.host && `host '${mount.host}'`,
|
|
741
|
+
mount.urlPath && `urlPath '${mount.urlPath}'`,
|
|
742
|
+
]
|
|
743
|
+
.filter(Boolean)
|
|
744
|
+
.join(
|
|
745
|
+
', '
|
|
746
|
+
)}), but its deprecated 'start'/'startOnMainThread' extension API receives the bare, unmounted server — routes registered there would stay reachable unconstrained. Upgrade to the new Plugin API (handleApplication(scope)) or drop the mount.`
|
|
747
|
+
);
|
|
748
|
+
componentLifecycle.failed(componentStatusName, error, `Component '${componentStatusName}' failed to load`);
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
|
|
600
752
|
if (isMainThread) {
|
|
601
753
|
// `startOnMainThread` is one-time main-thread init: run it at most once per component
|
|
602
754
|
// for the life of the process (first load / first deploy). On a reload of an
|
|
@@ -9,25 +9,39 @@
|
|
|
9
9
|
//
|
|
10
10
|
// This module solves that by broadcasting a structured "deploy:start" /
|
|
11
11
|
// "deploy:end" signal to every Harper thread. Each thread's deploy emitter
|
|
12
|
-
// fires locally so Scopes (and any plugin subscribers) can react: Scopes
|
|
13
|
-
// their EntryHandlers on start
|
|
12
|
+
// fires locally so Scopes (and any plugin subscribers) can react: Scopes pause
|
|
13
|
+
// their EntryHandlers on start and resume them on end. The result is that
|
|
14
14
|
// during a deploy, no watcher fires events for the deployed component, and
|
|
15
|
-
// after the deploy the
|
|
16
|
-
//
|
|
15
|
+
// after the deploy the watcher compares the new tree with its retained snapshot
|
|
16
|
+
// and emits the same logical add/change/unlink events consumers saw before #1806.
|
|
17
17
|
//
|
|
18
|
-
// State sharing across threads is intentionally narrow:
|
|
19
|
-
//
|
|
20
|
-
// need to gate their own work on deploy progress
|
|
21
|
-
// listen to the events directly.
|
|
18
|
+
// State sharing across threads is intentionally narrow: local deployment-owner
|
|
19
|
+
// records are rebuilt from the broadcast stream and reclaimed when their owner
|
|
20
|
+
// thread exits. Plugins that need to gate their own work on deploy progress
|
|
21
|
+
// import `deployLifecycle` and listen to the events directly.
|
|
22
22
|
|
|
23
23
|
import { EventEmitter } from 'node:events';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
26
|
+
import { isMainThread, threadId } from 'node:worker_threads';
|
|
27
|
+
import {
|
|
28
|
+
broadcast,
|
|
29
|
+
broadcastWithAcknowledgement,
|
|
30
|
+
isThreadRunning,
|
|
31
|
+
onMessageByType,
|
|
32
|
+
onThreadExit,
|
|
33
|
+
} from '../server/threads/manageThreads.js';
|
|
34
|
+
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
26
35
|
|
|
27
36
|
const DEPLOY_LIFECYCLE_MSG = 'harper:deploy:lifecycle';
|
|
28
37
|
|
|
29
38
|
export type DeployPhase = 'start' | 'end';
|
|
30
|
-
export type DeployLifecycleEvent = {
|
|
39
|
+
export type DeployLifecycleEvent = {
|
|
40
|
+
name: string;
|
|
41
|
+
phase: DeployPhase;
|
|
42
|
+
deploymentId?: string;
|
|
43
|
+
ownerThreadId?: number;
|
|
44
|
+
};
|
|
31
45
|
|
|
32
46
|
type DeployLifecycleEventsMap = {
|
|
33
47
|
'deploy:start': [componentName: string];
|
|
@@ -35,44 +49,109 @@ type DeployLifecycleEventsMap = {
|
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
class DeployLifecycle extends EventEmitter<DeployLifecycleEventsMap> {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
#inFlight = new Map<string, number>();
|
|
52
|
+
#deployments = new Map<string, { name: string; ownerThreadId: number }>();
|
|
53
|
+
#byComponent = new Map<string, Set<string>>();
|
|
54
|
+
#deadOwners = new Set<number>();
|
|
55
|
+
#legacyDeployments = new Map<string, string[]>();
|
|
56
|
+
#legacySequence = 0;
|
|
44
57
|
|
|
45
58
|
isDeployInFlight(componentName: string): boolean {
|
|
46
|
-
return (this.#
|
|
59
|
+
return (this.#byComponent.get(componentName)?.size ?? 0) > 0;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
// Process a deploy lifecycle event in-process. Called both from the
|
|
50
63
|
// broadcast receiver (for events originating on another thread) and from
|
|
51
64
|
// the broadcaster (so the originating thread also reacts locally).
|
|
52
65
|
_handle(event: DeployLifecycleEvent): void {
|
|
53
|
-
const
|
|
66
|
+
const ownerThreadId = event.ownerThreadId ?? threadId;
|
|
67
|
+
let deploymentId = event.deploymentId;
|
|
54
68
|
if (event.phase === 'start') {
|
|
55
|
-
this.#
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
if (this.#deadOwners.has(ownerThreadId)) return;
|
|
70
|
+
if (!deploymentId) {
|
|
71
|
+
deploymentId = `legacy:${threadId}:${++this.#legacySequence}`;
|
|
72
|
+
const legacy = this.#legacyDeployments.get(event.name) ?? [];
|
|
73
|
+
legacy.push(deploymentId);
|
|
74
|
+
this.#legacyDeployments.set(event.name, legacy);
|
|
75
|
+
}
|
|
76
|
+
if (this.#deployments.has(deploymentId)) return;
|
|
77
|
+
const active = this.#byComponent.get(event.name) ?? new Set<string>();
|
|
78
|
+
this.#deployments.set(deploymentId, { name: event.name, ownerThreadId });
|
|
79
|
+
this.#byComponent.set(event.name, active);
|
|
80
|
+
active.add(deploymentId);
|
|
81
|
+
if (active.size === 1) this.#emitSafely('deploy:start', event.name);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!deploymentId) deploymentId = this.#legacyDeployments.get(event.name)?.pop();
|
|
85
|
+
if (deploymentId) this.#removeDeployment(deploymentId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_reclaimOwner(ownerThreadId: number): void {
|
|
89
|
+
this.#deadOwners.add(ownerThreadId);
|
|
90
|
+
for (const [deploymentId, deployment] of this.#deployments) {
|
|
91
|
+
if (deployment.ownerThreadId === ownerThreadId) this.#removeDeployment(deploymentId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async _reclaimOwnerAfterTermination(
|
|
96
|
+
ownerThreadId: number,
|
|
97
|
+
waitForTermination: (ownerThreadId: number) => Promise<void> = waitForOwnerTermination
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
if (ownerThreadId === 0 || ownerThreadId === threadId) return;
|
|
100
|
+
this.#deadOwners.add(ownerThreadId);
|
|
101
|
+
await waitForTermination(ownerThreadId);
|
|
102
|
+
this._reclaimOwner(ownerThreadId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#removeDeployment(deploymentId: string): void {
|
|
106
|
+
const deployment = this.#deployments.get(deploymentId);
|
|
107
|
+
if (!deployment) return;
|
|
108
|
+
this.#deployments.delete(deploymentId);
|
|
109
|
+
const active = this.#byComponent.get(deployment.name);
|
|
110
|
+
if (!active) return;
|
|
111
|
+
active.delete(deploymentId);
|
|
112
|
+
if (active.size === 0) {
|
|
113
|
+
this.#byComponent.delete(deployment.name);
|
|
114
|
+
this.#emitSafely('deploy:end', deployment.name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#emitSafely(event: 'deploy:start' | 'deploy:end', componentName: string): void {
|
|
119
|
+
for (const listener of this.rawListeners(event)) {
|
|
120
|
+
try {
|
|
121
|
+
listener.call(this, componentName);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
harperLogger.error(`Listener for ${event} threw for ${componentName}:`, error);
|
|
64
124
|
}
|
|
65
125
|
}
|
|
66
126
|
}
|
|
67
127
|
|
|
68
128
|
// Test-only: clear in-flight state without firing events.
|
|
69
129
|
_clearForTests(): void {
|
|
70
|
-
this.#
|
|
130
|
+
this.#deployments.clear();
|
|
131
|
+
this.#byComponent.clear();
|
|
132
|
+
this.#deadOwners.clear();
|
|
133
|
+
this.#legacyDeployments.clear();
|
|
134
|
+
this.#legacySequence = 0;
|
|
71
135
|
}
|
|
72
136
|
}
|
|
73
137
|
|
|
74
138
|
export const deployLifecycle = new DeployLifecycle();
|
|
75
139
|
|
|
140
|
+
async function waitForOwnerTermination(ownerThreadId: number): Promise<void> {
|
|
141
|
+
let timeoutReported = false;
|
|
142
|
+
for (;;) {
|
|
143
|
+
try {
|
|
144
|
+
if (!(await isThreadRunning(ownerThreadId))) return;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!timeoutReported) {
|
|
147
|
+
timeoutReported = true;
|
|
148
|
+
harperLogger.warn(`Retrying liveness check for exited thread ${ownerThreadId}:`, error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
await delay(25, undefined, { ref: false });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
76
155
|
let receiverInstalled = false;
|
|
77
156
|
function ensureReceiver() {
|
|
78
157
|
if (receiverInstalled) return;
|
|
@@ -88,6 +167,11 @@ function ensureReceiver() {
|
|
|
88
167
|
// suppress only the main thread's watchers and the worker watchers would keep
|
|
89
168
|
// firing restart storms (harper#488).
|
|
90
169
|
ensureReceiver();
|
|
170
|
+
onThreadExit((deadThreadId: number) => {
|
|
171
|
+
void deployLifecycle
|
|
172
|
+
._reclaimOwnerAfterTermination(deadThreadId)
|
|
173
|
+
.catch((error) => harperLogger.error(`Could not reclaim deployments from thread ${deadThreadId}:`, error));
|
|
174
|
+
});
|
|
91
175
|
|
|
92
176
|
/**
|
|
93
177
|
* Announce the start of a deploy for `componentName`. Awaits acknowledgement
|
|
@@ -98,9 +182,10 @@ ensureReceiver();
|
|
|
98
182
|
* component compose correctly (each call must be paired with exactly one
|
|
99
183
|
* broadcastDeployEnd).
|
|
100
184
|
*/
|
|
101
|
-
export async function broadcastDeployStart(componentName: string): Promise<
|
|
185
|
+
export async function broadcastDeployStart(componentName: string): Promise<string> {
|
|
102
186
|
ensureReceiver();
|
|
103
|
-
const
|
|
187
|
+
const deploymentId = randomUUID();
|
|
188
|
+
const event: DeployLifecycleEvent = { name: componentName, phase: 'start', deploymentId, ownerThreadId: threadId };
|
|
104
189
|
deployLifecycle._handle(event); // local thread first
|
|
105
190
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
106
191
|
try {
|
|
@@ -125,6 +210,7 @@ export async function broadcastDeployStart(componentName: string): Promise<void>
|
|
|
125
210
|
} finally {
|
|
126
211
|
if (timer) clearTimeout(timer);
|
|
127
212
|
}
|
|
213
|
+
return deploymentId;
|
|
128
214
|
}
|
|
129
215
|
|
|
130
216
|
/**
|
|
@@ -135,9 +221,9 @@ export async function broadcastDeployStart(componentName: string): Promise<void>
|
|
|
135
221
|
* Must be called exactly once per matching broadcastDeployStart, even if the
|
|
136
222
|
* deploy errored — typically from a `finally` block.
|
|
137
223
|
*/
|
|
138
|
-
export function broadcastDeployEnd(componentName: string): void {
|
|
224
|
+
export function broadcastDeployEnd(componentName: string, deploymentId: string): void {
|
|
139
225
|
ensureReceiver();
|
|
140
|
-
const event: DeployLifecycleEvent = { name: componentName, phase: 'end' };
|
|
226
|
+
const event: DeployLifecycleEvent = { name: componentName, phase: 'end', deploymentId, ownerThreadId: threadId };
|
|
141
227
|
deployLifecycle._handle(event);
|
|
142
228
|
try {
|
|
143
229
|
void broadcast({ type: DEPLOY_LIFECYCLE_MSG, event }, false);
|
|
@@ -362,6 +362,16 @@ export function isSuperUser(user: AuthedUser | undefined): boolean {
|
|
|
362
362
|
return user?.role?.permission?.super_user === true;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
/**
|
|
366
|
+
* True if the request carries an authenticated principal. Anonymous MCP sessions are a supported
|
|
367
|
+
* deployment (the public-docs case, #1609) — the adapter hands them through as `{ username: '' }`
|
|
368
|
+
* rather than rejecting them — so a listing filter that means "any real user" has to test for this
|
|
369
|
+
* explicitly. `user?.role` is not a proxy: an authenticated user may legitimately carry no role.
|
|
370
|
+
*/
|
|
371
|
+
export function isAuthenticated(user: AuthedUser | undefined): boolean {
|
|
372
|
+
return typeof user?.username === 'string' && user.username.length > 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
365
375
|
/**
|
|
366
376
|
* Class-level verb introspection. Mirrors `resources/openApi.ts:149-153` so
|
|
367
377
|
* a class is considered to "have" a verb only if its prototype overrides the
|
|
@@ -27,6 +27,7 @@ import harperLogger from '../../../utility/logging/harper_logger.ts';
|
|
|
27
27
|
import {
|
|
28
28
|
addTool,
|
|
29
29
|
clearProfileTools,
|
|
30
|
+
isAuthenticated,
|
|
30
31
|
snapshotProfileTools,
|
|
31
32
|
type AuthedUser,
|
|
32
33
|
type ToolCallContext,
|
|
@@ -708,11 +709,17 @@ function makeVisibleTo(
|
|
|
708
709
|
return function visibleTo(user: AuthedUser): boolean {
|
|
709
710
|
// Super-user sees everything.
|
|
710
711
|
if (user?.role?.permission?.super_user === true) return true;
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
|
|
712
|
+
// A Resource with no backing table has no static permission gate to consult, so listing is
|
|
713
|
+
// open to any authenticated caller and the Resource's own `allow*` predicates enforce at call
|
|
714
|
+
// time — hiding these while still accepting `tools/call` on them bought no security, only
|
|
715
|
+
// undiscoverable tools (#1940).
|
|
716
|
+
//
|
|
717
|
+
// Anonymous sessions stay excluded. They are a supported deployment (#1609) and reach here as
|
|
718
|
+
// `{ username: '' }`, so this must be tested explicitly. Custom `mcpTools` DO list to them, but
|
|
719
|
+
// that is an author opt-in — declaring `static mcpTools` is a deliberate act of publishing.
|
|
720
|
+
// Verb tools are generated for every exported Resource with no author action, so publishing an
|
|
721
|
+
// app's field names and docstrings to unauthenticated callers by default is not equivalent.
|
|
722
|
+
if (!databaseName || !tableName) return isAuthenticated(user);
|
|
716
723
|
const perm = getUserTablePermissions(user, databaseName, tableName);
|
|
717
724
|
if (!perm) return false;
|
|
718
725
|
if (mode === 'read') return perm.read === true || perm.describe === true;
|
|
@@ -176,6 +176,9 @@ const DESTRUCTIVE_OPERATIONS: ReadonlySet<string> = new Set([
|
|
|
176
176
|
'restart_service',
|
|
177
177
|
'set_configuration',
|
|
178
178
|
'remove_node',
|
|
179
|
+
'restore_backup',
|
|
180
|
+
'delete_backup',
|
|
181
|
+
'purge_backups',
|
|
179
182
|
'delete_deployment_payload',
|
|
180
183
|
]);
|
|
181
184
|
|
|
@@ -300,6 +303,12 @@ export function makeOperationToolHandler(operationName: string) {
|
|
|
300
303
|
operation: operationName,
|
|
301
304
|
hdb_user: context.user,
|
|
302
305
|
};
|
|
306
|
+
// Authorization bypass is internal dispatch state, never client-supplied tool
|
|
307
|
+
// arguments — mirrors handlePostRequest's stripping in serverHandlers.js. Without
|
|
308
|
+
// this, a caller-supplied `bypass_auth`/`bypassAuth` argument would reach handlers
|
|
309
|
+
// (e.g. createTokens) that trust it directly from the body.
|
|
310
|
+
delete body.bypass_auth;
|
|
311
|
+
delete body.bypassAuth;
|
|
303
312
|
if (STREAMING_OPERATIONS.has(operationName)) {
|
|
304
313
|
// Reject before the handler runs at all — see STREAMING_OPERATIONS' comment for why
|
|
305
314
|
// destroying an already-returned stream isn't sufficient cleanup for these producers.
|
|
@@ -60,6 +60,9 @@ export const OPERATION_DESCRIPTIONS: Record<string, string> = {
|
|
|
60
60
|
// list_deployments: components/deploymentOperations.ts:50 — Deployment history with filtering and pagination.
|
|
61
61
|
list_deployments:
|
|
62
62
|
'Lists deployments from system.hdb_deployment with optional filtering by project, status, and date range, plus pagination.',
|
|
63
|
+
// list_backups: dataLayer/rocksdbBackup.ts (listBackups) — Managed RocksDB backup metadata for a database.
|
|
64
|
+
list_backups:
|
|
65
|
+
'Lists the managed RocksDB directory backups for a database (id, timestamp, size, file count). Requires super_user; RocksDB databases only.',
|
|
63
66
|
|
|
64
67
|
// ─── DEFAULT_ALLOW: search_* ──────────────────────────────────────────
|
|
65
68
|
// search_by_conditions: dataLayer/search.ts:6 — Multi-condition search with comparators.
|
|
@@ -101,6 +101,15 @@ export const OPERATION_INPUT_SCHEMAS: Record<string, object> = {
|
|
|
101
101
|
properties: {},
|
|
102
102
|
description: 'Lists deployed component versions in this Harper instance.',
|
|
103
103
|
},
|
|
104
|
+
list_backups: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
database: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Database name whose managed RocksDB backups to list. Defaults to `data`.',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
104
113
|
|
|
105
114
|
// ─── search_* ─────────────────────────────────────────────────────────
|
|
106
115
|
search_by_hash: {
|
package/components/operations.js
CHANGED
|
@@ -410,6 +410,7 @@ async function deployComponent(req) {
|
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
412
|
if (req.urlPath !== undefined) applicationConfig.urlPath = req.urlPath;
|
|
413
|
+
if (req.host !== undefined) applicationConfig.host = req.host;
|
|
413
414
|
// Persist credential references (never tokens) so every cold install of this component —
|
|
414
415
|
// reboot, new peer, rollback — re-resolves the credential from the store.
|
|
415
416
|
if (credentialReferences.length) applicationConfig.credentials = credentialReferences;
|
|
@@ -645,12 +646,10 @@ async function deployComponent(req) {
|
|
|
645
646
|
// deploy checks its own local isNewComponent, since directory state (and therefore
|
|
646
647
|
// whether the component was already active) can differ per node.
|
|
647
648
|
//
|
|
648
|
-
// An existing
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
// changes and requests the restart itself.
|
|
653
|
-
if (application.isNewComponent) {
|
|
649
|
+
// An existing component's watched files are handled by Scope/EntryHandler. Package
|
|
650
|
+
// metadata is deliberately outside most plugin globs, so compare it across the atomic
|
|
651
|
+
// swap as well: a dependency or module-entry change also invalidates loaded code.
|
|
652
|
+
if (application.isNewComponent || application.packageMetadataChanged) {
|
|
654
653
|
const { requestRestart } = require('./requestRestart.ts');
|
|
655
654
|
requestRestart();
|
|
656
655
|
}
|