@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.
Files changed (308) hide show
  1. package/bin/copyDb.ts +40 -11
  2. package/bin/harper.ts +20 -51
  3. package/bin/help.ts +216 -0
  4. package/components/Application.ts +236 -46
  5. package/components/ApplicationScope.ts +26 -0
  6. package/components/EntryHandler.ts +410 -105
  7. package/components/RuntimeModuleTracker.ts +189 -0
  8. package/components/Scope.ts +68 -24
  9. package/components/componentLoader.ts +70 -16
  10. package/components/deployLifecycle.ts +119 -33
  11. package/components/deploymentRecorder.ts +102 -11
  12. package/components/mcp/tools/operations.ts +6 -0
  13. package/components/operations.js +8 -6
  14. package/config/configUtils.ts +78 -20
  15. package/config/harperConfigEnvVars.ts +114 -17
  16. package/config-root.schema.json +10 -0
  17. package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
  18. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  19. package/dataLayer/hdbInfoController.ts +8 -0
  20. package/dataLayer/schemaDescribe.ts +2 -1
  21. package/dist/bin/copyDb.js +33 -9
  22. package/dist/bin/copyDb.js.map +1 -1
  23. package/dist/bin/harper.d.ts +6 -0
  24. package/dist/bin/harper.js +18 -50
  25. package/dist/bin/harper.js.map +1 -1
  26. package/dist/bin/help.d.ts +8 -0
  27. package/dist/bin/help.js +192 -0
  28. package/dist/bin/help.js.map +1 -0
  29. package/dist/components/Application.d.ts +16 -1
  30. package/dist/components/Application.js +210 -38
  31. package/dist/components/Application.js.map +1 -1
  32. package/dist/components/ApplicationScope.d.ts +7 -0
  33. package/dist/components/ApplicationScope.js +22 -0
  34. package/dist/components/ApplicationScope.js.map +1 -1
  35. package/dist/components/EntryHandler.d.ts +4 -4
  36. package/dist/components/EntryHandler.js +386 -95
  37. package/dist/components/EntryHandler.js.map +1 -1
  38. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  39. package/dist/components/RuntimeModuleTracker.js +189 -0
  40. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  41. package/dist/components/Scope.d.ts +1 -0
  42. package/dist/components/Scope.js +69 -22
  43. package/dist/components/Scope.js.map +1 -1
  44. package/dist/components/componentLoader.js +69 -10
  45. package/dist/components/componentLoader.js.map +1 -1
  46. package/dist/components/deployLifecycle.d.ts +6 -2
  47. package/dist/components/deployLifecycle.js +109 -31
  48. package/dist/components/deployLifecycle.js.map +1 -1
  49. package/dist/components/deploymentRecorder.d.ts +6 -0
  50. package/dist/components/deploymentRecorder.js +94 -12
  51. package/dist/components/deploymentRecorder.js.map +1 -1
  52. package/dist/components/mcp/tools/operations.js +6 -0
  53. package/dist/components/mcp/tools/operations.js.map +1 -1
  54. package/dist/components/operations.js +8 -6
  55. package/dist/components/operations.js.map +1 -1
  56. package/dist/config/configUtils.d.ts +6 -0
  57. package/dist/config/configUtils.js +78 -18
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/harperConfigEnvVars.js +108 -16
  60. package/dist/config/harperConfigEnvVars.js.map +1 -1
  61. package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
  62. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  63. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
  64. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  65. package/dist/dataLayer/hdbInfoController.js +4 -0
  66. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  67. package/dist/dataLayer/schemaDescribe.js +2 -1
  68. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  69. package/dist/resources/DatabaseTransaction.d.ts +57 -0
  70. package/dist/resources/DatabaseTransaction.js +284 -104
  71. package/dist/resources/DatabaseTransaction.js.map +1 -1
  72. package/dist/resources/ResourceInterface.d.ts +8 -2
  73. package/dist/resources/ResourceInterface.js.map +1 -1
  74. package/dist/resources/Resources.js +22 -4
  75. package/dist/resources/Resources.js.map +1 -1
  76. package/dist/resources/Table.d.ts +8 -6
  77. package/dist/resources/Table.js +158 -31
  78. package/dist/resources/Table.js.map +1 -1
  79. package/dist/resources/analytics/write.js +6 -6
  80. package/dist/resources/analytics/write.js.map +1 -1
  81. package/dist/resources/auditStore.js +27 -1
  82. package/dist/resources/auditStore.js.map +1 -1
  83. package/dist/resources/blob.d.ts +0 -1
  84. package/dist/resources/blob.js +15 -7
  85. package/dist/resources/blob.js.map +1 -1
  86. package/dist/resources/databases.d.ts +22 -5
  87. package/dist/resources/databases.js +118 -9
  88. package/dist/resources/databases.js.map +1 -1
  89. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
  90. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
  91. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  92. package/dist/resources/jsResource.d.ts +4 -26
  93. package/dist/resources/jsResource.js +5 -59
  94. package/dist/resources/jsResource.js.map +1 -1
  95. package/dist/resources/models/Models.d.ts +11 -1
  96. package/dist/resources/models/Models.js +10 -1
  97. package/dist/resources/models/Models.js.map +1 -1
  98. package/dist/resources/models/backendRegistry.d.ts +9 -0
  99. package/dist/resources/models/backendRegistry.js +10 -0
  100. package/dist/resources/models/backendRegistry.js.map +1 -1
  101. package/dist/resources/models/openaiStream.d.ts +16 -1
  102. package/dist/resources/models/openaiStream.js +113 -21
  103. package/dist/resources/models/openaiStream.js.map +1 -1
  104. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  105. package/dist/resources/models/v1/chatCompletions.js +115 -0
  106. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  107. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  108. package/dist/resources/models/v1/embeddings.js +71 -0
  109. package/dist/resources/models/v1/embeddings.js.map +1 -0
  110. package/dist/resources/models/v1/errors.d.ts +54 -0
  111. package/dist/resources/models/v1/errors.js +130 -0
  112. package/dist/resources/models/v1/errors.js.map +1 -0
  113. package/dist/resources/models/v1/index.d.ts +36 -0
  114. package/dist/resources/models/v1/index.js +75 -0
  115. package/dist/resources/models/v1/index.js.map +1 -0
  116. package/dist/resources/models/v1/models.d.ts +26 -0
  117. package/dist/resources/models/v1/models.js +44 -0
  118. package/dist/resources/models/v1/models.js.map +1 -0
  119. package/dist/resources/models/v1/translation.d.ts +133 -0
  120. package/dist/resources/models/v1/translation.js +298 -0
  121. package/dist/resources/models/v1/translation.js.map +1 -0
  122. package/dist/resources/roles.d.ts +1 -1
  123. package/dist/resources/roles.js +54 -7
  124. package/dist/resources/roles.js.map +1 -1
  125. package/dist/resources/search.d.ts +1 -1
  126. package/dist/resources/search.js +7 -3
  127. package/dist/resources/search.js.map +1 -1
  128. package/dist/security/jsLoader.js +84 -33
  129. package/dist/security/jsLoader.js.map +1 -1
  130. package/dist/security/role.js +4 -0
  131. package/dist/security/role.js.map +1 -1
  132. package/dist/security/superUserGuard.d.ts +7 -0
  133. package/dist/security/superUserGuard.js +23 -0
  134. package/dist/security/superUserGuard.js.map +1 -0
  135. package/dist/security/tokenAuthentication.d.ts +0 -1
  136. package/dist/security/tokenAuthentication.js +6 -2
  137. package/dist/security/tokenAuthentication.js.map +1 -1
  138. package/dist/security/user.d.ts +6 -1
  139. package/dist/security/user.js +23 -1
  140. package/dist/security/user.js.map +1 -1
  141. package/dist/server/http.d.ts +23 -2
  142. package/dist/server/http.js +99 -12
  143. package/dist/server/http.js.map +1 -1
  144. package/dist/server/operationsServer.d.ts +0 -2
  145. package/dist/server/operationsServer.js.map +1 -1
  146. package/dist/server/serverHelpers/multipartParser.js +9 -0
  147. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  148. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  149. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  150. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  151. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  152. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  153. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  154. package/dist/server/serverHelpers/serverHandlers.js +19 -3
  155. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  156. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  157. package/dist/server/serverHelpers/serverUtilities.js +10 -7
  158. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  159. package/dist/server/static.js +57 -18
  160. package/dist/server/static.js.map +1 -1
  161. package/dist/server/storageReclamation.d.ts +17 -0
  162. package/dist/server/storageReclamation.js +90 -6
  163. package/dist/server/storageReclamation.js.map +1 -1
  164. package/dist/server/threads/manageThreads.js +8 -0
  165. package/dist/server/threads/manageThreads.js.map +1 -1
  166. package/dist/server/threads/socketRouter.js +20 -0
  167. package/dist/server/threads/socketRouter.js.map +1 -1
  168. package/dist/server/threads/threadServer.js +12 -0
  169. package/dist/server/threads/threadServer.js.map +1 -1
  170. package/dist/sqlEngine/diff/differential.js +7 -3
  171. package/dist/sqlEngine/diff/differential.js.map +1 -1
  172. package/dist/sqlEngine/parser/normalizer.js +37 -9
  173. package/dist/sqlEngine/parser/normalizer.js.map +1 -1
  174. package/dist/sqlTranslator/index.js +6 -1
  175. package/dist/sqlTranslator/index.js.map +1 -1
  176. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  177. package/dist/upgrade/upgradePrompt.js +22 -3
  178. package/dist/upgrade/upgradePrompt.js.map +1 -1
  179. package/dist/utility/environment/environmentManager.d.ts +11 -0
  180. package/dist/utility/environment/environmentManager.js +154 -0
  181. package/dist/utility/environment/environmentManager.js.map +1 -1
  182. package/dist/utility/errors/commonErrors.d.ts +1 -0
  183. package/dist/utility/errors/commonErrors.js +1 -0
  184. package/dist/utility/errors/commonErrors.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/install/installer.js +32 -0
  189. package/dist/utility/install/installer.js.map +1 -1
  190. package/dist/utility/lmdb/environmentUtility.js +2 -1
  191. package/dist/utility/lmdb/environmentUtility.js.map +1 -1
  192. package/dist/utility/logging/harper_logger.d.ts +7 -0
  193. package/dist/utility/logging/harper_logger.js +57 -24
  194. package/dist/utility/logging/harper_logger.js.map +1 -1
  195. package/npm-shrinkwrap.json +208 -198
  196. package/package.json +10 -8
  197. package/resources/DESIGN.md +1 -0
  198. package/resources/DatabaseTransaction.ts +313 -102
  199. package/resources/ResourceInterface.ts +8 -2
  200. package/resources/Resources.ts +22 -4
  201. package/resources/Table.ts +398 -259
  202. package/resources/analytics/write.ts +22 -20
  203. package/resources/auditStore.ts +32 -1
  204. package/resources/blob.ts +15 -8
  205. package/resources/databases.ts +123 -12
  206. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
  207. package/resources/jsResource.ts +5 -62
  208. package/resources/models/Models.ts +14 -1
  209. package/resources/models/backendRegistry.ts +10 -0
  210. package/resources/models/openaiStream.ts +131 -19
  211. package/resources/models/v1/chatCompletions.ts +128 -0
  212. package/resources/models/v1/embeddings.ts +70 -0
  213. package/resources/models/v1/errors.ts +141 -0
  214. package/resources/models/v1/index.ts +72 -0
  215. package/resources/models/v1/models.ts +53 -0
  216. package/resources/models/v1/translation.ts +362 -0
  217. package/resources/roles.ts +67 -7
  218. package/resources/search.ts +8 -3
  219. package/security/jsLoader.ts +84 -30
  220. package/security/role.ts +7 -0
  221. package/security/superUserGuard.ts +20 -0
  222. package/security/tokenAuthentication.ts +6 -3
  223. package/security/user.ts +26 -1
  224. package/server/DESIGN.md +45 -34
  225. package/server/http.ts +100 -13
  226. package/server/operationsServer.ts +0 -2
  227. package/server/serverHelpers/multipartParser.ts +9 -0
  228. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  229. package/server/serverHelpers/registeredOperations.ts +19 -15
  230. package/server/serverHelpers/serverHandlers.js +20 -3
  231. package/server/serverHelpers/serverUtilities.ts +10 -7
  232. package/server/static.ts +75 -20
  233. package/server/storageReclamation.ts +104 -8
  234. package/server/threads/manageThreads.js +8 -0
  235. package/server/threads/socketRouter.ts +20 -0
  236. package/server/threads/threadServer.js +11 -0
  237. package/sqlTranslator/index.ts +6 -1
  238. package/static/defaultConfig.yaml +2 -0
  239. package/studio/web/assets/{Chat-DoVWScmq.js → Chat-BMUjPklt.js} +5 -5
  240. package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-BMUjPklt.js.map} +1 -1
  241. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-Cbghuxe5.js} +5 -5
  242. package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
  243. package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-CkgTQAqc.js} +2 -2
  244. package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-CkgTQAqc.js.map} +1 -1
  245. package/studio/web/assets/{applications-D03NA7wW.js → applications-WvF4NOTN.js} +13 -13
  246. package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
  247. package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
  248. package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
  249. package/studio/web/assets/index-CDeqVx71.css +1 -0
  250. package/studio/web/assets/index-Sevt3MVx.js +819 -0
  251. package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
  252. package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
  253. package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
  254. package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
  255. package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
  256. package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
  257. package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
  258. package/studio/web/assets/{notifications-0edoFTsb.js → notifications-Ap84YyQp.js} +2 -2
  259. package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-Ap84YyQp.js.map} +1 -1
  260. package/studio/web/assets/notifications-vehqyOLM.js +2 -0
  261. package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-vehqyOLM.js.map} +1 -1
  262. package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
  263. package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
  264. package/studio/web/assets/{profile-DUfEPQtx.js → profile-DzDpRLHY.js} +2 -2
  265. package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-DzDpRLHY.js.map} +1 -1
  266. package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
  267. package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
  268. package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
  269. package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
  270. package/studio/web/assets/register-BDy01-q3.js +3 -0
  271. package/studio/web/assets/register-BDy01-q3.js.map +1 -0
  272. package/studio/web/assets/register-CQHRwNP4.js +3 -0
  273. package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
  274. package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-D0aPcI3W.js} +2 -2
  275. package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
  276. package/studio/web/assets/setup-I15FAF9H.js +3 -0
  277. package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-I15FAF9H.js.map} +1 -1
  278. package/studio/web/assets/status-BF1eJshJ.js +62 -0
  279. package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
  280. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-DwSPXXhl.js} +2 -2
  281. package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
  282. package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
  283. package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
  284. package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-Bu51A-VG.js} +2 -2
  285. package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-Bu51A-VG.js.map} +1 -1
  286. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-DrV1zMrN.js} +2 -2
  287. package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
  288. package/studio/web/index.html +6 -7
  289. package/upgrade/upgradePrompt.ts +22 -3
  290. package/utility/environment/environmentManager.ts +152 -0
  291. package/utility/errors/commonErrors.ts +2 -0
  292. package/utility/hdbTerms.ts +2 -0
  293. package/utility/install/installer.ts +37 -0
  294. package/utility/lmdb/environmentUtility.ts +2 -1
  295. package/utility/logging/harper_logger.ts +57 -26
  296. package/studio/web/assets/applications-D03NA7wW.js.map +0 -1
  297. package/studio/web/assets/index-Bh_CNAHr.js +0 -809
  298. package/studio/web/assets/index-Bh_CNAHr.js.map +0 -1
  299. package/studio/web/assets/index-C8Mv9kHV.css +0 -1
  300. package/studio/web/assets/index.lazy-Dx3MpyDC.js +0 -14
  301. package/studio/web/assets/notifications-CwKhipK7.js +0 -2
  302. package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
  303. package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
  304. package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
  305. package/studio/web/assets/setup-B56Oz1_u.js +0 -3
  306. package/studio/web/assets/status-BAod7p3o.js +0 -62
  307. package/studio/web/assets/status-BAod7p3o.js.map +0 -1
  308. 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
