@harperfast/harper 5.2.0 → 5.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/copyDb.ts +40 -11
- package/bin/harper.ts +20 -51
- 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 +68 -24
- package/components/componentLoader.ts +70 -16
- package/components/deployLifecycle.ts +119 -33
- package/components/deploymentRecorder.ts +102 -11
- package/components/mcp/tools/operations.ts +6 -0
- package/components/operations.js +8 -6
- package/config/configUtils.ts +78 -20
- package/config/harperConfigEnvVars.ts +114 -17
- package/config-root.schema.json +10 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/copyDb.js +33 -9
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +6 -0
- package/dist/bin/harper.js +18 -50
- 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 +1 -0
- package/dist/components/Scope.js +69 -22
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +69 -10
- 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/deploymentRecorder.d.ts +6 -0
- package/dist/components/deploymentRecorder.js +94 -12
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +6 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/operations.js +8 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +6 -0
- package/dist/config/configUtils.js +78 -18
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.js +108 -16
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +57 -0
- package/dist/resources/DatabaseTransaction.js +284 -104
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -2
- 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 +8 -6
- package/dist/resources/Table.js +158 -31
- 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/auditStore.js +27 -1
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +0 -1
- package/dist/resources/blob.js +15 -7
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +22 -5
- package/dist/resources/databases.js +118 -9
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.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/search.d.ts +1 -1
- package/dist/resources/search.js +7 -3
- package/dist/resources/search.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/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.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 +19 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +10 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +57 -18
- 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/manageThreads.js +8 -0
- package/dist/server/threads/manageThreads.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/sqlEngine/parser/normalizer.js +37 -9
- package/dist/sqlEngine/parser/normalizer.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/environment/environmentManager.d.ts +11 -0
- package/dist/utility/environment/environmentManager.js +154 -0
- package/dist/utility/environment/environmentManager.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 +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- 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/lmdb/environmentUtility.js +2 -1
- package/dist/utility/lmdb/environmentUtility.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +7 -0
- package/dist/utility/logging/harper_logger.js +57 -24
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/npm-shrinkwrap.json +208 -198
- package/package.json +10 -8
- package/resources/DESIGN.md +1 -0
- package/resources/DatabaseTransaction.ts +313 -102
- package/resources/ResourceInterface.ts +8 -2
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +398 -259
- package/resources/analytics/write.ts +22 -20
- package/resources/auditStore.ts +32 -1
- package/resources/blob.ts +15 -8
- package/resources/databases.ts +123 -12
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
- 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/search.ts +8 -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 +45 -34
- package/server/http.ts +100 -13
- 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 +20 -3
- package/server/serverHelpers/serverUtilities.ts +10 -7
- package/server/static.ts +75 -20
- package/server/storageReclamation.ts +104 -8
- package/server/threads/manageThreads.js +8 -0
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +2 -0
- package/studio/web/assets/{Chat-DoVWScmq.js → Chat-BMUjPklt.js} +5 -5
- package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-BMUjPklt.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-Cbghuxe5.js} +5 -5
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
- package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-CkgTQAqc.js} +2 -2
- package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-CkgTQAqc.js.map} +1 -1
- package/studio/web/assets/{applications-D03NA7wW.js → applications-WvF4NOTN.js} +13 -13
- package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
- package/studio/web/assets/index-CDeqVx71.css +1 -0
- package/studio/web/assets/index-Sevt3MVx.js +819 -0
- package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
- package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
- package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
- package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
- package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
- package/studio/web/assets/{notifications-0edoFTsb.js → notifications-Ap84YyQp.js} +2 -2
- package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-Ap84YyQp.js.map} +1 -1
- package/studio/web/assets/notifications-vehqyOLM.js +2 -0
- package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-vehqyOLM.js.map} +1 -1
- package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
- package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
- package/studio/web/assets/{profile-DUfEPQtx.js → profile-DzDpRLHY.js} +2 -2
- package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-DzDpRLHY.js.map} +1 -1
- package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
- package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
- package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
- package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
- package/studio/web/assets/register-BDy01-q3.js +3 -0
- package/studio/web/assets/register-BDy01-q3.js.map +1 -0
- package/studio/web/assets/register-CQHRwNP4.js +3 -0
- package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-D0aPcI3W.js} +2 -2
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
- package/studio/web/assets/setup-I15FAF9H.js +3 -0
- package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-I15FAF9H.js.map} +1 -1
- package/studio/web/assets/status-BF1eJshJ.js +62 -0
- package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-DwSPXXhl.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
- package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
- package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
- package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-Bu51A-VG.js} +2 -2
- package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-Bu51A-VG.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-DrV1zMrN.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
- package/studio/web/index.html +6 -7
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/environment/environmentManager.ts +152 -0
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/install/installer.ts +37 -0
- package/utility/lmdb/environmentUtility.ts +2 -1
- package/utility/logging/harper_logger.ts +57 -26
- package/studio/web/assets/applications-D03NA7wW.js.map +0 -1
- package/studio/web/assets/index-Bh_CNAHr.js +0 -809
- package/studio/web/assets/index-Bh_CNAHr.js.map +0 -1
- package/studio/web/assets/index-C8Mv9kHV.css +0 -1
- package/studio/web/assets/index.lazy-Dx3MpyDC.js +0 -14
- package/studio/web/assets/notifications-CwKhipK7.js +0 -2
- package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
- package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
- package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
- package/studio/web/assets/setup-B56Oz1_u.js +0 -3
- package/studio/web/assets/status-BAod7p3o.js +0 -62
- package/studio/web/assets/status-BAod7p3o.js.map +0 -1
- package/studio/web/assets/textarea-DhLMZ3PA.js.map +0 -1
package/server/http.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { Blob } from '../resources/blob.ts';
|
|
29
29
|
import { recordAction, recordActionBinary } from '../resources/analytics/write.ts';
|
|
30
30
|
import { Readable, Writable, pipeline } from 'node:stream';
|
|
31
|
-
import { mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
31
|
+
import { mkdirSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'node:fs';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { server, type ServerOptions, type HttpOptions, type UpgradeOptions, UpgradeListener } from './Server.ts';
|
|
34
34
|
import { setPortServerMap, SERVERS, socketOptionDefaults } from './serverRegistry.ts';
|
|
@@ -67,7 +67,14 @@ export const uwsServeConfigs: Record<string, any> = {};
|
|
|
67
67
|
// backends that don't register their own Node http server (Bun, and the uWS HTTP path). Keeping
|
|
68
68
|
// them out of SERVERS is what prevents a competing Node server from binding the same port.
|
|
69
69
|
const fallbackServers: Record<string | number, any> = {};
|
|
70
|
-
|
|
70
|
+
// `ino`/`dev` are set once this worker's own bind is confirmed (see recordUdsBindSuccess) and are
|
|
71
|
+
// what make cleanupUdsFiles/markUdsBindFailed ownership-aware: on an overlapping restart (see
|
|
72
|
+
// restartWorkers()), the replacement worker rebinds this exact path before the outgoing worker
|
|
73
|
+
// exits, so by the time the outgoing worker cleans up, the identity on disk may already belong to
|
|
74
|
+
// the replacement, not to it. Captured as bigint (statSync's `{ bigint: true }` form): a plain
|
|
75
|
+
// Number loses precision above 2^53, which some filesystems (e.g. XFS with inode64) can exceed,
|
|
76
|
+
// letting two genuinely different inodes compare equal.
|
|
77
|
+
const udsCleanupPaths: { socketPath: string; yamlPath: string; identity?: { dev: bigint; ino: bigint } }[] = [];
|
|
71
78
|
// Sockets whose listen() has failed (see threadServer.js's fail-soft UDS handling). A TLS mirror's
|
|
72
79
|
// YAML metadata is written on its own schedule (SNICallback readiness / cert reload), independent of
|
|
73
80
|
// whether the mirror socket ever actually bound, so a failed bind must both cancel any metadata write
|
|
@@ -79,24 +86,100 @@ export function registerUdsCleanupPaths(socketPath: string, yamlPath: string) {
|
|
|
79
86
|
udsCleanupPaths.push({ socketPath, yamlPath });
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Record that this worker's own bind at `socketPath` succeeded, capturing the (dev, inode) pair that
|
|
91
|
+
* proves ownership. Call right after a mirror socket starts listening (see the bind sites in
|
|
92
|
+
* threadServer.js — the Node/h2/raw-socket mirrors via listenOnDomainSocket's onListening, the Bun
|
|
93
|
+
* path right after Bun.serve() returns, and the uWS path once createUwsServer() resolves). A no-op
|
|
94
|
+
* if `socketPath` was never registered.
|
|
95
|
+
*/
|
|
96
|
+
export function recordUdsBindSuccess(socketPath: string) {
|
|
97
|
+
const entry = udsCleanupPaths.find((candidate) => candidate.socketPath === socketPath);
|
|
98
|
+
if (!entry) return;
|
|
99
|
+
try {
|
|
100
|
+
const stat = statSync(socketPath, { bigint: true });
|
|
101
|
+
entry.identity = { dev: stat.dev, ino: stat.ino };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Extremely unlikely (we just bound this path), but silent failure here is expensive: the
|
|
104
|
+
// entry is permanently treated as unowned, so this worker leaks both files at shutdown with
|
|
105
|
+
// nothing in the logs to explain why.
|
|
106
|
+
harperLogger.warn(`Could not stat freshly bound UDS mirror at ${socketPath}`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 'owned' — the (dev, inode) this worker recorded at bind time still matches what's on disk.
|
|
112
|
+
* 'foreign' — a socket exists at the path, but with a different identity (or none was ever
|
|
113
|
+
* recorded): something else — almost always a replacement worker — currently owns it. Also the
|
|
114
|
+
* safe default when statSync fails for any reason other than ENOENT (e.g. EACCES): we can't
|
|
115
|
+
* confirm the path is genuinely empty, so treat it as though something we can't verify owns it.
|
|
116
|
+
* 'absent' — statSync confirmed ENOENT: nothing is on disk at the path. There is no live owner to
|
|
117
|
+
* protect.
|
|
118
|
+
*/
|
|
119
|
+
function ownershipOf(entry: {
|
|
120
|
+
socketPath: string;
|
|
121
|
+
identity?: { dev: bigint; ino: bigint };
|
|
122
|
+
}): 'owned' | 'foreign' | 'absent' {
|
|
123
|
+
let current: { dev: bigint; ino: bigint };
|
|
124
|
+
try {
|
|
125
|
+
const stat = statSync(entry.socketPath, { bigint: true });
|
|
126
|
+
current = { dev: stat.dev, ino: stat.ino };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'absent' : 'foreign';
|
|
129
|
+
}
|
|
130
|
+
return entry.identity !== undefined && current.dev === entry.identity.dev && current.ino === entry.identity.ino
|
|
131
|
+
? 'owned'
|
|
132
|
+
: 'foreign';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mark a Unix domain socket's listen() as failed: suppress its metadata, and remove any already
|
|
137
|
+
* written UNLESS a live socket at this path currently belongs to someone else (a replacement worker
|
|
138
|
+
* that already rebound it). Unlike cleanupUdsFiles, a failed bind never produced a socket of its own,
|
|
139
|
+
* so 'absent' here just means no bind ever won this path — not evidence of a live replacement — and
|
|
140
|
+
* is safe to retract stale metadata for (see server/DESIGN.md's UDS ownership note for why this can't
|
|
141
|
+
* require 'owned': the only real caller, an overlong path, fails before listen() ever runs, so this
|
|
142
|
+
* worker's own identity is never recorded).
|
|
143
|
+
*/
|
|
83
144
|
export function markUdsBindFailed(socketPath: string) {
|
|
84
145
|
failedUdsPaths.add(socketPath);
|
|
85
146
|
const entry = udsCleanupPaths.find((candidate) => candidate.socketPath === socketPath);
|
|
86
|
-
if (entry)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
147
|
+
if (!entry || ownershipOf(entry) === 'foreign') return;
|
|
148
|
+
try {
|
|
149
|
+
unlinkSync(entry.yamlPath);
|
|
150
|
+
} catch {}
|
|
91
151
|
}
|
|
92
152
|
|
|
93
153
|
export function cleanupUdsFiles() {
|
|
94
|
-
for (const
|
|
154
|
+
for (const entry of udsCleanupPaths) {
|
|
155
|
+
// ownershipOf() and the unlinks below are separate syscalls, so a replacement that unlinks
|
|
156
|
+
// and rebinds in the gap between them would still lose its files — unavoidable in Node
|
|
157
|
+
// (no unlink-if-inode-matches primitive), but this narrows that window from the
|
|
158
|
+
// seconds-wide one this fix closes down to microseconds.
|
|
159
|
+
const ownership = ownershipOf(entry);
|
|
160
|
+
// Consume this entry's recorded identity on first use, regardless of outcome. This function
|
|
161
|
+
// runs twice per worker (threadServer.js's SHUTDOWN handler, while this worker's own
|
|
162
|
+
// socket is still open, then again from process.on('exit') after closeServers() has
|
|
163
|
+
// released it) — and a freed inode can be handed to an unrelated later bind before the
|
|
164
|
+
// second call runs. Leaving the stale value in place would let that second call
|
|
165
|
+
// coincidentally re-match a path it no longer has any claim to; clearing it here makes a
|
|
166
|
+
// repeat call a guaranteed no-op instead of a second chance to delete a stranger's files.
|
|
167
|
+
entry.identity = undefined;
|
|
168
|
+
// Unlike markUdsBindFailed (where 'absent' is genuinely safe — its only caller runs
|
|
169
|
+
// synchronously before any bind attempt, with no window for anyone else to be mid-flight),
|
|
170
|
+
// this function's second call (process.on('exit')) can land seconds after the first, while
|
|
171
|
+
// a concurrently-booting replacement — on the non-overlapping-restart platforms, started
|
|
172
|
+
// right after this worker's SHUTDOWN, not after it fully exits — may have already written
|
|
173
|
+
// its own fresh yaml (SNICallback.ready resolves independently of its bind) without having
|
|
174
|
+
// bound its socket yet. Treating that window's 'absent' as retractable would delete the
|
|
175
|
+
// replacement's yaml out from under it. Only 'owned' is safe here; a stale yaml this worker
|
|
176
|
+
// never bound (or no longer owns) is left for markUdsBindFailed or the startup sweep.
|
|
177
|
+
if (ownership !== 'owned') continue;
|
|
95
178
|
try {
|
|
96
|
-
unlinkSync(socketPath);
|
|
179
|
+
unlinkSync(entry.socketPath);
|
|
97
180
|
} catch {}
|
|
98
181
|
try {
|
|
99
|
-
unlinkSync(yamlPath);
|
|
182
|
+
unlinkSync(entry.yamlPath);
|
|
100
183
|
} catch {}
|
|
101
184
|
}
|
|
102
185
|
}
|
|
@@ -161,9 +244,13 @@ export function writeUdsMetadata(
|
|
|
161
244
|
}
|
|
162
245
|
}
|
|
163
246
|
|
|
164
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Clean all files in the sockets directory. Call from main thread on process startup, before any
|
|
249
|
+
* worker can bind. Unconditional on the current TLS_UNIXDOMAINSOCKETS setting — the directory is
|
|
250
|
+
* Harper-owned and holds nothing but mirror .sock/.yaml files, so a crash that left stale ones
|
|
251
|
+
* behind while UDS was enabled must still be swept even if this boot has since disabled it.
|
|
252
|
+
*/
|
|
165
253
|
export function cleanupSocketsDirectory() {
|
|
166
|
-
if (!env.get(terms.CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) return;
|
|
167
254
|
const socketsDir = join(env.getHdbBasePath(), 'sockets');
|
|
168
255
|
try {
|
|
169
256
|
for (const file of readdirSync(socketsDir)) {
|
|
@@ -137,10 +137,8 @@ export interface ImpersonatePayload {
|
|
|
137
137
|
|
|
138
138
|
interface BaseOperationRequestBody {
|
|
139
139
|
operation: OperationFunctionName;
|
|
140
|
-
bypassAuth: boolean;
|
|
141
140
|
hdb_user?: userSchema.User;
|
|
142
141
|
hdbAuthHeader?: unknown;
|
|
143
|
-
bypass_auth?: boolean;
|
|
144
142
|
impersonate?: ImpersonatePayload;
|
|
145
143
|
password?: string;
|
|
146
144
|
payload?: string;
|
|
@@ -11,6 +11,7 @@ interface MultipartBody {
|
|
|
11
11
|
|
|
12
12
|
const FIELD_SIZE_LIMIT = 1024 * 1024; // 1 MB per non-file field — generous for JSON params
|
|
13
13
|
const MAX_FIELDS = 64;
|
|
14
|
+
const UNSAFE_FIELD_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
14
15
|
// Operation handlers stream the file part directly into extraction (gunzip + tar-fs),
|
|
15
16
|
// so there is no separate filesize cap to enforce here. Backpressure flows through busboy
|
|
16
17
|
// → the file Readable → the consumer, bounded by disk space rather than memory.
|
|
@@ -73,6 +74,14 @@ export function parseMultipartRequest(
|
|
|
73
74
|
logger.warn?.(`Multipart field "${name}" arrived after the file part; ignoring`);
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
77
|
+
if (UNSAFE_FIELD_NAMES.has(name)) {
|
|
78
|
+
const error = new ClientError(`Multipart field "${name}" is not allowed`, 400);
|
|
79
|
+
callDone(error);
|
|
80
|
+
// Stop accepting bytes from a malicious request immediately. The raw-stream error
|
|
81
|
+
// handler below also tears down busboy, so a later file part cannot remain unread.
|
|
82
|
+
rawStream.destroy(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
76
85
|
body[name] = decodeFieldValue(value);
|
|
77
86
|
});
|
|
78
87
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
const operationAuthorizationState = new AsyncLocalStorage<boolean>();
|
|
4
|
+
|
|
5
|
+
export function runWithOperationAuthorizationBypass<T>(bypassAuth: boolean, callback: () => T): T {
|
|
6
|
+
return operationAuthorizationState.run(bypassAuth === true, callback);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isOperationAuthorizationBypassed(): boolean {
|
|
10
|
+
return operationAuthorizationState.getStore() === true;
|
|
11
|
+
}
|
|
@@ -27,6 +27,7 @@ import harperLogger from '../../utility/logging/harper_logger.ts';
|
|
|
27
27
|
import { ServerError } from '../../utility/errors/hdbError.ts';
|
|
28
28
|
import { sendItcEvent } from '../threads/itc.js';
|
|
29
29
|
import { onMessageByType, onThreadExit } from '../threads/manageThreads.js';
|
|
30
|
+
import { runWithOperationAuthorizationBypass } from './operationAuthorizationState.ts';
|
|
30
31
|
|
|
31
32
|
const operationLog = harperLogger.loggerWithTag('operation');
|
|
32
33
|
|
|
@@ -49,7 +50,7 @@ const EXECUTE_TIMEOUT_MS = env.get(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_TIM
|
|
|
49
50
|
// survive the dist build). A worker can only receive an execute request after announcing a
|
|
50
51
|
// registration — which goes through serverUtilities — so these are always set on that path.
|
|
51
52
|
let localDispatch: {
|
|
52
|
-
chooseOperation: (body: any) => Function;
|
|
53
|
+
chooseOperation: (body: any, bypassAuth?: boolean) => Function;
|
|
53
54
|
processLocalTransaction: (req: any, operationFunction: Function) => Promise<any>;
|
|
54
55
|
};
|
|
55
56
|
export function setLocalOperationDispatch(dispatch: typeof localDispatch) {
|
|
@@ -99,10 +100,13 @@ let rotation = 0;
|
|
|
99
100
|
* function and metadata that `verifyPerms` needs only exist on the worker, which runs the full
|
|
100
101
|
* `chooseOperation` check with the forwarded `hdb_user`.
|
|
101
102
|
*/
|
|
102
|
-
export function getRemoteOperationFunction(
|
|
103
|
+
export function getRemoteOperationFunction(
|
|
104
|
+
name: string,
|
|
105
|
+
bypassAuth = false
|
|
106
|
+
): ((body: any) => Promise<any>) | undefined {
|
|
103
107
|
if (!isMainThread) return undefined;
|
|
104
108
|
if (!registeredByWorker.has(name)) return undefined;
|
|
105
|
-
return (body: any) => executeRemoteOperation(name, body);
|
|
109
|
+
return (body: any) => executeRemoteOperation(name, body, bypassAuth);
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
function attachMainListeners() {
|
|
@@ -135,7 +139,7 @@ function attachMainListeners() {
|
|
|
135
139
|
});
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
async function executeRemoteOperation(name: string, body: any): Promise<any> {
|
|
142
|
+
async function executeRemoteOperation(name: string, body: any, bypassAuth: boolean): Promise<any> {
|
|
139
143
|
attachMainListeners();
|
|
140
144
|
const workerIds = registeredByWorker.get(name);
|
|
141
145
|
const forwardBody = { ...body };
|
|
@@ -151,7 +155,7 @@ async function executeRemoteOperation(name: string, body: any): Promise<any> {
|
|
|
151
155
|
try {
|
|
152
156
|
sent = threads.sendToThread(targetThreadId, {
|
|
153
157
|
type: terms.ITC_EVENT_TYPES.OPERATION_EXECUTE_REQUEST,
|
|
154
|
-
message: { requestId, body: forwardBody, originator: threadId },
|
|
158
|
+
message: { requestId, body: forwardBody, bypassAuth, originator: threadId },
|
|
155
159
|
});
|
|
156
160
|
} catch (error) {
|
|
157
161
|
// Structured-clone failure (e.g. a function or native handle on the request body) is a
|
|
@@ -198,20 +202,20 @@ async function executeRemoteOperation(name: string, body: any): Promise<any> {
|
|
|
198
202
|
* (permission check + processLocalTransaction) and send the result back to the main thread.
|
|
199
203
|
*/
|
|
200
204
|
export async function operationExecuteRequestHandler(event: {
|
|
201
|
-
message: { requestId: number; body: any; originator: number };
|
|
205
|
+
message: { requestId: number; body: any; bypassAuth?: boolean; originator: number };
|
|
202
206
|
}) {
|
|
203
|
-
const { requestId, body, originator } = event.message;
|
|
207
|
+
const { requestId, body, bypassAuth, originator } = event.message;
|
|
204
208
|
let response;
|
|
205
209
|
try {
|
|
206
210
|
if (!localDispatch) throw new ServerError('This worker thread cannot execute operations', 503);
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
// Authorization state travels in the trusted same-process ITC envelope, never in the
|
|
212
|
+
// caller-controlled operation body. This preserves server.operation(..., false) across
|
|
213
|
+
// worker forwarding without creating a client-spoofable request property.
|
|
214
|
+
const trustedBypassAuth = bypassAuth === true;
|
|
215
|
+
const result = await runWithOperationAuthorizationBypass(trustedBypassAuth, () => {
|
|
216
|
+
const operationFunction = localDispatch.chooseOperation(body, trustedBypassAuth);
|
|
217
|
+
return localDispatch.processLocalTransaction({ body }, operationFunction);
|
|
218
|
+
});
|
|
215
219
|
if (result instanceof Readable || typeof result?.pipe === 'function') {
|
|
216
220
|
// Streaming results would need MessagePort transfer plumbing — explicitly unsupported
|
|
217
221
|
// for worker-registered operations for now (#1736), rather than failing opaquely.
|
|
@@ -4,7 +4,7 @@ const terms = require('../../utility/hdbTerms.ts');
|
|
|
4
4
|
const hdbUtil = require('../../utility/common_utils.ts');
|
|
5
5
|
const harperLogger = require('../../utility/logging/harper_logger.ts');
|
|
6
6
|
const { realExit } = require('../threads/workerProcessGuard.ts');
|
|
7
|
-
const { handleHDBError, hdbErrors } = require('../../utility/errors/hdbError.ts');
|
|
7
|
+
const { ClientError, handleHDBError, hdbErrors } = require('../../utility/errors/hdbError.ts');
|
|
8
8
|
const { isMainThread } = require('worker_threads');
|
|
9
9
|
const { Readable } = require('stream');
|
|
10
10
|
|
|
@@ -43,6 +43,19 @@ const NO_AUTH_OPERATIONS = [
|
|
|
43
43
|
terms.OPERATIONS_ENUM.LOGOUT,
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
const UNSAFE_REQUEST_BODY_PROPERTIES = ['__proto__', 'constructor', 'prototype'];
|
|
47
|
+
|
|
48
|
+
function validateRequestBodyProperties(body) {
|
|
49
|
+
if (!body || typeof body !== 'object') {
|
|
50
|
+
throw new ClientError('Invalid request body', 400);
|
|
51
|
+
}
|
|
52
|
+
for (const property of UNSAFE_REQUEST_BODY_PROPERTIES) {
|
|
53
|
+
if (Object.hasOwn(body, property)) {
|
|
54
|
+
throw new ClientError(`Request body property "${property}" is not allowed`, 400);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
46
59
|
function handleServerUncaughtException(err) {
|
|
47
60
|
let message = `Found an uncaught exception with message: ${err.message}. ${os.EOL}Stack: ${err.stack} ${
|
|
48
61
|
os.EOL
|
|
@@ -145,8 +158,12 @@ async function handlePostRequest(req, res, _bypassAuth = false) {
|
|
|
145
158
|
let operation_function;
|
|
146
159
|
|
|
147
160
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
validateRequestBodyProperties(req.body);
|
|
162
|
+
// Authorization bypass is internal dispatch state, never client request data. Dispatch no
|
|
163
|
+
// longer reads these properties, but remove both true and false values before operation
|
|
164
|
+
// code sees the body.
|
|
165
|
+
if (Object.hasOwn(req.body, 'bypass_auth')) delete req.body.bypass_auth;
|
|
166
|
+
if (Object.hasOwn(req.body, 'bypassAuth')) delete req.body.bypassAuth;
|
|
150
167
|
|
|
151
168
|
operation_function = serverUtilities.chooseOperation(req.body);
|
|
152
169
|
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
getRemoteOperationFunction,
|
|
49
49
|
setLocalOperationDispatch,
|
|
50
50
|
} from './registeredOperations.ts';
|
|
51
|
+
import { runWithOperationAuthorizationBypass } from './operationAuthorizationState.ts';
|
|
51
52
|
|
|
52
53
|
const pSearchSearch = util.promisify(search.search);
|
|
53
54
|
let pEvaluateSql: (sql: string) => Promise<any>;
|
|
@@ -203,7 +204,7 @@ server.setMcpQuotaHandler = (handler) => {
|
|
|
203
204
|
setMcpQuotaHandler(handler);
|
|
204
205
|
};
|
|
205
206
|
|
|
206
|
-
export function chooseOperation(json: OperationRequestBody) {
|
|
207
|
+
export function chooseOperation(json: OperationRequestBody, bypassAuth = false) {
|
|
207
208
|
let getOpResult: OperationFunctionObject;
|
|
208
209
|
try {
|
|
209
210
|
getOpResult = getOperationFunction(json);
|
|
@@ -214,7 +215,7 @@ export function chooseOperation(json: OperationRequestBody) {
|
|
|
214
215
|
// and its metadata actually exist, so no perm check is skipped by returning early
|
|
215
216
|
// here (#1736). Workers never re-forward, so an unknown op can't loop.
|
|
216
217
|
if (isMainThread) {
|
|
217
|
-
const remoteOperationFunction = getRemoteOperationFunction(json.operation);
|
|
218
|
+
const remoteOperationFunction = getRemoteOperationFunction(json.operation, bypassAuth);
|
|
218
219
|
if (remoteOperationFunction) return remoteOperationFunction;
|
|
219
220
|
}
|
|
220
221
|
operationLog.error(`Error when selecting operation function - ${err}`);
|
|
@@ -231,7 +232,7 @@ export function chooseOperation(json: OperationRequestBody) {
|
|
|
231
232
|
const sqlStatement = json.operation === 'sql' ? json.sql : json.search_operation.sql;
|
|
232
233
|
const parsedSqlObject = sql.convertSQLToAST(sqlStatement);
|
|
233
234
|
json.parsed_sql_object = parsedSqlObject;
|
|
234
|
-
if (!
|
|
235
|
+
if (!bypassAuth) {
|
|
235
236
|
const astPermCheck = sql.checkASTPermissions(json, parsedSqlObject);
|
|
236
237
|
if (astPermCheck) {
|
|
237
238
|
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
@@ -248,7 +249,7 @@ export function chooseOperation(json: OperationRequestBody) {
|
|
|
248
249
|
}
|
|
249
250
|
//we need to bypass permission checks to allow the createAuthorizationTokens
|
|
250
251
|
} else if (
|
|
251
|
-
!
|
|
252
|
+
!bypassAuth &&
|
|
252
253
|
json.operation !== terms.OPERATIONS_ENUM.CREATE_AUTHENTICATION_TOKENS &&
|
|
253
254
|
json.operation !== terms.OPERATIONS_ENUM.LOGIN &&
|
|
254
255
|
json.operation !== terms.OPERATIONS_ENUM.LOGOUT
|
|
@@ -310,9 +311,11 @@ _assignPackageExport('operation', operation);
|
|
|
310
311
|
*/
|
|
311
312
|
export function operation(operation: OperationRequestBody, context: Context, authorize: boolean) {
|
|
312
313
|
operation.hdb_user = context?.user;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
const bypassAuth = !authorize;
|
|
315
|
+
return runWithOperationAuthorizationBypass(bypassAuth, () => {
|
|
316
|
+
const operation_function = chooseOperation(operation, bypassAuth);
|
|
317
|
+
return processLocalTransaction({ body: operation }, operation_function);
|
|
318
|
+
});
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
interface Transaction {
|
package/server/static.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpathSync, existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { Scope } from '../components/Scope';
|
|
4
4
|
import { resolveBaseURLPath } from '../components/resolveBaseURLPath.ts';
|
|
5
5
|
import { convertToMS } from '../utility/common_utils.ts';
|
|
@@ -52,7 +52,7 @@ import send from 'send';
|
|
|
52
52
|
*
|
|
53
53
|
* This plugin dynamically updates its behavior based on the current configuration file. Users can make updates and immediately see the changes reflect in the next request.
|
|
54
54
|
*
|
|
55
|
-
* Updates to the `files` option
|
|
55
|
+
* Updates to the `files` option incrementally add newly matched paths and remove paths that no longer match while preserving common entries.
|
|
56
56
|
* Updates to `urlPath` request a restart: the HTTP route mount is registered once at load and cannot be re-registered on a live server (#1583).
|
|
57
57
|
*/
|
|
58
58
|
/**
|
|
@@ -146,7 +146,40 @@ export function handleApplication(scope: Scope) {
|
|
|
146
146
|
// in-memory map of static files
|
|
147
147
|
// keys are the URL paths relative to the mount base, values are the absolute paths to the files
|
|
148
148
|
const staticFiles = new Map<string, string>();
|
|
149
|
-
const indexEntries = new Map<string, string>();
|
|
149
|
+
const indexEntries = new Map<string, string | null>();
|
|
150
|
+
const staticFileOwners = new Map<string, Map<string, string>>();
|
|
151
|
+
const indexEntryOwners = new Map<string, Map<string, string | null>>();
|
|
152
|
+
|
|
153
|
+
function setOwnedPath<Path>(
|
|
154
|
+
paths: Map<string, Path>,
|
|
155
|
+
ownersByURL: Map<string, Map<string, Path>>,
|
|
156
|
+
urlPath: string,
|
|
157
|
+
owner: string,
|
|
158
|
+
path: Path
|
|
159
|
+
) {
|
|
160
|
+
let owners = ownersByURL.get(urlPath);
|
|
161
|
+
if (!owners) ownersByURL.set(urlPath, (owners = new Map()));
|
|
162
|
+
owners.delete(owner);
|
|
163
|
+
owners.set(owner, path);
|
|
164
|
+
paths.set(urlPath, path);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function removeOwnedPath<Path>(
|
|
168
|
+
paths: Map<string, Path>,
|
|
169
|
+
ownersByURL: Map<string, Map<string, Path>>,
|
|
170
|
+
urlPath: string,
|
|
171
|
+
owner: string
|
|
172
|
+
) {
|
|
173
|
+
const owners = ownersByURL.get(urlPath);
|
|
174
|
+
if (!owners?.delete(owner)) return;
|
|
175
|
+
const replacement = [...owners.values()].at(-1);
|
|
176
|
+
if (replacement === undefined) {
|
|
177
|
+
ownersByURL.delete(urlPath);
|
|
178
|
+
paths.delete(urlPath);
|
|
179
|
+
} else {
|
|
180
|
+
paths.set(urlPath, replacement);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
150
183
|
|
|
151
184
|
// The HTTP route below is registered once, with the urlPath in effect at load time; the mount
|
|
152
185
|
// cannot be re-registered at runtime. Capture the matching base once so map keys always agree
|
|
@@ -156,6 +189,9 @@ export function handleApplication(scope: Scope) {
|
|
|
156
189
|
// keep agreeing with the entry URL paths the file map is keyed by — but a redirect Location has
|
|
157
190
|
// to carry the application's mount too, or it would point outside the mount (#1583).
|
|
158
191
|
const externalBaseURLPath = scope.externalBasePath(baseURLPath);
|
|
192
|
+
let urlPathRestartPending = false;
|
|
193
|
+
let fileSelectionChangePending = false;
|
|
194
|
+
let entryHandler: ReturnType<Scope['handleEntry']>;
|
|
159
195
|
|
|
160
196
|
// A bare `before:` / `after:` key in YAML parses as null — treat it as unset, like before this
|
|
161
197
|
// option was validated.
|
|
@@ -187,14 +223,19 @@ export function handleApplication(scope: Scope) {
|
|
|
187
223
|
|
|
188
224
|
scope.options.on('change', (key) => {
|
|
189
225
|
if (key[0] === 'files') {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
226
|
+
fileSelectionChangePending = true;
|
|
227
|
+
const updateReady = entryHandler?.ready;
|
|
228
|
+
const clearPending = () => {
|
|
229
|
+
if (entryHandler?.ready === updateReady) fileSelectionChangePending = false;
|
|
230
|
+
};
|
|
231
|
+
updateReady?.then(clearPending, clearPending);
|
|
232
|
+
// EntryHandler updates are incremental: paths no longer matched emit unlink, newly matched
|
|
233
|
+
// paths emit add, and common unchanged paths remain valid in these maps.
|
|
234
|
+
scope.logger.info(`Static file matches updated due to change in ${key.join('.')}`);
|
|
194
235
|
return;
|
|
195
236
|
}
|
|
196
237
|
if (key[0] === 'urlPath') {
|
|
197
|
-
|
|
238
|
+
urlPathRestartPending = true;
|
|
198
239
|
scope.requestRestart();
|
|
199
240
|
return;
|
|
200
241
|
}
|
|
@@ -211,7 +252,12 @@ export function handleApplication(scope: Scope) {
|
|
|
211
252
|
});
|
|
212
253
|
|
|
213
254
|
// Handle entry events for the default entry handler based on the `files` and `urlPath` options
|
|
214
|
-
scope.handleEntry((entry) => {
|
|
255
|
+
entryHandler = scope.handleEntry((entry) => {
|
|
256
|
+
if (
|
|
257
|
+
urlPathRestartPending &&
|
|
258
|
+
(!fileSelectionChangePending || (entry.eventType !== 'unlink' && entry.eventType !== 'unlinkDir'))
|
|
259
|
+
)
|
|
260
|
+
return;
|
|
215
261
|
// entry.urlPath includes the component's base URL path, but when a `urlPath` is configured
|
|
216
262
|
// the routing chain strips that mount prefix from req.pathname before this plugin's handler
|
|
217
263
|
// runs — so key the maps relative to the base (#1583)
|
|
@@ -222,34 +268,43 @@ export function handleApplication(scope: Scope) {
|
|
|
222
268
|
switch (entry.eventType) {
|
|
223
269
|
// Directories only matter for the `index` files
|
|
224
270
|
case 'addDir':
|
|
225
|
-
case 'unlinkDir':
|
|
226
271
|
// Handle `index.html` for directories for if/when the user enables the `index` option
|
|
227
272
|
const indexPath = join(entry.absolutePath, 'index.html');
|
|
228
|
-
if (existsSync(indexPath))
|
|
229
|
-
|
|
230
|
-
|
|
273
|
+
if (existsSync(indexPath)) setOwnedPath(indexEntries, indexEntryOwners, urlPath, entry.absolutePath, indexPath);
|
|
274
|
+
break;
|
|
275
|
+
case 'unlinkDir':
|
|
276
|
+
removeOwnedPath(indexEntries, indexEntryOwners, urlPath, entry.absolutePath);
|
|
231
277
|
break;
|
|
232
278
|
// Otherwise, user must specify pattern to match individual files
|
|
233
279
|
case 'add':
|
|
234
280
|
// Store the file in memory for serving
|
|
235
|
-
staticFiles
|
|
281
|
+
setOwnedPath(staticFiles, staticFileOwners, urlPath, entry.absolutePath, entry.absolutePath);
|
|
236
282
|
// If the file is an index.html, also store it in the index entries
|
|
237
283
|
if (urlPath.endsWith('index.html')) {
|
|
238
284
|
// Without trailing slash; null -> 301 redirect to trailing slash
|
|
239
285
|
let lastSlashIndex = urlPath.lastIndexOf('/');
|
|
240
|
-
indexEntries
|
|
286
|
+
setOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex), entry.absolutePath, null);
|
|
241
287
|
// With trailing slash; serves the index.html file
|
|
242
|
-
|
|
288
|
+
setOwnedPath(
|
|
289
|
+
indexEntries,
|
|
290
|
+
indexEntryOwners,
|
|
291
|
+
urlPath.slice(0, lastSlashIndex + 1),
|
|
292
|
+
entry.absolutePath,
|
|
293
|
+
entry.absolutePath
|
|
294
|
+
);
|
|
243
295
|
}
|
|
244
296
|
break;
|
|
245
297
|
case 'unlink':
|
|
246
|
-
|
|
247
|
-
staticFiles.delete(urlPath);
|
|
298
|
+
removeOwnedPath(staticFiles, staticFileOwners, urlPath, entry.absolutePath);
|
|
248
299
|
// If the file is an index.html, remove it from the index entries as well
|
|
249
300
|
if (urlPath.endsWith('index.html')) {
|
|
250
301
|
let lastSlashIndex = urlPath.lastIndexOf('/');
|
|
251
|
-
|
|
252
|
-
|
|
302
|
+
const directoryURLPath = urlPath.slice(0, lastSlashIndex);
|
|
303
|
+
const directoryOwner = dirname(entry.absolutePath);
|
|
304
|
+
removeOwnedPath(indexEntries, indexEntryOwners, directoryURLPath, entry.absolutePath);
|
|
305
|
+
removeOwnedPath(indexEntries, indexEntryOwners, directoryURLPath, directoryOwner);
|
|
306
|
+
removeOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex + 1), entry.absolutePath);
|
|
307
|
+
removeOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex + 1), directoryOwner);
|
|
253
308
|
}
|
|
254
309
|
break;
|
|
255
310
|
}
|