@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/DESIGN.md +421 -0
  5. package/components/OptionsWatcher.ts +368 -102
  6. package/components/Scope.ts +20 -1
  7. package/components/componentLoader.ts +43 -4
  8. package/components/deploymentOperations.ts +4 -1
  9. package/components/deploymentRecorder.ts +9 -2
  10. package/components/mcp/DESIGN.md +109 -0
  11. package/components/mcp/audit.ts +21 -17
  12. package/components/operations.js +284 -52
  13. package/components/operationsValidation.js +49 -2
  14. package/components/packageComponent.ts +25 -1
  15. package/components/requestRestart.ts +11 -0
  16. package/config/DESIGN.md +306 -0
  17. package/config/RootConfigWatcher.ts +191 -37
  18. package/config/configReadRetry.ts +62 -0
  19. package/config/configUtils.ts +78 -26
  20. package/config/parseConfigFile.ts +34 -0
  21. package/config/readConfigFileSync.ts +44 -0
  22. package/config/watcherArming.ts +59 -0
  23. package/config-root.schema.json +4 -0
  24. package/dataLayer/DESIGN.md +179 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  26. package/dataLayer/restoreMarker.ts +92 -25
  27. package/dist/bin/cliOperations.js +13 -0
  28. package/dist/bin/cliOperations.js.map +1 -1
  29. package/dist/bin/restart.js +42 -6
  30. package/dist/bin/restart.js.map +1 -1
  31. package/dist/components/Application.d.ts +104 -9
  32. package/dist/components/Application.js +954 -102
  33. package/dist/components/Application.js.map +1 -1
  34. package/dist/components/OptionsWatcher.d.ts +4 -1
  35. package/dist/components/OptionsWatcher.js +378 -104
  36. package/dist/components/OptionsWatcher.js.map +1 -1
  37. package/dist/components/Scope.js +15 -1
  38. package/dist/components/Scope.js.map +1 -1
  39. package/dist/components/componentLoader.js +35 -3
  40. package/dist/components/componentLoader.js.map +1 -1
  41. package/dist/components/deploymentOperations.js +4 -1
  42. package/dist/components/deploymentOperations.js.map +1 -1
  43. package/dist/components/deploymentRecorder.d.ts +4 -2
  44. package/dist/components/deploymentRecorder.js +1 -0
  45. package/dist/components/deploymentRecorder.js.map +1 -1
  46. package/dist/components/mcp/audit.d.ts +2 -1
  47. package/dist/components/mcp/audit.js +21 -17
  48. package/dist/components/mcp/audit.js.map +1 -1
  49. package/dist/components/operations.d.ts +28 -0
  50. package/dist/components/operations.js +263 -54
  51. package/dist/components/operations.js.map +1 -1
  52. package/dist/components/operationsValidation.js +48 -2
  53. package/dist/components/operationsValidation.js.map +1 -1
  54. package/dist/components/packageComponent.js +24 -0
  55. package/dist/components/packageComponent.js.map +1 -1
  56. package/dist/components/requestRestart.d.ts +1 -0
  57. package/dist/components/requestRestart.js +7 -0
  58. package/dist/components/requestRestart.js.map +1 -1
  59. package/dist/config/RootConfigWatcher.d.ts +2 -0
  60. package/dist/config/RootConfigWatcher.js +189 -35
  61. package/dist/config/RootConfigWatcher.js.map +1 -1
  62. package/dist/config/configReadRetry.d.ts +8 -0
  63. package/dist/config/configReadRetry.js +62 -0
  64. package/dist/config/configReadRetry.js.map +1 -0
  65. package/dist/config/configUtils.d.ts +10 -9
  66. package/dist/config/configUtils.js +63 -27
  67. package/dist/config/configUtils.js.map +1 -1
  68. package/dist/config/parseConfigFile.d.ts +4 -0
  69. package/dist/config/parseConfigFile.js +35 -0
  70. package/dist/config/parseConfigFile.js.map +1 -0
  71. package/dist/config/readConfigFileSync.d.ts +1 -0
  72. package/dist/config/readConfigFileSync.js +47 -0
  73. package/dist/config/readConfigFileSync.js.map +1 -0
  74. package/dist/config/watcherArming.d.ts +15 -0
  75. package/dist/config/watcherArming.js +59 -0
  76. package/dist/config/watcherArming.js.map +1 -0
  77. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  78. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  79. package/dist/dataLayer/restoreMarker.d.ts +21 -8
  80. package/dist/dataLayer/restoreMarker.js +94 -27
  81. package/dist/dataLayer/restoreMarker.js.map +1 -1
  82. package/dist/index.d.ts +2 -0
  83. package/dist/index.js +9 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/json/systemSchema.json +3 -0
  86. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  87. package/dist/resources/DatabaseTransaction.js +224 -18
  88. package/dist/resources/DatabaseTransaction.js.map +1 -1
  89. package/dist/resources/LMDBTransaction.d.ts +2 -1
  90. package/dist/resources/LMDBTransaction.js +22 -3
  91. package/dist/resources/LMDBTransaction.js.map +1 -1
  92. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  93. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  94. package/dist/resources/RecordEncoder.d.ts +1 -1
  95. package/dist/resources/RecordEncoder.js +46 -10
  96. package/dist/resources/RecordEncoder.js.map +1 -1
  97. package/dist/resources/Resource.js +97 -13
  98. package/dist/resources/Resource.js.map +1 -1
  99. package/dist/resources/ResourceInterface.d.ts +8 -0
  100. package/dist/resources/RocksIndexStore.js +2 -1
  101. package/dist/resources/RocksIndexStore.js.map +1 -1
  102. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  103. package/dist/resources/RocksTransactionLogStore.js +104 -33
  104. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  105. package/dist/resources/Table.d.ts +58 -7
  106. package/dist/resources/Table.js +1143 -348
  107. package/dist/resources/Table.js.map +1 -1
  108. package/dist/resources/analytics/write.d.ts +3 -0
  109. package/dist/resources/analytics/write.js +49 -16
  110. package/dist/resources/analytics/write.js.map +1 -1
  111. package/dist/resources/auditStore.d.ts +170 -0
  112. package/dist/resources/auditStore.js +457 -11
  113. package/dist/resources/auditStore.js.map +1 -1
  114. package/dist/resources/crdt.d.ts +10 -0
  115. package/dist/resources/crdt.js +22 -0
  116. package/dist/resources/crdt.js.map +1 -1
  117. package/dist/resources/dataLoader.js +3 -4
  118. package/dist/resources/dataLoader.js.map +1 -1
  119. package/dist/resources/databases.d.ts +16 -13
  120. package/dist/resources/databases.js +625 -177
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  123. package/dist/resources/derivedIndexRegistry.js +68 -0
  124. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  125. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  126. package/dist/resources/derivedIndexRuntime.js +2027 -0
  127. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  128. package/dist/resources/graphql.js +3 -2
  129. package/dist/resources/graphql.js.map +1 -1
  130. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  131. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +867 -39
  132. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  133. package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
  134. package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
  135. package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
  136. package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
  137. package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
  138. package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
  139. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  140. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  141. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  142. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  143. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  144. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  145. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
  146. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
  147. package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
  148. package/dist/resources/nodeIdMapping.d.ts +5 -0
  149. package/dist/resources/nodeIdMapping.js +49 -0
  150. package/dist/resources/nodeIdMapping.js.map +1 -1
  151. package/dist/resources/recordLock.d.ts +47 -4
  152. package/dist/resources/recordLock.js +138 -7
  153. package/dist/resources/recordLock.js.map +1 -1
  154. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  155. package/dist/resources/recordLockCoordinator.js +2597 -0
  156. package/dist/resources/recordLockCoordinator.js.map +1 -0
  157. package/dist/resources/replayLogs.js +5 -0
  158. package/dist/resources/replayLogs.js.map +1 -1
  159. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  160. package/dist/resources/replicatedApplyFailure.js +63 -0
  161. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  162. package/dist/resources/scheduler/scheduler.js +3 -3
  163. package/dist/resources/scheduler/scheduler.js.map +1 -1
  164. package/dist/resources/search.d.ts +10 -4
  165. package/dist/resources/search.js +160 -40
  166. package/dist/resources/search.js.map +1 -1
  167. package/dist/resources/tracked.d.ts +5 -1
  168. package/dist/resources/tracked.js +74 -23
  169. package/dist/resources/tracked.js.map +1 -1
  170. package/dist/resources/transactionBroadcast.js +4 -6
  171. package/dist/resources/transactionBroadcast.js.map +1 -1
  172. package/dist/security/auth.js +59 -23
  173. package/dist/security/auth.js.map +1 -1
  174. package/dist/security/deferredAuthentication.d.ts +11 -0
  175. package/dist/security/deferredAuthentication.js +25 -3
  176. package/dist/security/deferredAuthentication.js.map +1 -1
  177. package/dist/security/jsLoader.js +9 -4
  178. package/dist/security/jsLoader.js.map +1 -1
  179. package/dist/server/REST.js +39 -5
  180. package/dist/server/REST.js.map +1 -1
  181. package/dist/server/http.d.ts +5 -1
  182. package/dist/server/http.js +34 -2
  183. package/dist/server/http.js.map +1 -1
  184. package/dist/server/mqtt.js +5 -1
  185. package/dist/server/mqtt.js.map +1 -1
  186. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  187. package/dist/server/serverHelpers/Headers.js +6 -0
  188. package/dist/server/serverHelpers/Headers.js.map +1 -1
  189. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  190. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  191. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  192. package/dist/server/serverHelpers/Request.d.ts +5 -10
  193. package/dist/server/serverHelpers/Request.js +38 -136
  194. package/dist/server/serverHelpers/Request.js.map +1 -1
  195. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  196. package/dist/server/serverHelpers/contentTypes.js +189 -15
  197. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  198. package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
  199. package/dist/server/serverHelpers/serverUtilities.js +110 -20
  200. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  201. package/dist/server/serverHelpers/uwsServer.js +4 -1
  202. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  203. package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
  204. package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
  205. package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
  206. package/dist/server/storageReclamation.js +1 -1
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  209. package/dist/server/threads/isolatedApplications.js +171 -0
  210. package/dist/server/threads/isolatedApplications.js.map +1 -0
  211. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  212. package/dist/server/threads/logRotationTransport.js +33 -0
  213. package/dist/server/threads/logRotationTransport.js.map +1 -0
  214. package/dist/server/threads/manageThreads.d.ts +64 -6
  215. package/dist/server/threads/manageThreads.js +261 -12
  216. package/dist/server/threads/manageThreads.js.map +1 -1
  217. package/dist/server/threads/socketRouter.d.ts +1 -0
  218. package/dist/server/threads/socketRouter.js +196 -13
  219. package/dist/server/threads/socketRouter.js.map +1 -1
  220. package/dist/server/threads/threadServer.js +30 -7
  221. package/dist/server/threads/threadServer.js.map +1 -1
  222. package/dist/utility/errors/hdbError.d.ts +24 -0
  223. package/dist/utility/errors/hdbError.js +58 -1
  224. package/dist/utility/errors/hdbError.js.map +1 -1
  225. package/dist/utility/hdbTerms.d.ts +2 -0
  226. package/dist/utility/hdbTerms.js +2 -0
  227. package/dist/utility/hdbTerms.js.map +1 -1
  228. package/dist/utility/logging/harper_logger.js +218 -38
  229. package/dist/utility/logging/harper_logger.js.map +1 -1
  230. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  231. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  232. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  233. package/dist/utility/logging/logRotation.d.ts +46 -0
  234. package/dist/utility/logging/logRotation.js +365 -0
  235. package/dist/utility/logging/logRotation.js.map +1 -0
  236. package/dist/utility/logging/logRotator.d.ts +1 -1
  237. package/dist/utility/logging/logRotator.js +172 -92
  238. package/dist/utility/logging/logRotator.js.map +1 -1
  239. package/dist/utility/npmUtilities.js +6 -4
  240. package/dist/utility/npmUtilities.js.map +1 -1
  241. package/dist/utility/watcherFallback.d.ts +0 -45
  242. package/dist/utility/watcherFallback.js +1 -125
  243. package/dist/utility/watcherFallback.js.map +1 -1
  244. package/dist/validation/configValidator.js +6 -3
  245. package/dist/validation/configValidator.js.map +1 -1
  246. package/index.ts +9 -0
  247. package/json/systemSchema.json +3 -0
  248. package/npm-shrinkwrap.json +177 -87
  249. package/package.json +13 -5
  250. package/resources/DESIGN.md +689 -19
  251. package/resources/DatabaseTransaction.ts +230 -17
  252. package/resources/LMDBTransaction.ts +21 -3
  253. package/resources/PrimaryRocksDatabase.ts +20 -7
  254. package/resources/RecordEncoder.ts +52 -10
  255. package/resources/Resource.ts +97 -13
  256. package/resources/ResourceInterface.ts +8 -0
  257. package/resources/RocksIndexStore.ts +2 -1
  258. package/resources/RocksTransactionLogStore.ts +111 -31
  259. package/resources/Table.ts +1273 -396
  260. package/resources/analytics/DESIGN.md +38 -0
  261. package/resources/analytics/write.ts +50 -17
  262. package/resources/auditStore.ts +460 -11
  263. package/resources/crdt.ts +22 -0
  264. package/resources/dataLoader.ts +3 -4
  265. package/resources/databases.ts +611 -147
  266. package/resources/derivedIndexRegistry.ts +56 -0
  267. package/resources/derivedIndexRuntime.ts +2292 -0
  268. package/resources/graphql.ts +3 -2
  269. package/resources/indexes/DESIGN.md +833 -0
  270. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +924 -46
  271. package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
  272. package/resources/indexes/fullTextNativeBinding.ts +146 -0
  273. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  274. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  275. package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
  276. package/resources/nodeIdMapping.ts +50 -0
  277. package/resources/record-locks.md +1407 -0
  278. package/resources/recordLock.ts +173 -7
  279. package/resources/recordLockCoordinator.ts +3082 -0
  280. package/resources/replayLogs.ts +5 -0
  281. package/resources/replicatedApplyFailure.ts +77 -0
  282. package/resources/scheduler/DESIGN.md +40 -0
  283. package/resources/scheduler/scheduler.ts +4 -4
  284. package/resources/search.ts +169 -49
  285. package/resources/tracked.ts +73 -22
  286. package/resources/transactionBroadcast.ts +4 -4
  287. package/security/DESIGN.md +175 -0
  288. package/security/auth.ts +53 -24
  289. package/security/deferredAuthentication.ts +24 -2
  290. package/security/jsLoader.ts +9 -4
  291. package/server/DESIGN.md +275 -0
  292. package/server/REST.ts +42 -6
  293. package/server/http.ts +34 -2
  294. package/server/mqtt.ts +6 -4
  295. package/server/serverHelpers/Headers.ts +5 -1
  296. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  297. package/server/serverHelpers/Request.ts +33 -131
  298. package/server/serverHelpers/contentTypes.ts +188 -15
  299. package/server/serverHelpers/serverUtilities.ts +157 -27
  300. package/server/serverHelpers/uwsServer.ts +4 -1
  301. package/server/serverHelpers/webSocketCloseReason.ts +25 -0
  302. package/server/storageReclamation.ts +2 -2
  303. package/server/threads/isolatedApplications.ts +157 -0
  304. package/server/threads/logRotationTransport.ts +40 -0
  305. package/server/threads/manageThreads.js +254 -12
  306. package/server/threads/socketRouter.ts +217 -11
  307. package/server/threads/threadServer.js +30 -7
  308. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-DADFFGe_.js} +1 -1
  309. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-D_mI-rZ7.js} +3 -3
  310. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-c2NiSDHa.js} +1 -1
  311. package/studio/web/assets/{applications-DHxGi7JH.js → applications-DktUqh7G.js} +1 -1
  312. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-Cs_75Xhw.js} +1 -1
  313. package/studio/web/assets/{editor-DNcRHK54.js → editor-19b-Y1IN.js} +1 -1
  314. package/studio/web/assets/{html-Bdssedlg.js → html-DiYEQMpB.js} +1 -1
  315. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CmR0y7P_.js} +1 -1
  316. package/studio/web/assets/{index-D6sxmFLR.js → index-Dm0rfkJ7.js} +5 -5
  317. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-7vqt2CC3.js} +1 -1
  318. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-BWtCFuOt.js} +1 -1
  319. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-Buzzbv9y.js} +1 -1
  320. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-SqsFWfTM.js} +1 -1
  321. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
  322. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-CAB-LZWT.js} +1 -1
  323. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-DRzmSRxM.js} +1 -1
  324. package/studio/web/assets/{profile-DdOwtntb.js → profile-BNKAl79n.js} +1 -1
  325. package/studio/web/assets/{regions-n69fwagr.js → regions-CUow_Zw2.js} +1 -1
  326. package/studio/web/assets/{register-PfWTCXWB.js → register-Dkt3WUMp.js} +2 -2
  327. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BZRfMD0N.js} +1 -1
  328. package/studio/web/assets/{setup-CUx_aUDl.js → setup-D_yiEPO2.js} +2 -2
  329. package/studio/web/assets/{status-D7BVKqX9.js → status-DhHh1Ge-.js} +1 -1
  330. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-D7L1PDtV.js} +1 -1
  331. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-CCwLk1YS.js} +1 -1
  332. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-BP1j1mjn.js} +1 -1
  333. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-D7bnYxLw.js} +1 -1
  334. package/studio/web/assets/{workers-pR3jRY9D.js → workers-tOuCNT17.js} +1 -1
  335. package/studio/web/assets/{xml-2iRnMhQO.js → xml-BSG_3mQT.js} +1 -1
  336. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-DxiLprBB.js} +1 -1
  337. package/studio/web/index.html +1 -1
  338. package/utility/DESIGN.md +55 -0
  339. package/utility/errors/hdbError.ts +54 -0
  340. package/utility/hdbTerms.ts +2 -0
  341. package/utility/logging/harper_logger.ts +210 -30
  342. package/utility/logging/logGenerationCoordinator.ts +196 -0
  343. package/utility/logging/logRotation.ts +367 -0
  344. package/utility/logging/logRotator.ts +196 -91
  345. package/utility/npmUtilities.ts +6 -4
  346. package/utility/watcherFallback.ts +0 -122
  347. package/validation/configValidator.ts +6 -3