- const udsCleanupPaths: { socketPath: string; yamlPath: string }[] = [];
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
- /** Mark a Unix domain socket's listen() as failed: suppress its metadata and remove any already written. */
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
- try {
88
- unlinkSync(entry.yamlPath);
89
- } catch {}
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 { socketPath, yamlPath } of udsCleanupPaths) {
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
- /** Clean all files in the sockets directory. Call from main thread on process startup. */
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(name: string): ((body: any) => Promise<any>) | undefined {
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
- // bypass_auth is trusted as forwarded: the main thread already strips it from external
208
- // HTTP requests (handlePostRequest, before any dispatch decision), and an internal caller
209
- // legitimately setting it (server.operation(op, context, false)) must see the same
210
- // authorize:false behavior whether the operation happens to run locally or via this bridge.
211
- // ITC is an internal, same-process trust boundary — a worker able to forge this message
212
- // already has direct DB access and gains nothing by spoofing bypass_auth.
213
- const operationFunction = localDispatch.chooseOperation(body);
214
- const result = await localDispatch.processLocalTransaction({ body }, operationFunction);
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
- // Just in case someone tries to bypass auth
149
- if (req.body.bypass_auth) delete req.body.bypass_auth;
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 (!json.bypass_auth) {
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
- !json.bypass_auth &&
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
- operation.bypass_auth = !authorize;
314
- const operation_function = chooseOperation(operation);
315
- return processLocalTransaction({ body: operation }, operation_function);
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 will clear the in-memory maps and allow them to regenerate based on the new configuration (since the default EntryHandler will regenerate anyways).
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
- // If the files option changes, clear the maps and let the entry handler regenerate them
191
- staticFiles.clear();
192
- indexEntries.clear();
193
- scope.logger.info(`Static files reinitialized due to change in ${key.join('.')}`);
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
- // The route mount cannot be changed on a live server registration — restart to apply
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
- indexEntries[entry.eventType === 'addDir' ? 'set' : 'delete'](urlPath, indexPath);
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.set(urlPath, entry.absolutePath);
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.set(urlPath.slice(0, lastSlashIndex), null);
286
+ setOwnedPath(indexEntries, indexEntryOwners, urlPath.slice(0, lastSlashIndex), entry.absolutePath, null);
241
287
  // With trailing slash; serves the index.html file
242
- indexEntries.set(urlPath.slice(0, lastSlashIndex + 1), entry.absolutePath);
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
- // Remove the file from memory when it is deleted
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
- indexEntries.delete(urlPath.slice(0, lastSlashIndex));
252
- indexEntries.delete(urlPath.slice(0, lastSlashIndex + 1));
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
  }