harper 5.1.20 → 5.2.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/agent/operations.ts +13 -16
  2. package/components/Application.ts +188 -7
  3. package/components/ApplicationScope.ts +3 -0
  4. package/components/Scope.ts +12 -0
  5. package/components/anthropic/index.ts +9 -2
  6. package/components/bedrock/index.ts +25 -2
  7. package/components/componentLoader.ts +56 -7
  8. package/components/componentSecrets.ts +451 -0
  9. package/components/deploymentRecorder.ts +7 -1
  10. package/components/mcp/adapters/fastify.ts +3 -0
  11. package/components/mcp/adapters/harperHttp.ts +3 -0
  12. package/components/mcp/audit.ts +21 -6
  13. package/components/mcp/customResourceRegistry.ts +17 -38
  14. package/components/mcp/listChanged.ts +37 -20
  15. package/components/mcp/quota.ts +145 -0
  16. package/components/mcp/rateLimit.ts +152 -14
  17. package/components/mcp/resources.ts +2 -4
  18. package/components/mcp/toolRegistry.ts +73 -4
  19. package/components/mcp/tools/application.ts +77 -8
  20. package/components/mcp/tools/operations.ts +106 -36
  21. package/components/mcp/tools/schemas/operationDescriptions.ts +4 -4
  22. package/components/mcp/tools/schemas/operations.ts +5 -0
  23. package/components/mcp/transport.ts +55 -4
  24. package/components/ollama/index.ts +6 -2
  25. package/components/openai/index.ts +9 -2
  26. package/components/operations.js +204 -10
  27. package/components/operationsValidation.js +173 -0
  28. package/components/secretOperations.ts +498 -0
  29. package/config/configUtils.ts +25 -1
  30. package/config-app.schema.json +81 -0
  31. package/config-root.schema.json +8 -1
  32. package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
  33. package/dataLayer/readAuditLog.ts +14 -0
  34. package/dataLayer/schemaDescribe.ts +1 -0
  35. package/dist/agent/operations.d.ts +7 -4
  36. package/dist/agent/operations.js +13 -15
  37. package/dist/agent/operations.js.map +1 -1
  38. package/dist/components/Application.d.ts +25 -2
  39. package/dist/components/Application.js +166 -7
  40. package/dist/components/Application.js.map +1 -1
  41. package/dist/components/ApplicationScope.d.ts +2 -0
  42. package/dist/components/ApplicationScope.js +3 -0
  43. package/dist/components/ApplicationScope.js.map +1 -1
  44. package/dist/components/Scope.d.ts +8 -0
  45. package/dist/components/Scope.js +11 -0
  46. package/dist/components/Scope.js.map +1 -1
  47. package/dist/components/anthropic/index.d.ts +4 -1
  48. package/dist/components/anthropic/index.js +4 -2
  49. package/dist/components/anthropic/index.js.map +1 -1
  50. package/dist/components/bedrock/index.d.ts +4 -1
  51. package/dist/components/bedrock/index.js +22 -2
  52. package/dist/components/bedrock/index.js.map +1 -1
  53. package/dist/components/componentLoader.d.ts +2 -0
  54. package/dist/components/componentLoader.js +52 -10
  55. package/dist/components/componentLoader.js.map +1 -1
  56. package/dist/components/componentSecrets.d.ts +63 -0
  57. package/dist/components/componentSecrets.js +405 -0
  58. package/dist/components/componentSecrets.js.map +1 -0
  59. package/dist/components/deploymentRecorder.d.ts +3 -0
  60. package/dist/components/deploymentRecorder.js +4 -3
  61. package/dist/components/deploymentRecorder.js.map +1 -1
  62. package/dist/components/mcp/adapters/fastify.d.ts +2 -0
  63. package/dist/components/mcp/adapters/fastify.js +1 -0
  64. package/dist/components/mcp/adapters/fastify.js.map +1 -1
  65. package/dist/components/mcp/adapters/harperHttp.d.ts +2 -0
  66. package/dist/components/mcp/adapters/harperHttp.js +1 -0
  67. package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
  68. package/dist/components/mcp/audit.d.ts +2 -2
  69. package/dist/components/mcp/audit.js +20 -5
  70. package/dist/components/mcp/audit.js.map +1 -1
  71. package/dist/components/mcp/customResourceRegistry.d.ts +5 -12
  72. package/dist/components/mcp/customResourceRegistry.js +15 -30
  73. package/dist/components/mcp/customResourceRegistry.js.map +1 -1
  74. package/dist/components/mcp/listChanged.d.ts +7 -2
  75. package/dist/components/mcp/listChanged.js +28 -0
  76. package/dist/components/mcp/listChanged.js.map +1 -1
  77. package/dist/components/mcp/quota.d.ts +31 -0
  78. package/dist/components/mcp/quota.js +155 -0
  79. package/dist/components/mcp/quota.js.map +1 -0
  80. package/dist/components/mcp/rateLimit.d.ts +19 -3
  81. package/dist/components/mcp/rateLimit.js +129 -13
  82. package/dist/components/mcp/rateLimit.js.map +1 -1
  83. package/dist/components/mcp/resources.js.map +1 -1
  84. package/dist/components/mcp/toolRegistry.d.ts +39 -1
  85. package/dist/components/mcp/toolRegistry.js +60 -5
  86. package/dist/components/mcp/toolRegistry.js.map +1 -1
  87. package/dist/components/mcp/tools/application.d.ts +8 -1
  88. package/dist/components/mcp/tools/application.js +65 -5
  89. package/dist/components/mcp/tools/application.js.map +1 -1
  90. package/dist/components/mcp/tools/operations.d.ts +15 -3
  91. package/dist/components/mcp/tools/operations.js +99 -38
  92. package/dist/components/mcp/tools/operations.js.map +1 -1
  93. package/dist/components/mcp/tools/schemas/operationDescriptions.js +4 -4
  94. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  95. package/dist/components/mcp/tools/schemas/operations.js +4 -0
  96. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  97. package/dist/components/mcp/transport.d.ts +6 -0
  98. package/dist/components/mcp/transport.js +47 -3
  99. package/dist/components/mcp/transport.js.map +1 -1
  100. package/dist/components/ollama/index.d.ts +4 -1
  101. package/dist/components/ollama/index.js +4 -2
  102. package/dist/components/ollama/index.js.map +1 -1
  103. package/dist/components/openai/index.d.ts +4 -1
  104. package/dist/components/openai/index.js +4 -2
  105. package/dist/components/openai/index.js.map +1 -1
  106. package/dist/components/operations.d.ts +39 -0
  107. package/dist/components/operations.js +194 -2
  108. package/dist/components/operations.js.map +1 -1
  109. package/dist/components/operationsValidation.d.ts +38 -0
  110. package/dist/components/operationsValidation.js +155 -0
  111. package/dist/components/operationsValidation.js.map +1 -1
  112. package/dist/components/secretOperations.d.ts +113 -0
  113. package/dist/components/secretOperations.js +475 -0
  114. package/dist/components/secretOperations.js.map +1 -0
  115. package/dist/config/configUtils.d.ts +4 -1
  116. package/dist/config/configUtils.js +18 -1
  117. package/dist/config/configUtils.js.map +1 -1
  118. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -3
  119. package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
  120. package/dist/dataLayer/readAuditLog.js +11 -0
  121. package/dist/dataLayer/readAuditLog.js.map +1 -1
  122. package/dist/dataLayer/schemaDescribe.js +2 -0
  123. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  124. package/dist/globals.d.ts +1 -1
  125. package/dist/globals.js +1 -0
  126. package/dist/globals.js.map +1 -1
  127. package/dist/index.d.ts +3 -0
  128. package/dist/index.js +1 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/json/systemSchema.json +38 -0
  131. package/dist/resources/DatabaseTransaction.d.ts +31 -3
  132. package/dist/resources/DatabaseTransaction.js +190 -29
  133. package/dist/resources/DatabaseTransaction.js.map +1 -1
  134. package/dist/resources/LMDBTransaction.d.ts +1 -1
  135. package/dist/resources/LMDBTransaction.js +43 -5
  136. package/dist/resources/LMDBTransaction.js.map +1 -1
  137. package/dist/resources/PrimaryRocksDatabase.d.ts +51 -0
  138. package/dist/resources/PrimaryRocksDatabase.js +194 -0
  139. package/dist/resources/PrimaryRocksDatabase.js.map +1 -0
  140. package/dist/resources/RecordEncoder.d.ts +1 -0
  141. package/dist/resources/RecordEncoder.js +81 -75
  142. package/dist/resources/RecordEncoder.js.map +1 -1
  143. package/dist/resources/RequestTarget.d.ts +19 -0
  144. package/dist/resources/RequestTarget.js.map +1 -1
  145. package/dist/resources/Resource.d.ts +0 -1
  146. package/dist/resources/Resource.js +103 -16
  147. package/dist/resources/Resource.js.map +1 -1
  148. package/dist/resources/ResourceInterface.d.ts +6 -2
  149. package/dist/resources/ResourceInterface.js.map +1 -1
  150. package/dist/resources/Resources.js.map +1 -1
  151. package/dist/resources/Table.d.ts +6 -4
  152. package/dist/resources/Table.js +247 -24
  153. package/dist/resources/Table.js.map +1 -1
  154. package/dist/resources/analytics/read.js +25 -28
  155. package/dist/resources/analytics/read.js.map +1 -1
  156. package/dist/resources/blob.js +165 -22
  157. package/dist/resources/blob.js.map +1 -1
  158. package/dist/resources/crdt.d.ts +2 -0
  159. package/dist/resources/crdt.js +45 -36
  160. package/dist/resources/crdt.js.map +1 -1
  161. package/dist/resources/databases.d.ts +1 -0
  162. package/dist/resources/databases.js +134 -7
  163. package/dist/resources/databases.js.map +1 -1
  164. package/dist/resources/graphql.js +8 -0
  165. package/dist/resources/graphql.js.map +1 -1
  166. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +35 -5
  167. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +239 -14
  168. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  169. package/dist/resources/jsResource.js +6 -0
  170. package/dist/resources/jsResource.js.map +1 -1
  171. package/dist/resources/loadEnv.js +30 -3
  172. package/dist/resources/loadEnv.js.map +1 -1
  173. package/dist/resources/login.js +2 -2
  174. package/dist/resources/login.js.map +1 -1
  175. package/dist/resources/models/Models.js +14 -3
  176. package/dist/resources/models/Models.js.map +1 -1
  177. package/dist/resources/models/embedHook.js +10 -2
  178. package/dist/resources/models/embedHook.js.map +1 -1
  179. package/dist/resources/models/openaiStream.d.ts +56 -0
  180. package/dist/resources/models/openaiStream.js +94 -0
  181. package/dist/resources/models/openaiStream.js.map +1 -0
  182. package/dist/resources/replayLogs.js +4 -3
  183. package/dist/resources/replayLogs.js.map +1 -1
  184. package/dist/resources/search.d.ts +1 -1
  185. package/dist/resources/search.js +104 -11
  186. package/dist/resources/search.js.map +1 -1
  187. package/dist/resources/secretDecryptor.d.ts +54 -0
  188. package/dist/resources/secretDecryptor.js +131 -0
  189. package/dist/resources/secretDecryptor.js.map +1 -0
  190. package/dist/resources/tracked.js +15 -5
  191. package/dist/resources/tracked.js.map +1 -1
  192. package/dist/security/auth.js +71 -8
  193. package/dist/security/auth.js.map +1 -1
  194. package/dist/security/jsLoader.js +11 -0
  195. package/dist/security/jsLoader.js.map +1 -1
  196. package/dist/security/tokenAuthentication.d.ts +9 -1
  197. package/dist/security/tokenAuthentication.js +31 -1
  198. package/dist/security/tokenAuthentication.js.map +1 -1
  199. package/dist/server/DurableSubscriptionsSession.d.ts +1 -1
  200. package/dist/server/DurableSubscriptionsSession.js +8 -4
  201. package/dist/server/DurableSubscriptionsSession.js.map +1 -1
  202. package/dist/server/REST.js +28 -9
  203. package/dist/server/REST.js.map +1 -1
  204. package/dist/server/fastifyRoutes.js +11 -1
  205. package/dist/server/fastifyRoutes.js.map +1 -1
  206. package/dist/server/graphqlQuerying.js +4 -3
  207. package/dist/server/graphqlQuerying.js.map +1 -1
  208. package/dist/server/http.d.ts +40 -2
  209. package/dist/server/http.js +560 -77
  210. package/dist/server/http.js.map +1 -1
  211. package/dist/server/itc/serverHandlers.js +64 -0
  212. package/dist/server/itc/serverHandlers.js.map +1 -1
  213. package/dist/server/liveSubscriptionAuth.d.ts +12 -0
  214. package/dist/server/liveSubscriptionAuth.js +140 -0
  215. package/dist/server/liveSubscriptionAuth.js.map +1 -0
  216. package/dist/server/loadRootComponents.js +2 -1
  217. package/dist/server/loadRootComponents.js.map +1 -1
  218. package/dist/server/middlewareChain.d.ts +44 -1
  219. package/dist/server/middlewareChain.js +86 -10
  220. package/dist/server/middlewareChain.js.map +1 -1
  221. package/dist/server/mqtt.js +2 -1
  222. package/dist/server/mqtt.js.map +1 -1
  223. package/dist/server/operationsServer.js +1 -1
  224. package/dist/server/operationsServer.js.map +1 -1
  225. package/dist/server/serverHelpers/Headers.d.ts +9 -0
  226. package/dist/server/serverHelpers/Headers.js +22 -0
  227. package/dist/server/serverHelpers/Headers.js.map +1 -1
  228. package/dist/server/serverHelpers/JSONStream.js +3 -3
  229. package/dist/server/serverHelpers/JSONStream.js.map +1 -1
  230. package/dist/server/serverHelpers/Request.d.ts +52 -1
  231. package/dist/server/serverHelpers/Request.js +122 -1
  232. package/dist/server/serverHelpers/Request.js.map +1 -1
  233. package/dist/server/serverHelpers/contentTypes.js +3 -8
  234. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  235. package/dist/server/serverHelpers/progressEmitter.d.ts +19 -0
  236. package/dist/server/serverHelpers/progressEmitter.js +52 -4
  237. package/dist/server/serverHelpers/progressEmitter.js.map +1 -1
  238. package/dist/server/serverHelpers/registeredOperations.d.ts +40 -0
  239. package/dist/server/serverHelpers/registeredOperations.js +286 -0
  240. package/dist/server/serverHelpers/registeredOperations.js.map +1 -0
  241. package/dist/server/serverHelpers/serverHandlers.js +11 -5
  242. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  243. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -0
  244. package/dist/server/serverHelpers/serverUtilities.js +67 -5
  245. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  246. package/dist/server/serverHelpers/uwsServer.d.ts +80 -0
  247. package/dist/server/serverHelpers/uwsServer.js +433 -0
  248. package/dist/server/serverHelpers/uwsServer.js.map +1 -0
  249. package/dist/server/static.d.ts +0 -15
  250. package/dist/server/static.js +161 -2
  251. package/dist/server/static.js.map +1 -1
  252. package/dist/server/status/index.d.ts +4 -1
  253. package/dist/server/status/index.js +64 -4
  254. package/dist/server/status/index.js.map +1 -1
  255. package/dist/server/threads/manageThreads.d.ts +10 -0
  256. package/dist/server/threads/manageThreads.js +107 -4
  257. package/dist/server/threads/manageThreads.js.map +1 -1
  258. package/dist/server/threads/socketRouter.js +5 -1
  259. package/dist/server/threads/socketRouter.js.map +1 -1
  260. package/dist/server/threads/threadServer.js +92 -14
  261. package/dist/server/threads/threadServer.js.map +1 -1
  262. package/dist/upgrade/directives/5-2-0.d.ts +7 -0
  263. package/dist/upgrade/directives/5-2-0.js +108 -0
  264. package/dist/upgrade/directives/5-2-0.js.map +1 -0
  265. package/dist/upgrade/directives/directivesController.js +2 -1
  266. package/dist/upgrade/directives/directivesController.js.map +1 -1
  267. package/dist/utility/common_utils.js +5 -0
  268. package/dist/utility/common_utils.js.map +1 -1
  269. package/dist/utility/envFile.d.ts +41 -0
  270. package/dist/utility/envFile.js +221 -0
  271. package/dist/utility/envFile.js.map +1 -0
  272. package/dist/utility/globalSchema.d.ts +9 -0
  273. package/dist/utility/hdbTerms.d.ts +25 -0
  274. package/dist/utility/hdbTerms.js +31 -0
  275. package/dist/utility/hdbTerms.js.map +1 -1
  276. package/dist/utility/logging/harper_logger.d.ts +21 -0
  277. package/dist/utility/logging/harper_logger.js +160 -8
  278. package/dist/utility/logging/harper_logger.js.map +1 -1
  279. package/dist/utility/logging/logRotator.js +29 -17
  280. package/dist/utility/logging/logRotator.js.map +1 -1
  281. package/dist/utility/logging/readLog.d.ts +1 -1
  282. package/dist/utility/logging/readLog.js +305 -2
  283. package/dist/utility/logging/readLog.js.map +1 -1
  284. package/dist/utility/operationPermissions.d.ts +18 -0
  285. package/dist/utility/operationPermissions.js +35 -1
  286. package/dist/utility/operationPermissions.js.map +1 -1
  287. package/dist/utility/operation_authorization.d.ts +17 -0
  288. package/dist/utility/operation_authorization.js +38 -0
  289. package/dist/utility/operation_authorization.js.map +1 -1
  290. package/dist/utility/secretEnvelope.d.ts +30 -0
  291. package/dist/utility/secretEnvelope.js +94 -0
  292. package/dist/utility/secretEnvelope.js.map +1 -0
  293. package/dist/utility/signalling.d.ts +7 -0
  294. package/dist/utility/signalling.js +16 -0
  295. package/dist/utility/signalling.js.map +1 -1
  296. package/dist/validation/configValidator.js +70 -24
  297. package/dist/validation/configValidator.js.map +1 -1
  298. package/index.ts +4 -0
  299. package/json/systemSchema.json +38 -0
  300. package/npm-shrinkwrap.json +15066 -9347
  301. package/package.json +13 -8
  302. package/resources/DESIGN.md +15 -12
  303. package/resources/DatabaseTransaction.ts +206 -37
  304. package/resources/LMDBTransaction.ts +43 -9
  305. package/resources/PrimaryRocksDatabase.ts +201 -0
  306. package/resources/RecordEncoder.ts +81 -69
  307. package/resources/RequestTarget.ts +21 -0
  308. package/resources/Resource.ts +109 -25
  309. package/resources/ResourceInterface.ts +6 -2
  310. package/resources/Resources.ts +1 -3
  311. package/resources/Table.ts +238 -29
  312. package/resources/analytics/read.ts +25 -30
  313. package/resources/blob.ts +159 -23
  314. package/resources/crdt.ts +37 -31
  315. package/resources/databases.ts +143 -6
  316. package/resources/graphql.ts +8 -0
  317. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +251 -17
  318. package/resources/jsResource.ts +6 -0
  319. package/resources/loadEnv.ts +34 -3
  320. package/resources/login.ts +2 -2
  321. package/resources/models/Models.ts +19 -3
  322. package/resources/models/embedHook.ts +13 -2
  323. package/resources/models/openaiStream.ts +133 -0
  324. package/resources/replayLogs.ts +4 -3
  325. package/resources/search.ts +113 -11
  326. package/resources/secretDecryptor.ts +159 -0
  327. package/resources/tracked.ts +24 -17
  328. package/security/auth.ts +70 -10
  329. package/security/jsLoader.ts +12 -0
  330. package/security/tokenAuthentication.ts +42 -1
  331. package/server/DESIGN.md +10 -0
  332. package/server/DurableSubscriptionsSession.ts +8 -4
  333. package/server/REST.ts +26 -9
  334. package/server/fastifyRoutes.ts +10 -1
  335. package/server/graphqlQuerying.ts +4 -3
  336. package/server/http.ts +563 -77
  337. package/server/itc/serverHandlers.js +70 -0
  338. package/server/liveSubscriptionAuth.ts +152 -0
  339. package/server/loadRootComponents.js +2 -1
  340. package/server/middlewareChain.ts +107 -17
  341. package/server/mqtt.ts +2 -1
  342. package/server/operationsServer.ts +2 -2
  343. package/server/serverHelpers/Headers.ts +24 -0
  344. package/server/serverHelpers/JSONStream.ts +3 -3
  345. package/server/serverHelpers/Request.ts +127 -0
  346. package/server/serverHelpers/contentTypes.ts +3 -8
  347. package/server/serverHelpers/progressEmitter.ts +55 -5
  348. package/server/serverHelpers/registeredOperations.ts +259 -0
  349. package/server/serverHelpers/serverHandlers.js +11 -5
  350. package/server/serverHelpers/serverUtilities.ts +88 -5
  351. package/server/serverHelpers/uwsServer.ts +488 -0
  352. package/server/static.ts +180 -2
  353. package/server/status/index.ts +72 -4
  354. package/server/threads/manageThreads.js +102 -4
  355. package/server/threads/socketRouter.ts +5 -1
  356. package/server/threads/threadServer.js +93 -10
  357. package/studio/web/assets/{Chat-SdD1EZca.js → Chat-Cv_2paZE.js} +2 -2
  358. package/studio/web/assets/{Chat-SdD1EZca.js.map → Chat-Cv_2paZE.js.map} +1 -1
  359. package/studio/web/assets/{FloatingChat-GMq6tHK9.js → FloatingChat-CPyPIcVR.js} +4 -4
  360. package/studio/web/assets/{FloatingChat-GMq6tHK9.js.map → FloatingChat-CPyPIcVR.js.map} +1 -1
  361. package/studio/web/assets/{applications-B5hhv7uA.js → applications-C0jT2vdZ.js} +2 -2
  362. package/studio/web/assets/{applications-B5hhv7uA.js.map → applications-C0jT2vdZ.js.map} +1 -1
  363. package/studio/web/assets/{index-VFcpNWgq.js → index-D_GKOkhn.js} +6 -6
  364. package/studio/web/assets/index-D_GKOkhn.js.map +1 -0
  365. package/studio/web/assets/{index.lazy-DgcJojIA.js → index.lazy-CAmCIA65.js} +4 -4
  366. package/studio/web/assets/{index.lazy-DgcJojIA.js.map → index.lazy-CAmCIA65.js.map} +1 -1
  367. package/studio/web/assets/{profile-BqN-8vzm.js → profile-CaF-4aZe.js} +2 -2
  368. package/studio/web/assets/{profile-BqN-8vzm.js.map → profile-CaF-4aZe.js.map} +1 -1
  369. package/studio/web/assets/{setComponentFile-CHFvFq9u.js → setComponentFile-DLW1DHCt.js} +2 -2
  370. package/studio/web/assets/{setComponentFile-CHFvFq9u.js.map → setComponentFile-DLW1DHCt.js.map} +1 -1
  371. package/studio/web/assets/{setup-CQ3vWJSr.js → setup-TGCErIhq.js} +2 -2
  372. package/studio/web/assets/{setup-CQ3vWJSr.js.map → setup-TGCErIhq.js.map} +1 -1
  373. package/studio/web/assets/{status-BsW5fjuh.js → status-DG0Maoao.js} +2 -2
  374. package/studio/web/assets/{status-BsW5fjuh.js.map → status-DG0Maoao.js.map} +1 -1
  375. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js → swagger-ui-react-8T4SgQ1m.js} +2 -2
  376. package/studio/web/assets/{swagger-ui-react-DITdeB9-.js.map → swagger-ui-react-8T4SgQ1m.js.map} +1 -1
  377. package/studio/web/assets/{tsMode-Dyph-OUs.js → tsMode-DJ6Sl24Q.js} +2 -2
  378. package/studio/web/assets/{tsMode-Dyph-OUs.js.map → tsMode-DJ6Sl24Q.js.map} +1 -1
  379. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js → useEntityRestURL-JO2mfWTQ.js} +2 -2
  380. package/studio/web/assets/{useEntityRestURL-D0QTUqex.js.map → useEntityRestURL-JO2mfWTQ.js.map} +1 -1
  381. package/studio/web/index.html +1 -1
  382. package/upgrade/directives/5-2-0.ts +82 -0
  383. package/upgrade/directives/directivesController.ts +2 -1
  384. package/utility/common_utils.ts +5 -0
  385. package/utility/envFile.ts +218 -0
  386. package/utility/hdbTerms.ts +31 -0
  387. package/utility/logging/harper_logger.ts +156 -8
  388. package/utility/logging/logRotator.ts +24 -14
  389. package/utility/logging/readLog.ts +323 -2
  390. package/utility/operationPermissions.ts +35 -1
  391. package/utility/operation_authorization.ts +74 -1
  392. package/utility/secretEnvelope.ts +113 -0
  393. package/utility/signalling.ts +15 -0
  394. package/validation/configValidator.ts +78 -25
  395. package/studio/web/assets/index-VFcpNWgq.js.map +0 -1