@@ -192,7 +192,7 @@ function notifyFromTransactionData(subscriptions, auditLogIterable?, allowYield
192
192
  let ancestorLevel = 0;
193
193
  do {
194
194
  // we iterate through the key hierarchy, notifying all subscribers for each key,
195
- // so for an id like resource/foo/bar, we notify subscribers for resource/foo/bar, resource/foo/, resource/foo, resource/, and resource
195
+ // so for an id like resource/foo/bar, we notify subscribers for resource/foo/bar, resource/foo/, resource/, and the root (null)
196
196
  // this allows for efficient subscriptions to children ids/topics
197
197
  const keySubscriptions = tableSubscriptions.get(matchingKey);
198
198
  if (keySubscriptions) {
@@ -230,9 +230,9 @@ function notifyFromTransactionData(subscriptions, auditLogIterable?, allowYield
230
230
  if (lastSlash !== matchingKey.length - 1) {
231
231
  ancestorLevel++; // don't increase the ancestor level for this going from resource/ to resource
232
232
  }
233
- if (lastSlash > -1) {
234
- matchingKey = matchingKey.slice(0, lastSlash + 1);
235
- } else matchingKey = null;
233
+ // lastIndexOf clamps a negative fromIndex to 0, so '/' would otherwise yield itself forever
234
+ const parentKey = lastSlash > -1 ? matchingKey.slice(0, lastSlash + 1) : null;
235
+ matchingKey = parentKey === matchingKey ? null : parentKey;
236
236
  } while (true);
237
237
  }
238
238
  } else if (auditRecord.type === 'reload') {
@@ -0,0 +1,175 @@
1
+ # security/ — Design notes
2
+
3
+ Authentication tokens, OIDC exchange and TLS material.
4
+
5
+ **Read this when:** touching `tokenAuthentication.ts`, `impersonation.ts`, `authn/oidc/` or `keys.ts`.
6
+
7
+ Index of every design note: [DESIGN.md](../DESIGN.md).
8
+
9
+ ---
10
+
11
+ ## OIDC trusted publishing (`security/authn/oidc/`)
12
+
13
+ `exchange_oidc_token` lets a workload authenticate with no stored Harper credential (#2171): it presents an identity token minted by its runtime, and gets back a one-hour operation token for the user a stored trust policy names. It is in `NO_AUTH_OPERATIONS` because it _is_ the authentication, the same way `create_authentication_tokens` is against a password — the same three wiring points apply (`serverHandlers.js` `NO_AUTH_OPERATIONS`, the `verifyPerms` bypass in `serverUtilities.ts`, and a `permission(false, [])` registration).
14
+
15
+ **The core is issuer-agnostic; everything issuer-specific lives in `providers/`.** That split is the point of the layout, not an accident of it — a new workload-identity issuer should be a profile, not a change to verification, matching, or storage.
16
+
17
+ - `claims.ts` — matching and constraint _shape_ validation. Knows nothing about any issuer.
18
+ - `jwks.ts` — issuer keys. The rate-limit clock for unknown-`kid` refetches lives _outside_ the cache entry: a successful fetch replaces the entry, and a rate limit that resets whenever it fires is not a rate limit. Keeping it separate also means a genuine key rotation is picked up on first use rather than after the window.
19
+ - `identityToken.ts` — signature, issuer, audience, `exp`, and a bounded lifetime. Owns `rejectToken`, shared with the exchange so both halves refuse identically.
20
+ - `tokenExchange.ts` — policy selection, replay, minting, audit. Verification is memoized per audience, so N policies sharing one cost one signature check.
21
+ - `providers/` — `assertPolicyIsSpecific` / `assertAudienceIsSpecific` / `normalizeClaims` / `describePrincipal` / optional `vetoClaims`, resolved by normalized issuer.
22
+
23
+ **An unregistered issuer gets `providers/generic.ts`, which is strict rather than permissive:** the policy must pin `sub`. That is what makes Kubernetes service accounts, GCP service accounts, and SPIFFE SVIDs work with zero provider code — each has a stable canonical subject. GitHub needs its own profile precisely because its `sub` is the one claim you should _not_ pin: it varies by trigger, and its format changed for repositories created after 2026-07-15.
24
+
25
+ Four constraints that look like choices but are not:
26
+
27
+ 1. **Every rejection returns the same message.** The endpoint is unauthenticated; a caller told which check failed can enumerate a policy one claim at a time. Reasons go to the `oidc-trust` logger.
28
+ 2. **A GitHub policy must gate the ref.** `githubActionsProfile.assertPolicyIsSpecific` rejects a policy pinning only repository + workflow, because anyone who can push a branch could then add that workflow to it and mint a token. Stricter than npm's trusted-publishing model, which mitigates the same hole with environment protection instead — and profile-scoped, so it never constrains another issuer.
29
+ 3. **`createOperationToken`, not `createTokens`.** `createTokens` overwrites `hdb_user.refresh_token` as a side effect, so minting for CI would silently revoke whatever credential that user already held (#2018) — the exact problem this feature removes.
30
+ 4. **The role is the boundary; the per-policy `operations` allowlist only narrows it.** Least privilege is primarily the role of the user the policy names. A policy may _optionally_ carry an `operations` scope, which can only subtract from that role — never add to it. It is deliberately not merged into `permission.operations`: gate 2 in `operation_authorization.ts` treats an explicit listing of an SU-only operation as a deliberate grant, so reusing that field would _widen_ where this must only narrow. The scope is carried as a separate `tokenOperations` claim and intersected ahead of every early return, including the super_user bypass.
31
+
32
+ Its enforcement surface is the operations API and SQL (`verifyPerms` / `verifyPermsAST`) — **not** the application REST/GraphQL resource path, which authorizes through table-level `checkPermission` and does not consult the scope. A scoped token therefore still carries its role's full CRUD there, which is why the role has to be least-privilege on its own; the scope is defense in depth, not a substitute. Closing that gap is a follow-up on the same surface as CORE-3061. Because a second authorization mechanism beside roles is one more place for the two to disagree, whether to keep this at all is an open design question on #2173 rather than a settled constraint.
33
+
34
+ Naming `sql` in a scope grants the SQL interface, not unrestricted DML through it: a write statement additionally requires its matching data operation (`insert`/`update`/`delete`) in scope. That is what keeps `read_only` — which expands to include `sql` — from admitting a DELETE, given that `verifyPermsAST` returns early for a super_user before any table check runs.
35
+
36
+ `hdb_oidc_token_use` (created lazily via `table()`, not the system schema) records spent tokens keyed on a SHA-256 of the token itself, with `expiresAt` past the token's own expiry. Hashed rather than stored, so the table never holds a credential; keyed on the token's **signed input** (`header.payload`) rather than `jti` because not every issuer emits one (Azure uses `uti`). Not on the whole token string: the signature segment is covered by nothing, and base64url decoding ignores the surplus low bits of its final character, so 16 distinct spellings of an RS256 signature decode to the same bytes, all verify, and all hash differently — one leaked token would buy 16 exchanges. ES\* malleability (`s → n−s`) is a second such vector. The signed input is exactly what the issuer asserted, so every variant collapses to one fingerprint. The get-then-put is not atomic and does not claim to be: a concurrent replay is not a privilege escalation, since whoever holds the token could obtain one operation token anyway.
37
+
38
+ ## Scoped tokens and synthetic-role identity (`security/tokenAuthentication.ts`, `security/impersonation.ts`)
39
+
40
+ `create_authentication_tokens` with an inline `role` **object** mints a `sub: 'scoped-operation'`
41
+ JWT that embeds its whole (downgraded, deep-validated) permission set; the bearer needs no
42
+ `hdb_user`/`hdb_role` row and the `username` is attribution only. Minting is super_user-gated
43
+ (or trusted internal dispatch via `isOperationAuthorizationBypassed()`); a string `role` keeps its
44
+ legacy meaning (component-defined token, rejected by `validateOperationToken`). Scoped tokens get
45
+ no refresh token, touch no user record, and are therefore **irrevocable until expiry** — expiry is
46
+ the only control, which is why `auth.ts` evicts cached Bearer identities at exact `authExpiresAt`
47
+ rather than waiting for the auth-cache TTL.
48
+
49
+ The attribution `username` must NOT name an existing `hdb_user` (rejected at mint; the default is
50
+ `scoped:<minter>`): code paths that rehydrate a user by name would otherwise substitute the real
51
+ principal's permissions for the token's — or fail-closed on the non-existent name. The three known
52
+ by-name sites are handled, all by the same `_scopedToken` short-circuit: the MQTT last-will replay
53
+ (`DurableSubscriptionsSession.ts` persists the scoped role/marker/expiry on the will and skips
54
+ rehydration — and both the restart-replay and the live abnormal-disconnect paths refuse to publish
55
+ a scoped will past `authExpiresAt`), the live-subscription stale-auth recheck (`Resource.ts`
56
+ `registerLiveSubscriptionForContext` keeps the embedded role as the identity), and the MCP
57
+ `list_changed` session refresh (`components/mcp/listChanged.ts` `refreshSessionUser`). The scoped
58
+ principal also cannot self-mint standing tokens: the passwordless path of `createTokens` rejects an
59
+ `hdb_user._scopedToken` requester. **Any future by-name rehydration must check `_scopedToken`.** A
60
+ user _created after minting_ with a colliding name is therefore inert at every current site; the
61
+ residual is only some _new_ unguarded by-name site — another reason to prefer short expiries.
62
+
63
+ Scope of the `operations` allowlist: it gates the **operations API** (including the `sql` path,
64
+ which never reaches `verifyPerms` and calls `verifyOperationsAllowlist` directly from
65
+ `chooseOperation`) — it does NOT gate the application/REST/GraphQL/MQTT surfaces, which authorize
66
+ on translated table CRUD permissions only. A scoped token intended to be read-only on app
67
+ endpoints must carry restrictive table permissions; `operations: ['read_only']` alone does not
68
+ constrain REST writes if table perms allow them.
69
+
70
+ The invariant to preserve when touching any synthetic (inline/impersonated/scoped) role:
71
+ `permissionsTranslator.getRolePermissions` memoizes translated permissions **by role name** (keyed
72
+ further by `__updatedtime__` + schema). A synthetic role must therefore never carry a constant
73
+ name or a per-request timestamp — two different permission sets would alias one cache slot (a
74
+ same-millisecond `Date.now()` was enough), leaking one principal's translated permissions to
75
+ another. `syntheticRoleName()` derives the name from a hash of the post-downgrade permission
76
+ content with `__updatedtime__: 0`, so identical sets share a slot and distinct sets can't collide;
77
+ `applyImpersonation` re-keys all three impersonation modes the same way (Mode B/C previously wrote
78
+ downgraded copies under the _persisted_ role's name). Synthetic translations live in a separate
79
+ 256-entry LRU (`syntheticRolePermsMap`), not the permanent `rolePermsMap` — so >256 concurrently
80
+ live distinct permission sets degrade to per-request translation (a deliberate cliff; raise the
81
+ constant if a legitimate workload hits it). The `_` name prefix is the discriminator; a persisted
82
+ role named with a leading underscore lands in the LRU too (correct, just evictable). Relatedly,
83
+ the role `operations` allowlist gate in `verifyPerms` must stay **ahead of** the ambient privilege
84
+ early-returns (super_user, structure_user, system-table allowances): persisted roles can't combine
85
+ `super_user` with other permission keys, but inline roles can combine `structure_user` with an
86
+ allowlist, and the gate ordering is what keeps unlisted schema ops unreachable.
87
+
88
+ ## TLS hot-reload: cert vs. private key follow two different propagation paths (`security/keys.ts`)
89
+
90
+ A renewed **certificate** and a renewed **private key** reach a worker's live TLS secure context
91
+ by completely separate routes, and the two must reconverge or HTTPS breaks on that worker.
92
+ Certificates propagate through data: only the main thread watches the cert file (`isMainThread`
93
+ guard in `loadCertificates`) and writes the new PEM into the `system.hdb_certificate` table; every
94
+ worker is subscribed to that table and rebuilds its secure contexts (`updateTLS` inside
95
+ `createTLSSelector`) on the notification. Private keys never touch the table — each worker watches
96
+ its own key file (the private-key `loadAndWatch` has no `isMainThread` guard) and loads the PEM
97
+ straight into its in-thread `privateKeys` map. `getPrivateKeyByName` reads that map first, so an
98
+ already-built secure context has the key bytes baked in (`setCert`/`setKey` at build time); a later
99
+ map update does not touch contexts already built.
100
+
101
+ The hazard when a rotation changes **both**: the cert can win the race to a worker (table write +
102
+ subscription) and trigger `updateTLS` before that worker has reloaded the matching key, producing a
103
+ context that pairs the new cert with the old key — every handshake on it then fails, and nothing
104
+ rebuilds it until the _next_ cert-table change. The fix: a private-key reload (`handlePrivateKeyReload`,
105
+ the single sink for both the chokidar watcher and PR #1394's periodic poll) triggers a debounced
106
+ rebuild of every live selector via the module-level `liveTLSRebuilders` set, so the worker reconverges
107
+ on its own. Subtleties to preserve: the rotation guard (`previous !== undefined && previous !== key`)
108
+ must skip both the initial load and identical-content reloads or watchers thrash; transient one-shot
109
+ selectors (`getReplicationCert`) pass `liveReload=false` so they don't accumulate in the never-pruned
110
+ set; and the cert subscription shares the same debounced `scheduleRebuild` (same 1500ms cadence), so
111
+ its coalescing must stay a superset-safe no-op for the single-swap #586 case. Regression coverage:
112
+ `integrationTests/security/cert-key-reload.test.ts` deterministically pins the cert-before-key ordering
113
+ (it fails by design without the rebuild trigger); `cert-reload.test.ts` guards the cert-only #586 path.
114
+
115
+ **Publication is transactional (#2382).** `updateTLS` builds the entire replacement state —
116
+ hostname→context map, CA map, and default candidate — into pass-local candidates and reconciles the
117
+ live maps in place only after the pass completes (their identity is load-bearing:
118
+ `server.secureContexts` and each context's `availableCAs` alias them). A record that is still in the
119
+ table but fails to build (`ERR_OSSL_X509_KEY_VALUES_MISMATCH` when the table's cert outruns the
120
+ on-disk key, a missing key on this thread) keeps every live entry it owns _and_ its default
121
+ candidacy — a record can be serving as the default with no hostname entries at all — so a transient
122
+ mismatch never downgrades serving below last-good (the pre-fix behavior served the self-signed
123
+ default for days). Retention is trust-aware: a context froze its `ca:` list at build time, so when
124
+ the CA set has changed since, the retained pair is rebuilt against the current trust material —
125
+ new handshakes never see revoked client-CA trust; established sessions and outstanding session
126
+ tickets are unaffected, exactly as on a fresh build (ticket keys are process-wide and never rotate
127
+ on trust changes) — and if that rebuild fails the record's entries
128
+ drop for that pass, except when nothing else is servable: the zero-certificate guard then retains
129
+ the old state (availability outranks the drop in that corner) while the failure keeps retrying. Deleting the record remains the way to drop its contexts; a corrupt authority
130
+ row is a pass failure like any other (reported through the signature throttle, armed for retry) and
131
+ its trust drops until it heals. A failed pass arms a
132
+ self-retry on the shared debounce with a per-signature backoff (1.5s doubling to 5min) and
133
+ signature-throttled logging; external triggers (table subscription, key reload) stay at the plain
134
+ debounce. `loadAndWatch` latches its mtime before the callback for chokidar/poll dedupe, but rolls
135
+ the latch back on a synchronous throw or a rejected callback promise (equality-guarded so a stale
136
+ rejection cannot unlatch a newer reload) — the latch means "last successfully applied", so the
137
+ periodic poll can heal a lost `hdb_certificate` write instead of deduplicating it forever.
138
+
139
+ ## A component-facing export needs BOTH `index.ts` and `getHarperExports` (`security/jsLoader.ts`, `index.ts`)
140
+
141
+ Adding `export { x } from './…'` to `index.ts` publishes `x` on the `harper` **package** but does not
142
+ make `import { x } from 'harper'` work inside an application. A component loaded into a VM compartment
143
+ resolves `harper` to a synthetic module built from `getHarperExports()` — a hand-maintained object
144
+ literal, not a re-export of `index.ts` — so a value added to only one list fails at component load with
145
+ `The requested module 'harper' does not provide an export named 'x'`. No unit test sees that, because
146
+ unit tests `require('#src/…')` directly; only a fixture that imports from `'harper'` does
147
+ (`integrationTests/security/fixtures/deferred-credential-rejection/resources.js`, harper#2703).
148
+
149
+ Export the value from the module that _defines_ it rather than re-exporting it through an intermediate,
150
+ so a component-created value shares that module's private symbols. `markCredentialRejection` depends on
151
+ this: its tag is a module-private `Symbol` that `isCredentialRejection` checks by identity.
152
+
153
+ ## Authentication converts every principal-resolution failure into a decision (`security/auth.ts`)
154
+
155
+ `authentication()` resolves a principal from three sources — an mTLS certificate CN, the `Authorization`
156
+ header, and the `hdb_session` cookie — each through the overridable `server.getUser`. A failure from any
157
+ of them must become a **decision**, never a throw: an internal fault (and anything on the operations API,
158
+ where Harper owns every route) is answered in place, and a tagged credential rejection is deferred so the
159
+ layer owning the route settles it. A throw that escapes instead unwinds the whole middleware chain to
160
+ `server/http.ts`'s terminal handler and renders as a plain-text body, which is what harper#2703 reported.
161
+ `settleAuthFailure()` performs the conversion; `rejectAuthenticationInPlace()` is the only way to answer
162
+ in place, and a new principal-resolving path must route through them.
163
+
164
+ Two consequences that are easy to miss:
165
+
166
+ - **A returned 401 is invisible to a WebSocket or MQTT upgrade.** `server/REST.ts` and `server/mqtt.ts`
167
+ only `await chainCompletion` and discard its resolved value, so an in-place decision has to be recorded
168
+ on the request (`markAuthenticationRejectedInPlace`) for `assertNoDeferredCredentialRejection` — which
169
+ both already call — to fail the upgrade closed. Converting a throw into a returned descriptor without
170
+ that record turns a fail-closed upgrade into one that proceeds with no principal. The certificate
171
+ revocation exit is the case that was missed first.
172
+ - **A deferred rejection outranks a later success.** Route owners call
173
+ `settleDeferredCredentialRejection` _before_ they read `request.user`, so once a credential is deferred,
174
+ resolving a principal from a different credential is a contradiction. Hence a rejected certificate
175
+ identity stops resolution outright instead of falling through to Basic, the session, or the local bypass.
package/security/auth.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  deferCredentialRejection,
19
19
  getDeferredCredentialRejection,
20
20
  isCredentialRejection,
21
+ markAuthenticationRejectedInPlace,
21
22
  } from './deferredAuthentication.ts';
22
23
  import { serializeMessage } from '../server/serverHelpers/contentTypes.ts';
23
24
  import { hdbErrors } from '../utility/errors/hdbError.ts';
@@ -69,6 +70,27 @@ export function bypassAuth() {
69
70
  bypassUser = { username: 'bypass', role: { role: 'super_user', permission: { super_user: true } } };
70
71
  }
71
72
 
73
+ /**
74
+ * Turns a failed principal resolution into a decision, so it never escapes the chain as a throw.
75
+ * Returns a response descriptor for the caller to pass through `applyResponseHeaders`, or
76
+ * `undefined` once the rejection has been deferred for the route owner to settle (#2703).
77
+ */
78
+ function settleAuthFailure(request, error, strategy: string): { status: number; body: any } | undefined {
79
+ const internalFault = !isCredentialRejection(error);
80
+ if (request.isOperationsServer || internalFault) {
81
+ if (internalFault) authLogger.error('Authentication failed internally', errorForLog(error));
82
+ const message = internalFault ? AUTHENTICATION_ERROR_MSGS.GENERIC_AUTH_FAIL : error.message;
83
+ return rejectAuthenticationInPlace(request, message);
84
+ }
85
+ deferCredentialRejection(request, error, strategy);
86
+ return undefined;
87
+ }
88
+
89
+ function rejectAuthenticationInPlace(request, message: string): { status: number; body: any } {
90
+ markAuthenticationRejectedInPlace(request, 401, message);
91
+ return { status: 401, body: serializeMessage({ error: message }, request) };
92
+ }
93
+
72
94
  // TODO: Make this not return a promise if it can be fulfilled synchronously (from cache)
73
95
  export async function authentication(request, nextHandler) {
74
96
  const headers = request.headers.asObject; // we cheat and use the node headers object since it is a little faster
@@ -156,6 +178,7 @@ export async function authentication(request, nextHandler) {
156
178
  )
157
179
  authEventLog.error?.('Authorization error:', request._nodeRequest.socket.authorizationError);
158
180
 
181
+ let mtlsCredentialDeferred = false;
159
182
  if (request.mtlsConfig && request.authorized && request.peerCertificate.subject) {
160
183
  const verificationResult = await verifyCertificate(request.peerCertificate, request.mtlsConfig);
161
184
  if (!verificationResult.valid) {
@@ -165,10 +188,7 @@ export async function authentication(request, nextHandler) {
165
188
  'for',
166
189
  request.peerCertificate.subject.CN
167
190
  );
168
- return applyResponseHeaders({
169
- status: 401,
170
- body: serializeMessage({ error: 'Certificate revoked or verification failed' }, request),
171
- });
191
+ return applyResponseHeaders(rejectAuthenticationInPlace(request, 'Certificate revoked or verification failed'));
172
192
  }
173
193
 
174
194
  // Alternative behavior: Instead of returning 401 above, we could just not set the user
@@ -183,8 +203,19 @@ export async function authentication(request, nextHandler) {
183
203
  // null means no user is defined from certificate, need regular authentication as well
184
204
  if (username === undefined || username === 'Common Name' || username === 'CN')
185
205
  username = request.peerCertificate.subject.CN;
186
- request.user = await server.getUser(username, null, request);
187
- authAuditLog(username, AUTH_AUDIT_STATUS.SUCCESS, 'mTLS');
206
+ let mtlsUser;
207
+ try {
208
+ mtlsUser = await server.getUser(username, null, request);
209
+ } catch (error) {
210
+ if (LOG_AUTH_FAILED) authAuditLog(username, AUTH_AUDIT_STATUS.FAILURE, 'mTLS');
211
+ const failureResponse = settleAuthFailure(request, error, 'mTLS');
212
+ if (failureResponse) return applyResponseHeaders(failureResponse);
213
+ mtlsCredentialDeferred = true;
214
+ }
215
+ if (!mtlsCredentialDeferred) {
216
+ request.user = mtlsUser;
217
+ authAuditLog(username, AUTH_AUDIT_STATUS.SUCCESS, 'mTLS');
218
+ }
188
219
  } else {
189
220
  debug('HTTPS/WSS mTLS authorized connection (mTLS did not authorize a user)', 'from', request.ip);
190
221
  }
@@ -193,6 +224,9 @@ export async function authentication(request, nextHandler) {
193
224
  let newUser;
194
225
  if (request.user) {
195
226
  // already authenticated
227
+ } else if (mtlsCredentialDeferred) {
228
+ // a principal resolved from another credential here would be contradicted by the deferred
229
+ // certificate rejection downstream, so the certificate's decision stands alone
196
230
  } else if (authorization) {
197
231
  let cachedUser = authorizationCache.get(authorization);
198
232
  // A cached Bearer identity must not outlive its token: expiry is the only revocation
@@ -214,7 +248,7 @@ export async function authentication(request, nextHandler) {
214
248
  const strategy = authorization.slice(0, spaceIndex);
215
249
  const credentials = authorization.slice(spaceIndex + 1);
216
250
  let username, password;
217
- let credentialRejection;
251
+ let credentialDeferred = false;
218
252
  try {
219
253
  switch (strategy) {
220
254
  case 'Basic':
@@ -269,23 +303,12 @@ export async function authentication(request, nextHandler) {
269
303
  }
270
304
  }
271
305
 
272
- const internalFault = !isCredentialRejection(err);
273
- if (request.isOperationsServer || internalFault) {
274
- if (internalFault) authLogger.error('Authentication failed internally', errorForLog(err));
275
- return applyResponseHeaders({
276
- status: 401,
277
- body: serializeMessage(
278
- { error: internalFault ? AUTHENTICATION_ERROR_MSGS.GENERIC_AUTH_FAIL : err.message },
279
- request
280
- ),
281
- });
282
- }
283
- credentialRejection = err;
306
+ const failureResponse = settleAuthFailure(request, err, strategy);
307
+ if (failureResponse) return applyResponseHeaders(failureResponse);
308
+ credentialDeferred = true;
284
309
  }
285
310
 
286
- if (credentialRejection) {
287
- deferCredentialRejection(request, credentialRejection, strategy);
288
- } else {
311
+ if (!credentialDeferred) {
289
312
  authorizationCache.set(authorization, newUser);
290
313
  if (LOG_AUTH_SUCCESSFUL && newUser != null)
291
314
  authAuditLog(newUser.username, AUTH_AUDIT_STATUS.SUCCESS, strategy);
@@ -299,8 +322,14 @@ export async function authentication(request, nextHandler) {
299
322
 
300
323
  request.user = newUser;
301
324
  } else if (session?.user) {
302
- // or should this be cached in the session?
303
- request.user = await server.getUser(session.user, null, request);
325
+ try {
326
+ // or should this be cached in the session?
327
+ request.user = await server.getUser(session.user, null, request);
328
+ } catch (error) {
329
+ if (LOG_AUTH_FAILED) authAuditLog(session.user, AUTH_AUDIT_STATUS.FAILURE, 'Session');
330
+ const failureResponse = settleAuthFailure(request, error, 'Session');
331
+ if (failureResponse) return applyResponseHeaders(failureResponse);
332
+ }
304
333
  } else if (
305
334
  (AUTHORIZE_LOCAL && bypassUser) || // explicit bypass (test mode); also covers ::ffff:127.x addresses
306
335
  (AUTHORIZE_LOCAL && (request.ip?.includes('127.0.0.') || request.ip == '::1')) ||
@@ -65,7 +65,29 @@ export function settleDeferredCredentialRejection(
65
65
  };
66
66
  }
67
67
 
68
+ const REJECTED_IN_PLACE = Symbol('harper.authenticationRejectedInPlace');
69
+
70
+ /**
71
+ * Records an authentication decision the middleware answered in place, with the client-safe message
72
+ * the HTTP response carries. A WebSocket or MQTT upgrade only awaits the chain and never reads its
73
+ * resolved value, so a returned 401 is invisible to it; without this record such an upgrade would
74
+ * continue with no principal on a fault that failed the equivalent HTTP request closed.
75
+ */
76
+ export function markAuthenticationRejectedInPlace(request: any, status: number, message: string): void {
77
+ if (request?.[REJECTED_IN_PLACE]) return;
78
+ Object.defineProperty(request, REJECTED_IN_PLACE, {
79
+ value: Object.freeze({ status, message }),
80
+ enumerable: false,
81
+ configurable: false,
82
+ writable: false,
83
+ });
84
+ }
85
+
86
+ export function getAuthenticationRejectedInPlace(request: any): { status: number; message: string } | undefined {
87
+ return request?.[REJECTED_IN_PLACE];
88
+ }
89
+
68
90
  export function assertNoDeferredCredentialRejection(request: any): void {
69
- const deferred = getDeferredCredentialRejection(request);
70
- if (deferred) throw new ClientError(deferred.message, deferred.status);
91
+ const rejected = getDeferredCredentialRejection(request) ?? getAuthenticationRejectedInPlace(request);
92
+ if (rejected) throw new ClientError(rejected.message, rejected.status);
71
93
  }
@@ -18,6 +18,7 @@ import * as child_process from 'node:child_process';
18
18
  import { CONFIG_PARAMS, DEFAULT_DATABASE_NAME } from '../utility/hdbTerms.ts';
19
19
 
20
20
  import { contentTypes } from '../server/serverHelpers/contentTypes.ts';
21
+ import { markCredentialRejection, credentialRejectionError } from './credentialRejection.ts';
21
22
  import type {} from 'ses';
22
23
  import {
23
24
  existsSync,
@@ -32,6 +33,7 @@ import {
32
33
  } from 'node:fs';
33
34
  import { EventEmitter } from 'node:events';
34
35
  import { whenComponentsLoaded } from '../server/threads/threadServer.js';
36
+ import { thisThreadOwnsApplication } from '../server/threads/isolatedApplications.ts';
35
37
 
36
38
  type Lockdown = 'none' | 'freeze' | 'ses' | 'freeze-after-load';
37
39
  const APPLICATIONS_LOCKDOWN: Lockdown = env.get(CONFIG_PARAMS.APPLICATIONS_LOCKDOWN);
@@ -893,13 +895,14 @@ function scopedDatabaseBindings(scope: ApplicationScope): { databases: any; tabl
893
895
  }
894
896
 
895
897
  /**
896
- * `defineTable` for a branched application registers through that application's table factory, so
897
- * a branched name lands in its branch; an unbranched application gets `defineTable` itself.
898
+ * `defineTable` for a branched or isolated application registers through that application's table
899
+ * factory, so a branched name lands in its branch and an isolated schema is tagged with its owner.
898
900
  */
899
901
  function scopedDefineTable(scope: ApplicationScope): typeof defineTable {
900
902
  const branches = scope.branches;
901
- if (!branches?.size) return defineTable;
902
- const declareTable = scopedTableFactory(branches);
903
+ const isolatedApplicationOwner = thisThreadOwnsApplication(scope.name);
904
+ if (!branches?.size && !isolatedApplicationOwner) return defineTable;
905
+ const declareTable = scopedTableFactory(branches, isolatedApplicationOwner);
903
906
  return function (name: string, shape: any, options?: any) {
904
907
  return defineTableUsing(declareTable, name, shape, options);
905
908
  } as typeof defineTable;
@@ -944,6 +947,8 @@ function getHarperExports(scope: ApplicationScope) {
944
947
  authenticateUser: server.authenticateUser,
945
948
  operation: server.operation,
946
949
  contentTypes,
950
+ markCredentialRejection,
951
+ credentialRejectionError,
947
952
  Attribute: undefined,
948
953
  Config: undefined,
949
954
  ConfigValue: undefined,