package/server/http.ts CHANGED
@@ -13,10 +13,11 @@ import * as terms from '../utility/hdbTerms.ts';
13
13
  import { getConfigPath } from '../config/configUtils.ts';
14
14
  import { getTicketKeys, getWorkerIndex } from './threads/manageThreads.js';
15
15
  import { createTLSSelector } from '../security/keys.ts';
16
- import { createSecureServer } from 'node:http2';
16
+ import { createSecureServer, createServer as createH2CServer } from 'node:http2';
17
17
  import { createServer as createSecureServerHttp1 } from 'node:https';
18
18
  import { createServer, IncomingMessage } from 'node:http';
19
- import { Request, BunRequest, isBun } from './serverHelpers/Request.ts';
19
+ import { createServer as createNetServer } from 'node:net';
20
+ import { Request, BunRequest, UwsRequest, isBun } from './serverHelpers/Request.ts';
20
21
  import { appendHeader, Headers, toWriteHeadHeaders } from './serverHelpers/Headers.ts';
21
22
  import { Blob } from '../resources/blob.ts';
22
23
  import { recordAction, recordActionBinary } from '../resources/analytics/write.ts';
@@ -27,10 +28,10 @@ import { server, type ServerOptions, type HttpOptions, type UpgradeOptions, Upgr
27
28
  import { setPortServerMap, SERVERS } from './serverRegistry.ts';
28
29
  import { getComponentName } from '../components/componentLoader.ts';
29
30
  import { throttle } from './throttle.ts';
30
- import { makeCallbackChain as buildCallbackChain } from './middlewareChain.ts';
31
+ import { makeCallbackChain as buildCallbackChain, describeChains } from './middlewareChain.ts';
31
32
  import { WebSocketServer } from 'ws';
32
33
 
33
- const { errorToString } = harperLogger;
34
+ const { errorToString, errorForLog } = harperLogger;
34
35
  server.http = httpServer;
35
36
  server.request = onRequest;
36
37
  server.ws = onWebSocket;
@@ -51,8 +52,15 @@ let httpOptions: HttpOptions = {};
51
52
  export const universalHeaders: [string, string][] = [];
52
53
  // Bun-specific: stores fetch handler configs per port, used by threadServer.js to call Bun.serve()
53
54
  export const bunServeConfigs: Record<string | number, any> = {};
54
- // Bun-specific: stores non-function listeners (e.g. Fastify servers) per port for fallback delegation
55
- const bunFallbackServers: Record<string | number, any> = {};
55
+ // uWS backend (#914): stores { socketPath, secure, handler } configs keyed by UDS path. When
56
+ // HARPER_UWS_UDS is set, the per-worker UDS mirror is served by uWebSockets.js instead of a Node
57
+ // http server; threadServer.js consumes these and calls createUwsServer(). Symphony must forward
58
+ // client identity via X-Forwarded-For (not the PROXY protocol) on these sockets, as the Bun path does.
59
+ export const uwsServeConfigs: Record<string, any> = {};
60
+ // Stores non-function listeners (e.g. Fastify servers) per port for fallback delegation on the
61
+ // backends that don't register their own Node http server (Bun, and the uWS HTTP path). Keeping
62
+ // them out of SERVERS is what prevents a competing Node server from binding the same port.
63
+ const fallbackServers: Record<string | number, any> = {};
56
64
  const udsCleanupPaths: { socketPath: string; yamlPath: string }[] = [];
57
65
 
58
66
  export function registerUdsCleanupPaths(socketPath: string, yamlPath: string) {
@@ -71,9 +79,12 @@ export function cleanupUdsFiles() {
71
79
  }
72
80
 
73
81
  /** Write YAML metadata for a UDS mirror socket, describing the TLS certs from the corresponding secure server. */
74
- export function writeUdsMetadata(yamlPath: string, port: number | string, secureServer: any) {
82
+ export function writeUdsMetadata(yamlPath: string, port: number | string, secureServer: any, protocol?: string) {
75
83
  const contexts = secureServer.secureContexts;
76
84
  let yaml = `pid: ${process.pid}\ntid: ${currentThreadId()}\nport: ${port}\n`;
85
+ // Which application protocol this socket speaks (absent = http/1.1, the historical
86
+ // default) — lets a fronting proxy route by negotiated ALPN.
87
+ if (protocol) yaml += `protocol: ${protocol}\n`;
77
88
  yaml += `certificates:\n`;
78
89
  if (contexts?.size > 0) {
79
90
  const seen = new Set();
@@ -300,7 +311,16 @@ export function httpServer(listener, options) {
300
311
  httpResponders[options?.runFirst ? 'unshift' : 'push'](entry);
301
312
  } else if (isBun) {
302
313
  // On Bun, store non-function listeners (e.g. Fastify's http.Server) for fallback delegation
303
- bunFallbackServers[port] = listener;
314
+ fallbackServers[port] = listener;
315
+ } else if ((httpServers[port] as any)?.uws) {
316
+ // uWS HTTP path (#914, HARPER_UWS_HTTP): the port is backed by uWebSockets.js, not a Node
317
+ // http server, so a raw non-function listener (e.g. Fastify's http.Server via
318
+ // server.http(fastify.server)) must NOT go through registerServer() — that would put it in
319
+ // SERVERS and threadServer would bind a Node http server competing with uWS on the same TCP
320
+ // port. Divert it to the fallback map like the Bun path; makeUwsHandler delegates unhandled
321
+ // requests to it via inject(). The { uws: true } marker is guaranteed present here: the
322
+ // getServer(port) call above (same loop iteration) sets it before this branch runs.
323
+ fallbackServers[port] = listener;
304
324
  } else {
305
325
  listener.isSecure = secure;
306
326
  registerServer(listener, port, false);
@@ -317,6 +337,31 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
317
337
  if (!httpServers[port]) {
318
338
  // TODO: These should all come from httpOptions or operationsApiOptions
319
339
  const serverPrefix = isOperationsServer ? 'operationsApi_network' : (usageType ?? 'http');
340
+ // uWS plaintext-HTTP path (#914, HARPER_UWS_HTTP): back a non-secure TCP HTTP port with
341
+ // uWebSockets.js directly instead of a Node http server. This is the flag used to run the
342
+ // integration suite through uWS (no symphony/UDS needed). WebSocket upgrades are wired on this
343
+ // path via onWebSocket's uWS branch; it's opt-in and separate from HARPER_UWS_UDS.
344
+ const lastColon = String(port).lastIndexOf(':');
345
+ const uwsPort = lastColon > 0 ? +String(port).slice(lastColon + 1) : +port;
346
+ if (
347
+ process.env.HARPER_UWS_HTTP &&
348
+ !secure &&
349
+ !isOperationsServer &&
350
+ !String(port).includes('/') &&
351
+ !Number.isNaN(uwsPort)
352
+ ) {
353
+ uwsServeConfigs[port] = {
354
+ port: uwsPort,
355
+ host: lastColon > 0 ? String(port).slice(0, lastColon).replace(/[[\]]/g, '') : undefined,
356
+ secure: false,
357
+ handler: makeUwsHandler(port, isOperationsServer, env.get(serverPrefix + '_requestQueueLimit')),
358
+ };
359
+ // Marker so the httpServers guard is satisfied and the caller has a truthy handle; the
360
+ // actual listen happens in threadServer.js from uwsServeConfigs. onWebSocket() detects
361
+ // this marker (server.uws) and wires native uWS WebSocket handling into the same config.
362
+ httpServers[port] = { uws: true, port } as any;
363
+ return httpServers[port];
364
+ }
320
365
  const keepAliveTimeout = env.get(serverPrefix + '_keepAliveTimeout');
321
366
  const requestTimeout = env.get(serverPrefix + '_timeout');
322
367
  const headersTimeout = env.get(serverPrefix + '_headersTimeout');
@@ -500,17 +545,19 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
500
545
  }
501
546
  function onError(error) {
502
547
  const headers = error.headers;
503
- const status = error.statusCode || 500;
548
+ // the HTTP status may be carried as `statusCode` (our error classes) or `status` (e.g. a thrown plain object)
549
+ const statusCode = error.statusCode ?? error.status;
550
+ const status = statusCode || 500;
504
551
  try {
505
552
  nodeResponse.writeHead(status, toWriteHeadHeaders(headers));
506
553
  } catch {} // silently ignore errors writing headers, because they may have been set already
507
554
  nodeResponse.end(errorToString(error));
508
555
  logRequest(nodeRequest, status, requestId, performance.now() - startTime);
509
556
  // a status code is interpreted as an expected error, so just info or warn, otherwise log as error
510
- if (error.statusCode) {
511
- if (error.statusCode === 500) harperLogger.warn(error);
512
- else harperLogger.info(error);
513
- } else harperLogger.error(error);
557
+ if (statusCode) {
558
+ if (statusCode === 500) harperLogger.warn(errorForLog(error));
559
+ else harperLogger.info(errorForLog(error));
560
+ } else harperLogger.error(errorForLog(error));
514
561
  }
515
562
  };
516
563
  // create a throttled version of the request handler, so we can throttle POST requests
@@ -571,38 +618,281 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
571
618
  const udsPath = join(socketsDir, `${socketName}.sock`);
572
619
  const yamlPath = join(socketsDir, `${socketName}.yaml`);
573
620
 
574
- // Create a plain HTTP server (no TLS) with the same request handler
575
- const udsServer = createServer(
576
- {
577
- keepAliveTimeout,
578
- headersTimeout,
579
- requestTimeout,
580
- highWaterMark: 128 * 1024,
581
- noDelay: true,
582
- keepAlive: true,
583
- keepAliveInitialDelay: 600,
584
- maxHeaderSize: env.get(terms.CONFIG_PARAMS.HTTP_MAXHEADERSIZE),
585
- },
586
- (nodeRequest: IncomingMessage, nodeResponse: any) => {
587
- const method = nodeRequest.method;
588
- if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD') requestHandler(nodeRequest, nodeResponse);
589
- else throttledRequestHandler(nodeRequest, nodeResponse);
590
- }
591
- );
621
+ if (process.env.HARPER_UWS_UDS) {
622
+ // uWS backend (#914): serve the UDS mirror with uWebSockets.js instead of a Node http
623
+ // server. threadServer.js consumes uwsServeConfigs and calls createUwsServer(). uWS does
624
+ // not parse the PROXY protocol, so symphony must use sourceAddressHeader: 'xForwardedFor'
625
+ // for this socket (the same mode it uses for the Bun path).
626
+ uwsServeConfigs[udsPath] = {
627
+ socketPath: udsPath,
628
+ secure: true,
629
+ handler: makeUwsHandler(port, isOperationsServer, env.get(serverPrefix + '_requestQueueLimit')),
630
+ };
631
+ } else {
632
+ // Create a plain HTTP server (no TLS) with the same request handler
633
+ const udsServer = createServer(
634
+ {
635
+ keepAliveTimeout,
636
+ headersTimeout,
637
+ requestTimeout,
638
+ highWaterMark: 128 * 1024,
639
+ noDelay: true,
640
+ keepAlive: true,
641
+ keepAliveInitialDelay: 600,
642
+ maxHeaderSize: env.get(terms.CONFIG_PARAMS.HTTP_MAXHEADERSIZE),
643
+ },
644
+ (nodeRequest: IncomingMessage, nodeResponse: any) => {
645
+ const method = nodeRequest.method;
646
+ if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD')
647
+ requestHandler(nodeRequest, nodeResponse);
648
+ else throttledRequestHandler(nodeRequest, nodeResponse);
649
+ }
650
+ );
592
651
 
593
- udsServer.isPerThreadSocket = true;
594
- enableProxyProtocol(udsServer);
595
- SERVERS[udsPath] = udsServer;
652
+ udsServer.isPerThreadSocket = true;
653
+ enableProxyProtocol(udsServer);
654
+ SERVERS[udsPath] = udsServer;
655
+ }
596
656
  registerUdsCleanupPaths(udsPath, yamlPath);
597
657
 
598
658
  const writeMetadata = () => writeUdsMetadata(yamlPath, port, server);
599
659
  options.SNICallback.ready.then(writeMetadata);
600
660
  server.secureContextsListeners.push(writeMetadata);
661
+
662
+ // Optional cleartext HTTP/2 mirror (spike: HARPER_H2C_UDS=1). A separate socket
663
+ // (`<worker>-<port>-h2.sock`) so a fronting proxy can route by negotiated ALPN:
664
+ // h2 connections here, http/1.1 to the plain mirror above. The metadata yaml
665
+ // carries `protocol: h2` so the proxy can discover which socket speaks what.
666
+ if (process.env.HARPER_H2C_UDS) {
667
+ const udsPathH2 = join(socketsDir, `${socketName}-h2.sock`);
668
+ const yamlPathH2 = join(socketsDir, `${socketName}-h2.yaml`);
669
+ const h2Server = createH2CServer({}, (nodeRequest: any, nodeResponse: any) => {
670
+ const method = nodeRequest.method;
671
+ if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD') requestHandler(nodeRequest, nodeResponse);
672
+ else throttledRequestHandler(nodeRequest, nodeResponse);
673
+ });
674
+ // A stray non-h2 client (or a truncated preface) fails the session, not the worker.
675
+ h2Server.on('sessionError', (error: Error) => {
676
+ harperLogger.debug('h2c UDS session error:', error);
677
+ });
678
+ const h2Front = createH2CProxyFront(h2Server) as any;
679
+ h2Front.isPerThreadSocket = true;
680
+ SERVERS[udsPathH2] = h2Front;
681
+ registerUdsCleanupPaths(udsPathH2, yamlPathH2);
682
+
683
+ const writeMetadataH2 = () => writeUdsMetadata(yamlPathH2, port, server, 'h2');
684
+ options.SNICallback.ready.then(writeMetadataH2);
685
+ server.secureContextsListeners.push(writeMetadataH2);
686
+ }
601
687
  }
602
688
  }
603
689
  return httpServers[port];
604
690
  }
605
691
 
692
+ /**
693
+ * uWS backend (#914): builds the per-request handler for a uWS UDS server. Mirrors the Bun
694
+ * fetchHandler's post-processing (httpChain, unhandled, universalHeaders, Server-Timing,
695
+ * analytics, logging) but returns a plain Harper response descriptor for createUwsServer to
696
+ * serialize onto the uWS HttpResponse. When the chain doesn't handle the request (status === -1)
697
+ * and a Fastify fallback is registered for the port, it delegates via inject() (see injectToFastify),
698
+ * mirroring the Bun path — so legacy Fastify routes work behind uWS too.
699
+ */
700
+ function makeUwsHandler(port: number | string, isOperationsServer: boolean, requestQueueLimit?: number) {
701
+ // Build a fresh response descriptor rather than mutating what the chain returned: a handler may
702
+ // return a WHATWG `Response` (read-only `status`/`body` accessors), which the Bun path also never
703
+ // mutates. `headers` is normalized in place the same way the Bun path does.
704
+ const handle = async (request: any) => {
705
+ const startTime = performance.now();
706
+ let requestId = 0;
707
+ if (isOperationsServer) request.isOperationsServer = true;
708
+ if (httpOptions.logging?.id) request.requestId = requestId = getRequestId();
709
+ let response = await httpChain[port](request);
710
+ if (!response) response = unhandled(request);
711
+ let headers = response.headers;
712
+ if (!headers?.set) headers = new Headers(headers);
713
+ for (const [key, value] of universalHeaders) headers.set(key, value);
714
+ if (response.status === -1) {
715
+ // The chain didn't handle it. If a Fastify fallback is registered for this port (legacy
716
+ // custom-function routes via server.http(fastify.server)), delegate to it via inject(),
717
+ // mirroring the Bun path; otherwise it's a genuine 404.
718
+ const fastify = fastifyInstances[port];
719
+ if (fastify) {
720
+ const injectResult = await injectToFastify(fastify, {
721
+ method: request.method,
722
+ url: request.url,
723
+ headers: request.headers.asObject,
724
+ body: request.body, // stream; inject() consumes it as the payload
725
+ user: request.user,
726
+ });
727
+ const respHeaders = new Headers();
728
+ for (const [k, v] of Object.entries(injectResult.headers)) {
729
+ if (v == null) continue;
730
+ // Keep Set-Cookie multi-valued (Harper Headers + writeHeaders emit each separately);
731
+ // only comma-join other repeated headers.
732
+ if (Array.isArray(v)) respHeaders.set(k, k.toLowerCase() === 'set-cookie' ? v : v.join(', '));
733
+ else respHeaders.set(k, String(v));
734
+ }
735
+ logHttpRequest(request, injectResult.statusCode, requestId, performance.now() - startTime);
736
+ const responseStream = injectResult.stream();
737
+ // Event-stream (SSE) responses must reach the client incrementally — stream the body and,
738
+ // on client disconnect, destroy the inject response so the Fastify reply's teardown runs
739
+ // (matches the Bun path). Finite responses buffer so Content-Length stays set.
740
+ if (String(injectResult.headers['content-type'] ?? '').includes('text/event-stream')) {
741
+ const injectResponse = injectResult.raw?.res;
742
+ if (injectResponse && typeof injectResponse.destroy === 'function') {
743
+ responseStream.once('close', () => injectResponse.destroy());
744
+ }
745
+ return { status: injectResult.statusCode, headers: respHeaders, body: responseStream };
746
+ }
747
+ const chunks: Buffer[] = [];
748
+ for await (const chunk of responseStream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
749
+ return {
750
+ status: injectResult.statusCode,
751
+ headers: respHeaders,
752
+ body: chunks.length > 0 ? Buffer.concat(chunks) : null,
753
+ };
754
+ }
755
+ logHttpRequest(request, 404, requestId, performance.now() - startTime);
756
+ return { status: 404, headers: new Headers({ 'content-type': 'text/plain' }), body: 'Not found\n' };
757
+ }
758
+ const status = response.status || 200;
759
+ const executionTime = performance.now() - startTime;
760
+ if (!response.handlesHeaders) {
761
+ let serverTiming = `hdb;dur=${executionTime.toFixed(2)}`;
762
+ if (response.wasCacheMiss) serverTiming += ', miss';
763
+ appendHeader(headers, 'Server-Timing', serverTiming, true);
764
+ }
765
+ recordAction(
766
+ executionTime,
767
+ 'duration',
768
+ request.handlerPath,
769
+ request.method,
770
+ response.wasCacheMiss == undefined ? undefined : response.wasCacheMiss ? 'cache-miss' : 'cache-hit'
771
+ );
772
+ recordActionBinary(status < 400, 'success', request.handlerPath, request.method);
773
+ recordActionBinary(1, 'response_' + status, request.handlerPath, request.method);
774
+ logHttpRequest(request, status, requestId, executionTime);
775
+ // Static handlers (the only handlesHeaders producers) return a `send` SendStream that writes
776
+ // its own headers/body to a Node ServerResponse via .pipe(). uWS has no such object, and a
777
+ // SendStream doesn't start until piped, so streaming it directly hangs (headers never flush).
778
+ // Pipe it into a Writable shim that captures the headers and buffers the file, mirroring the
779
+ // Bun path. Non-handlesHeaders bodies keep streaming through normalizeUwsBody.
780
+ if (response.handlesHeaders && response.body && typeof response.body.pipe === 'function') {
781
+ // send() may return 304 (conditional GET) or 206/416 (Range) — honor the status it set.
782
+ const sent = await bufferSendStream(response.body, headers, status, request.signal);
783
+ return { status: sent.status, headers, handlesHeaders: true, body: sent.body };
784
+ }
785
+ const body = await normalizeUwsBody(response.body, request.signal);
786
+ return { status, headers, handlesHeaders: response.handlesHeaders, body };
787
+ };
788
+ // Shed data-modifying requests when the event queue is backed up (503), mirroring the Node UDS
789
+ // path — GET/OPTIONS/HEAD are cheap and always run, everything else goes through the throttle.
790
+ const throttledHandle = throttle(
791
+ handle,
792
+ (_request: any) => {
793
+ recordAction(true, 'service-unavailable', port);
794
+ return {
795
+ status: 503,
796
+ headers: new Headers({ 'content-type': 'text/plain' }),
797
+ body: 'Service unavailable, exceeded request queue limit',
798
+ };
799
+ },
800
+ requestQueueLimit
801
+ );
802
+ return (request: any) => {
803
+ const method = request.method;
804
+ if (method === 'GET' || method === 'OPTIONS' || method === 'HEAD') return handle(request);
805
+ return throttledHandle(request);
806
+ };
807
+ }
808
+
809
+ /**
810
+ * uWS: normalize a Harper response body into what the adapter can serialize. Finite bodies collapse
811
+ * to a string/Buffer; a Node stream or async-iterable is returned as a Readable so writeResponse can
812
+ * stream it incrementally (buffering an SSE/event-stream body here would never return). `signal`
813
+ * aborts the collapse of a sync iterable if the client disconnects mid-response.
814
+ */
815
+ /**
816
+ * Drive a `send` SendStream to completion against a Writable shim, capturing the headers it writes
817
+ * (setHeader/writeHead) onto `headers` and the status it sets (statusCode/writeHead) and buffering
818
+ * the file body. `send` targets an http.ServerResponse (setHeader/writeHead/statusCode/finished);
819
+ * uWS has none, so we adapt — mirrors the Bun fetchHandler's SendStream path. The captured status
820
+ * carries send's conditional-GET (304) and Range (206/416) results. Buffering is fine for static
821
+ * assets and keeps Content-Length set. `defaultStatus` is used when send sets none.
822
+ */
823
+ function bufferSendStream(
824
+ body: any,
825
+ headers: Headers,
826
+ defaultStatus: number,
827
+ signal?: AbortSignal
828
+ ): Promise<{ body: Buffer; status: number }> {
829
+ return new Promise((resolve, reject) => {
830
+ const chunks: Buffer[] = [];
831
+ const dest: any = new Writable({
832
+ write(chunk, _encoding, callback) {
833
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
834
+ callback();
835
+ },
836
+ final(callback) {
837
+ callback();
838
+ resolve({ body: Buffer.concat(chunks), status: dest.statusCode || defaultStatus });
839
+ },
840
+ });
841
+ Object.assign(dest, {
842
+ setHeader: (n: string, v: any) => headers.set(n, v),
843
+ getHeader: (n: string) => headers.get(n),
844
+ removeHeader: (n: string) => (headers as any).delete(n.toLowerCase()),
845
+ // send conveys 304/206/416 via statusCode and/or writeHead's status arg — capture both so
846
+ // conditional-GET and Range responses aren't flattened to the default 200.
847
+ writeHead: (s: number, hdrs?: any) => {
848
+ if (s) dest.statusCode = s;
849
+ if (hdrs) for (const k in hdrs) headers.set(k, hdrs[k]);
850
+ },
851
+ statusCode: defaultStatus,
852
+ headersSent: false,
853
+ // 'on-finished' (used by 'send') treats a non-false `finished` as already-done and destroys
854
+ // the read stream before data flows; keep it false so it waits for the 'finish' event.
855
+ finished: false,
856
+ });
857
+ const onAbort = () => {
858
+ body.destroy?.();
859
+ dest.destroy?.();
860
+ reject(new Error('client aborted'));
861
+ };
862
+ if (signal) {
863
+ if (signal.aborted) return onAbort();
864
+ signal.addEventListener('abort', onAbort, { once: true });
865
+ }
866
+ body.on('error', reject);
867
+ dest.on('error', reject);
868
+ body.pipe(dest);
869
+ });
870
+ }
871
+
872
+ async function normalizeUwsBody(
873
+ body: any,
874
+ signal?: AbortSignal
875
+ ): Promise<string | Buffer | Uint8Array | Readable | null> {
876
+ if (body == null) return null;
877
+ if (typeof body === 'string' || Buffer.isBuffer(body) || body instanceof Uint8Array) return body;
878
+ if (body instanceof Blob) return Buffer.from(await body.arrayBuffer());
879
+ if (typeof body.then === 'function') return normalizeUwsBody(await body, signal);
880
+ // Already a Node stream — stream it as-is (re-wrapping in Readable.from breaks destroy propagation).
881
+ if (typeof body.pipe === 'function') return body;
882
+ // Async-iterable (e.g. an event queue) — adapt to a Readable and stream it.
883
+ if (body[Symbol.asyncIterator]) return Readable.from(body);
884
+ // Sync iterable — small/finite, collapse to a buffer.
885
+ if (body[Symbol.iterator]) {
886
+ const chunks: Buffer[] = [];
887
+ for (const chunk of body) {
888
+ if (signal?.aborted) throw new Error('client aborted');
889
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
890
+ }
891
+ return Buffer.concat(chunks);
892
+ }
893
+ return String(body);
894
+ }
895
+
606
896
  /**
607
897
  * Bun-specific HTTP server setup. Instead of creating a Node http.Server, we store a fetch handler config
608
898
  * that will be passed to Bun.serve() when listenOnPorts() is called in threadServer.js.
@@ -632,14 +922,14 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
632
922
  response.headers.set(key, value);
633
923
  }
634
924
  if (response.status === -1) {
635
- const fallbackServer = bunFallbackServers[port];
925
+ const fallbackServer = fallbackServers[port];
636
926
  if (fallbackServer) {
637
927
  // Delegate to the fallback server (e.g. Fastify) via node:http compatibility.
638
928
  // We create a Node-compatible IncomingMessage/ServerResponse and emit 'request'
639
929
  // on the fallback server, then capture the response.
640
930
  return await bunDelegateToNodeServer(fallbackServer, webRequest, request);
641
931
  }
642
- logBunRequest(request, 404, requestId, performance.now() - startTime);
932
+ logHttpRequest(request, 404, requestId, performance.now() - startTime);
643
933
  return new Response('Not found\n', { status: 404 });
644
934
  }
645
935
  const status = response.status || 200;
@@ -693,7 +983,7 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
693
983
  );
694
984
  recordActionBinary(status < 400, 'success', handlerPath, method);
695
985
  recordActionBinary(1, 'response_' + status, handlerPath, method);
696
- logBunRequest(request, status, requestId, executionTime);
986
+ logHttpRequest(request, status, requestId, executionTime);
697
987
  // Convert body to something Bun's Response can accept
698
988
  if (body instanceof ReadableStream) {
699
989
  return new Response(body, { status, headers: responseHeaders });
@@ -745,12 +1035,14 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
745
1035
  }
746
1036
  return new Response(body, { status, headers: responseHeaders });
747
1037
  } catch (error) {
748
- const status = error.statusCode || 500;
749
- logBunRequest(null, status, requestId, performance.now() - startTime);
750
- if (error.statusCode) {
751
- if (error.statusCode === 500) harperLogger.warn(error);
752
- else harperLogger.info(error);
753
- } else harperLogger.error(error);
1038
+ // the HTTP status may be carried as `statusCode` (our error classes) or `status` (e.g. a thrown plain object)
1039
+ const statusCode = error.statusCode ?? error.status;
1040
+ const status = statusCode || 500;
1041
+ logHttpRequest(null, status, requestId, performance.now() - startTime);
1042
+ if (statusCode) {
1043
+ if (statusCode === 500) harperLogger.warn(errorForLog(error));
1044
+ else harperLogger.info(errorForLog(error));
1045
+ } else harperLogger.error(errorForLog(error));
754
1046
  return new Response(errorToString(error), { status });
755
1047
  }
756
1048
  };
@@ -789,46 +1081,61 @@ function getBunHTTPServer(port: number, secure: boolean, options: ServerOptions)
789
1081
  * Bridge a Bun fetch request to a Node.js http.Server (e.g. Fastify) by using Fastify's inject()
790
1082
  * method to send the request through its internal router without needing a real socket.
791
1083
  */
792
- let bunFastifyInstances: Record<string | number, any> = {};
793
- export function registerBunFastifyInstance(port: string | number, instance: any) {
794
- bunFastifyInstances[port] = instance;
1084
+ let fastifyInstances: Record<string | number, any> = {};
1085
+ export function registerFastifyInstance(port: string | number, instance: any) {
1086
+ fastifyInstances[port] = instance;
795
1087
  }
796
1088
  const INTERNAL_USER_HEADER = 'x-harper-internal-pre-auth-user';
797
1089
 
1090
+ /**
1091
+ * Run a request through a Fastify instance via inject() — its internal router, no socket needed.
1092
+ * Shared by the Bun and uWS fallback-delegation paths. Strips any forged pre-auth header from the
1093
+ * client and, when Harper's auth middleware resolved a user without credentials (e.g. AUTHORIZE_LOCAL
1094
+ * for loopback in dev), forwards it so Fastify can skip its own auth — only when no Authorization
1095
+ * header was supplied, otherwise Fastify's Passport validates the credentials normally.
1096
+ * `payloadAsStream` makes inject() resolve as soon as the response headers are written and exposes
1097
+ * the body as a Readable, so a long-lived SSE response (the MCP server-push GET) streams instead of
1098
+ * buffering forever.
1099
+ */
1100
+ function injectToFastify(
1101
+ fastify: any,
1102
+ req: { method: string; url: string; headers: Record<string, any>; body?: Buffer | Readable; user?: any }
1103
+ ) {
1104
+ const headers: Record<string, any> = {};
1105
+ for (const key in req.headers) {
1106
+ if (key.toLowerCase() !== INTERNAL_USER_HEADER) headers[key] = req.headers[key];
1107
+ }
1108
+ // Both callers pass already-lowercased header keys (uWS lowercases at the protocol level →
1109
+ // RequestHeaders.asObject; Bun's webRequest.headers.forEach yields lowercase), so the literal
1110
+ // 'authorization' lookup is reliable — the pre-auth user is only forwarded when the client sent
1111
+ // no credentials of its own.
1112
+ if (req.user && !headers['authorization']) {
1113
+ headers[INTERNAL_USER_HEADER] = JSON.stringify(req.user);
1114
+ }
1115
+ return fastify.inject({ method: req.method, url: req.url, headers, payload: req.body, payloadAsStream: true });
1116
+ }
1117
+
798
1118
  async function bunDelegateToNodeServer(
799
1119
  nodeServer: any,
800
1120
  webRequest: globalThis.Request,
801
1121
  bunRequest?: any
802
1122
  ): Promise<Response> {
803
1123
  // Check if there's a Fastify instance registered for this port (preferred path)
804
- for (const port in bunFallbackServers) {
805
- if (bunFallbackServers[port] === nodeServer && bunFastifyInstances[port]) {
806
- const fastify = bunFastifyInstances[port];
1124
+ for (const port in fallbackServers) {
1125
+ if (fallbackServers[port] === nodeServer && fastifyInstances[port]) {
1126
+ const fastify = fastifyInstances[port];
807
1127
  const url = new URL(webRequest.url);
808
1128
  const body = webRequest.body ? Buffer.from(await webRequest.arrayBuffer()) : undefined;
809
1129
  const headers: Record<string, string> = {};
810
1130
  webRequest.headers.forEach((value, key) => {
811
- // Strip any forged pre-auth header from real clients
812
- if (key.toLowerCase() !== INTERNAL_USER_HEADER) headers[key] = value;
1131
+ headers[key] = value;
813
1132
  });
814
- // If Harper's auth middleware authenticated this request without credentials (e.g. via
815
- // AUTHORIZE_LOCAL for loopback connections in dev mode), pass the user so Fastify can
816
- // skip its own auth. Only applies when there is no Authorization header — if credentials
817
- // were provided, let Fastify's Passport validate them normally.
818
- if (bunRequest?.user && !headers['authorization']) {
819
- headers[INTERNAL_USER_HEADER] = JSON.stringify(bunRequest.user);
820
- }
821
- // `payloadAsStream` makes inject() resolve as soon as the response headers are
822
- // written and exposes the body as a Readable, instead of buffering the whole
823
- // payload and resolving only on response end. Without it a long-lived SSE
824
- // response (the MCP server-push GET) never ends, so `await inject()` would
825
- // hang forever and the client would never receive headers.
826
- const injectResult = await fastify.inject({
1133
+ const injectResult = await injectToFastify(fastify, {
827
1134
  method: webRequest.method,
828
1135
  url: url.pathname + url.search,
829
1136
  headers,
830
- payload: body,
831
- payloadAsStream: true,
1137
+ body,
1138
+ user: bunRequest?.user,
832
1139
  });
833
1140
  const webHeaders = new globalThis.Headers();
834
1141
  for (const [k, v] of Object.entries(injectResult.headers)) {
@@ -874,22 +1181,87 @@ async function bunDelegateToNodeServer(
874
1181
  return new Response('Not found\n', { status: 404 });
875
1182
  }
876
1183
 
877
- function makeCallbackChain(responders: typeof httpResponders, portNum: number | string, requestArgIndex: number = 0) {
1184
+ type SerializedRoute = { host?: string; urlPath?: string; order: string[] };
1185
+ // Resolved order captured at chain-build time, keyed identically to httpChain/upgradeChains/
1186
+ // websocketChains (kind → port → routes). Reporting the stored build-time order rather than
1187
+ // recomputing from current responders guarantees get_status matches the callback chain actually
1188
+ // serving that port — including cases where a late `port: 'all'` registration rebuilds only the
1189
+ // 'all' chain and leaves a concrete port's chain (and this description) unchanged (#1573).
1190
+ const resolvedChainDescriptions: Record<string, Record<string, SerializedRoute[]>> = {
1191
+ http: {},
1192
+ upgrade: {},
1193
+ websocket: {},
1194
+ };
1195
+
1196
+ function makeCallbackChain(
1197
+ responders: typeof httpResponders,
1198
+ portNum: number | string,
1199
+ requestArgIndex: number = 0,
1200
+ kind: string = 'http'
1201
+ ) {
1202
+ const onCycle = () => {
1203
+ harperLogger.warn(
1204
+ `Cycle detected in ${kind} middleware before/after ordering on port ${portNum}; falling back to registration order.`
1205
+ );
1206
+ };
1207
+ // describeChains reuses the same resolvers as buildCallbackChain, so this is the served order.
1208
+ // onCycle is omitted: the build call owns the single cycle warning, and on a cycle describeChains
1209
+ // falls back to registration order exactly as the built chain does.
1210
+ const routes: SerializedRoute[] = describeChains(responders, portNum).map((route) => ({
1211
+ host: route.host,
1212
+ urlPath: route.urlPath,
1213
+ order: route.order.map((entry) => entry.name ?? '(anonymous)'),
1214
+ }));
1215
+ resolvedChainDescriptions[kind][portNum] = routes;
1216
+ if (harperLogger.debug) {
1217
+ for (const route of routes) {
1218
+ const scope = route.host || route.urlPath ? ` [${route.host ?? '*'}${route.urlPath ?? ''}]` : '';
1219
+ harperLogger.debug(
1220
+ `Resolved ${kind} middleware chain on port ${portNum}${scope}: ${route.order.join(' → ') || '(empty)'}`
1221
+ );
1222
+ }
1223
+ }
878
1224
  return buildCallbackChain(
879
1225
  responders,
880
1226
  portNum,
881
1227
  unhandled,
882
- () => {
1228
+ onCycle,
1229
+ requestArgIndex,
1230
+ ({ entryName, kind: refKind, target }) => {
883
1231
  harperLogger.warn(
884
- `Cycle detected in middleware before/after ordering on port ${portNum}; falling back to registration order.`
1232
+ `Middleware ordering: ${entryName ? `'${entryName}'` : 'a handler'} requested \`${refKind}: '${target}'\` but no handler named '${target}' is registered on port ${portNum}, so the constraint is ignored. Handler names are the config keys as registered (e.g. 'rest').`
885
1233
  );
886
- },
887
- requestArgIndex
1234
+ }
888
1235
  );
889
1236
  }
1237
+
1238
+ /**
1239
+ * Returns the resolved middleware order for every built HTTP, upgrade, and WebSocket chain on the
1240
+ * current thread, as plain serializable data (listeners omitted). Surfaced via the `get_status`
1241
+ * operation so chain placement can be verified on a running instance (#1573). The 'all' pseudo-port
1242
+ * is excluded: makeCallbackChain builds a chain for it, but it is not a bound listener (its
1243
+ * responders already fold into every concrete port's chain).
1244
+ *
1245
+ * Note: this reflects the calling thread's built chains. All HTTP worker threads register
1246
+ * identically, so any worker's view is representative.
1247
+ */
1248
+ export function describeMiddlewareChains() {
1249
+ const concretePorts = (byPort: Record<string, SerializedRoute[]>) => {
1250
+ const out: Record<string, SerializedRoute[]> = {};
1251
+ for (const port of Object.keys(byPort)) if (port !== 'all') out[port] = byPort[port];
1252
+ return out;
1253
+ };
1254
+ return {
1255
+ http: concretePorts(resolvedChainDescriptions.http),
1256
+ upgrade: concretePorts(resolvedChainDescriptions.upgrade),
1257
+ websocket: concretePorts(resolvedChainDescriptions.websocket),
1258
+ };
1259
+ }
890
1260
  function unhandled(request) {
891
- if (request.user) {
892
- // pass on authentication information to the next server
1261
+ if (request.user && request._nodeRequest) {
1262
+ // pass on authentication information to the next server (Node fallback delegation via the
1263
+ // 'unhandled' event chain). The Bun/uWS adapters have no _nodeRequest; they forward the
1264
+ // resolved user to the Fastify fallback via injectToFastify's INTERNAL_USER_HEADER instead.
893
1265
  request._nodeRequest.user = request.user;
894
1266
  }
895
1267
  return {
@@ -929,7 +1301,7 @@ function onUpgrade(listener: UpgradeListener, options: UpgradeOptions) {
929
1301
  host: options?.host || undefined,
930
1302
  };
931
1303
  upgradeListeners[options?.runFirst ? 'unshift' : 'push'](entry);
932
- upgradeChains[port] = makeCallbackChain(upgradeListeners, port);
1304
+ upgradeChains[port] = makeCallbackChain(upgradeListeners, port, 0, 'upgrade');
933
1305
  }
934
1306
  }
935
1307
 
@@ -965,7 +1337,35 @@ function onWebSocket(listener: (ws: WebSocket) => void, options: OnWebSocketOpti
965
1337
 
966
1338
  const server = getHTTPServer(port, secure, options);
967
1339
 
968
- if (!websocketServers[port]) {
1340
+ if ((server as any)?.uws) {
1341
+ // uWS-backed port (HARPER_UWS_HTTP): uWS owns the socket, so route upgrades through uWS's
1342
+ // native app.ws() rather than the Node ws.WebSocketServer + server 'upgrade' event. We wire a
1343
+ // wsHandler into the shared uwsServeConfig; createUwsServer registers app.ws() when it listens.
1344
+ const cfg = uwsServeConfigs[port];
1345
+ if (cfg && !cfg.wsHandler) {
1346
+ // Honor a configured WebSocket maxPayload on the uWS transport too (else it defaults to 100 MiB).
1347
+ if (options.maxPayload != null) cfg.wsMaxPayload = options.maxPayload;
1348
+ cfg.wsHandler = (ws: any, upgrade: any) => {
1349
+ try {
1350
+ const request: any = new UwsRequest({
1351
+ method: 'GET',
1352
+ url: upgrade.url,
1353
+ headers: upgrade.headers,
1354
+ secure,
1355
+ ip: upgrade.ip,
1356
+ });
1357
+ request.isWebSocket = true;
1358
+ const chainCompletion = httpChain[port](request);
1359
+ websocketChains[port](ws, request, chainCompletion);
1360
+ } catch (error) {
1361
+ harperLogger.warn('Error in handling WS connection', error);
1362
+ try {
1363
+ ws.close();
1364
+ } catch {}
1365
+ }
1366
+ };
1367
+ }
1368
+ } else if (!websocketServers[port]) {
969
1369
  websocketServers[port] = new WebSocketServer({
970
1370
  noServer: true,
971
1371
  // TODO: this should be a global config and not per ws listener
@@ -1023,7 +1423,7 @@ function onWebSocket(listener: (ws: WebSocket) => void, options: OnWebSocketOpti
1023
1423
  host: options?.host || undefined,
1024
1424
  };
1025
1425
  websocketListeners[options?.runFirst ? 'unshift' : 'push'](wsEntry);
1026
- websocketChains[port] = makeCallbackChain(websocketListeners, port, 1);
1426
+ websocketChains[port] = makeCallbackChain(websocketListeners, port, 1, 'websocket');
1027
1427
 
1028
1428
  // mqtt doesn't invoke the http handler so this needs to be here to load up the http chains.
1029
1429
  httpChain[port] = makeCallbackChain(httpResponders, port);
@@ -1105,6 +1505,92 @@ export function enableProxyProtocol(httpServer) {
1105
1505
  });
1106
1506
  }
1107
1507
 
1508
+ /**
1509
+ * Front a cleartext HTTP/2 server with optional PROXY v1 handling on a Unix domain socket.
1510
+ *
1511
+ * enableProxyProtocol() can't be used here: Node's Http2Session consumes the socket's
1512
+ * native handle directly, so data never surfaces as JS 'data' events to intercept. The
1513
+ * PROXY header must instead be consumed *before* the socket is handed to the HTTP/2
1514
+ * server. Bytes beyond the header (typically the coalesced h2 connection preface) are
1515
+ * unshifted back onto the socket; the native session picks them up (verified on Node 24
1516
+ * — covered by a unit test so a Node upgrade regressing this fails loudly).
1517
+ *
1518
+ * The returned server's close() also gracefully closes live h2 sessions (GOAWAY,
1519
+ * in-flight streams finish) so closeServers()'s generic server.close() drains instead
1520
+ * of riding its 5s force-exit backstop — the h1 mirror gets this via http.Server's
1521
+ * closeIdleConnections drain, which a net.Server doesn't have.
1522
+ */
1523
+ export function createH2CProxyFront(h2Server, prehandoffTimeout = 10_000) {
1524
+ const sessions = new Set<any>();
1525
+ const prehandoffSockets = new Set<any>();
1526
+ let closing = false;
1527
+ h2Server.on('session', (session) => {
1528
+ // A connection can be mid-handoff (header read, session not yet created) when
1529
+ // close() runs — its session forms after the close sweep, so close it here or
1530
+ // it would never receive GOAWAY and would ride the 5s force-exit backstop.
1531
+ if (closing) session.close();
1532
+ sessions.add(session);
1533
+ session.on('close', () => sessions.delete(session));
1534
+ });
1535
+ const front = createNetServer({ noDelay: true }, (socket) => {
1536
+ let buf: Buffer | null = null;
1537
+ // Until handoff the h2 session's own handlers aren't attached yet: swallow socket
1538
+ // errors (a reset mid-header) and bound how long we'll wait for the header, so a
1539
+ // stalled connection can't hold an fd forever.
1540
+ prehandoffSockets.add(socket);
1541
+ socket.on('close', () => prehandoffSockets.delete(socket));
1542
+ const onPrehandoffError = () => socket.destroy();
1543
+ socket.on('error', onPrehandoffError);
1544
+ const onPrehandoffTimeout = () => socket.destroy();
1545
+ socket.setTimeout(prehandoffTimeout, onPrehandoffTimeout);
1546
+ const handoff = (rest: Buffer) => {
1547
+ prehandoffSockets.delete(socket);
1548
+ socket.removeListener('readable', onReadable);
1549
+ socket.removeListener('error', onPrehandoffError);
1550
+ socket.setTimeout(0);
1551
+ socket.removeListener('timeout', onPrehandoffTimeout);
1552
+ if (rest.length > 0) socket.unshift(rest);
1553
+ h2Server.emit('connection', socket);
1554
+ };
1555
+ const onReadable = () => {
1556
+ let chunk: Buffer;
1557
+ while ((chunk = socket.read()) !== null) {
1558
+ buf = buf ? Buffer.concat([buf, chunk]) : chunk;
1559
+ // Compare against "PROXY " for as many bytes as we have so far; a non-PROXY
1560
+ // prefix (e.g. a direct h2 client with no fronting proxy) is handed off as-is.
1561
+ const cmpLen = Math.min(PROXY_V1_PREFIX.length, buf.length);
1562
+ if (buf.compare(PROXY_V1_PREFIX, 0, cmpLen, 0, cmpLen) !== 0) return handoff(buf);
1563
+ const eol = buf.indexOf('\r\n');
1564
+ if (eol !== -1) {
1565
+ // Complete header: "PROXY TCP4 <src-ip> <dst-ip> <src-port> <dst-port>"
1566
+ const parts = buf.toString('latin1', 0, eol).split(' ');
1567
+ if (parts.length === 6) {
1568
+ // Override the UDS socket's undefined remoteAddress/remotePort with the real
1569
+ // client values; http2's compat req.socket proxies through to these.
1570
+ Object.defineProperty(socket, 'remoteAddress', { value: parts[2], configurable: true });
1571
+ Object.defineProperty(socket, 'remotePort', { value: parseInt(parts[4], 10), configurable: true });
1572
+ }
1573
+ return handoff(buf.subarray(eol + 2));
1574
+ }
1575
+ // No CRLF within the spec max — not a valid PROXY header after all.
1576
+ if (buf.length >= PROXY_V1_MAX_HEADER) return handoff(buf);
1577
+ }
1578
+ };
1579
+ socket.on('readable', onReadable);
1580
+ });
1581
+ const netClose = front.close.bind(front);
1582
+ front.close = (callback?: (error?: Error) => void) => {
1583
+ closing = true;
1584
+ for (const session of sessions) session.close();
1585
+ // Header-waiting sockets carry no in-flight work; drop them so they can't hold
1586
+ // the close callback open for the rest of the pre-handoff timeout.
1587
+ for (const socket of prehandoffSockets) socket.destroy();
1588
+ h2Server.close();
1589
+ return netClose(callback);
1590
+ };
1591
+ return front;
1592
+ }
1593
+
1108
1594
  function defaultNotFound(request, response) {
1109
1595
  if (response.headersSent || response.writableEnded) return;
1110
1596
  response.writeHead(404);
@@ -1113,7 +1599,7 @@ function defaultNotFound(request, response) {
1113
1599
  }
1114
1600
  let httpLogger: any;
1115
1601
 
1116
- function logBunRequest(request: any, status: number, requestId: number, executionTime?: number) {
1602
+ function logHttpRequest(request: any, status: number, requestId: number, executionTime?: number) {
1117
1603
  const logging = httpOptions.logging;
1118
1604
  if (logging) {
1119
1605
  if (!httpLogger) {