akanjs 3.1.1 → 3.1.2-rc.0

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 (333) hide show
  1. package/README.ko.md +22 -0
  2. package/README.md +22 -0
  3. package/client/cookie.ts +23 -7
  4. package/client/csrTypes.ts +1 -1
  5. package/client/makePageProto.tsx +10 -1
  6. package/client/router.ts +57 -16
  7. package/common/IpLiteral.ts +4 -2
  8. package/common/TrustedProxy.ts +45 -2
  9. package/common/akanNames.ts +169 -0
  10. package/common/codeAgentProfile.ts +51 -4
  11. package/common/index.ts +13 -0
  12. package/common/localeConfig.ts +30 -5
  13. package/common/localePath.ts +26 -0
  14. package/common/markdownSpans.ts +3 -1
  15. package/common/mcpExposure.ts +7 -7
  16. package/common/websocketContract.ts +50 -0
  17. package/constant/index.ts +1 -0
  18. package/constant/inputParser.ts +10 -2
  19. package/constant/requestValue.ts +47 -0
  20. package/dictionary/agent.dictionary.ts +2 -2
  21. package/dictionary/base.dictionary.ts +4 -0
  22. package/dictionary/dictInfo.ts +9 -9
  23. package/document/database.ts +1 -0
  24. package/document/documentQuery.ts +42 -6
  25. package/document/filterMeta.ts +56 -8
  26. package/document/queryEvaluator.ts +13 -8
  27. package/fetch/client/fetchClient.ts +28 -62
  28. package/fetch/client/httpClient.ts +1 -0
  29. package/fetch/client/sliceInitHandle.ts +18 -18
  30. package/fetch/index.ts +1 -0
  31. package/fetch/sessionCookie.ts +37 -0
  32. package/index.ts +8 -0
  33. package/package.json +13 -27
  34. package/server/akanApp.ts +108 -29
  35. package/server/akanServer.ts +18 -7
  36. package/server/assetHeaders.ts +20 -1
  37. package/server/cachePolicy.ts +18 -5
  38. package/server/devtools/depsSerializer.ts +2 -1
  39. package/server/devtools/devtoolsRouter.ts +3 -2
  40. package/server/devtools/signalSerializer.ts +5 -5
  41. package/server/hmr/clientReport.ts +10 -5
  42. package/server/imageOptimizer.ts +117 -22
  43. package/server/lifecycle/buildEnv.ts +11 -0
  44. package/server/lifecycle/loadBuildEnv.ts +3 -0
  45. package/server/logging/logControlSocket.ts +20 -14
  46. package/server/logging/logStreamRoute.ts +12 -8
  47. package/server/logging/rotatingLogWriter.ts +15 -1
  48. package/server/machine/machineToken.ts +4 -0
  49. package/server/mcp/McpAuth.ts +12 -4
  50. package/server/mcp/McpExecutionContext.ts +4 -0
  51. package/server/mcp/McpRouter.ts +25 -13
  52. package/server/mcp/PagePromptComposer.ts +4 -7
  53. package/server/oauth/OAuthClientIdMetadata.ts +29 -6
  54. package/server/oauth/OAuthRegistration.ts +9 -0
  55. package/server/ops/failureLimiter.ts +42 -0
  56. package/server/ops/opsCommand.ts +19 -7
  57. package/server/ops/opsRoute.ts +60 -9
  58. package/server/ops/opsToken.ts +5 -0
  59. package/server/ops/snapshotJobs.ts +3 -1
  60. package/server/ops/snapshotRestore.ts +23 -1
  61. package/server/ops/sqliteSnapshot.ts +98 -18
  62. package/server/ops/statusAccess.ts +37 -19
  63. package/server/proxy/hostBasePathWebProxy.ts +4 -7
  64. package/server/proxy/localeWebProxy.ts +36 -16
  65. package/server/resolver/database.resolver.ts +15 -11
  66. package/server/resolver/service.resolver.ts +17 -2
  67. package/server/resolver/signal.resolver.ts +50 -40
  68. package/server/routeElementComposer.tsx +42 -9
  69. package/server/routeShellGate.ts +42 -0
  70. package/server/routing/apiRouter.ts +64 -1
  71. package/server/routing/serverLimits.ts +151 -0
  72. package/server/routing/soloAppRoutes.ts +23 -12
  73. package/server/rscClient.tsx +5 -3
  74. package/server/rscScrollRestoration.ts +60 -9
  75. package/server/rscWorker.tsx +76 -24
  76. package/server/sitemap.ts +5 -7
  77. package/server/ssrFromRscRenderer.tsx +36 -4
  78. package/server/ssrTypes.ts +5 -2
  79. package/server/staticPath.ts +36 -1
  80. package/server/systemPageDocument.tsx +5 -3
  81. package/server/systemPages.tsx +14 -8
  82. package/server/tunnel/TunnelAgent.ts +19 -2
  83. package/server/tunnel/TunnelDataSocket.ts +4 -2
  84. package/server/tunnel/TunnelHttpStream.ts +7 -2
  85. package/server/tunnel/TunnelTcpStream.ts +6 -3
  86. package/server/tunnel/TunnelWebsocketStream.ts +9 -2
  87. package/server/tunnel/tunnelStream.ts +6 -0
  88. package/server/types.tsx +20 -2
  89. package/server/webRouter.ts +38 -18
  90. package/service/agent.service.ts +9 -7
  91. package/service/injectInfo.ts +2 -1
  92. package/service/liveSyncHub.ts +6 -0
  93. package/service/predefinedAdaptor/anthropicLlm.ts +13 -12
  94. package/service/predefinedAdaptor/database.adaptor.ts +4 -0
  95. package/service/predefinedAdaptor/llm.adaptor.ts +3 -0
  96. package/service/predefinedAdaptor/llmOverflow.ts +5 -1
  97. package/service/predefinedAdaptor/openaiDialect.ts +9 -8
  98. package/service/predefinedAdaptor/searchIndex.ts +21 -1
  99. package/service/predefinedAdaptor/sql/QueryCompiler.ts +28 -4
  100. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +45 -11
  101. package/service/predefinedAdaptor/sql/dialect/postgres.ts +6 -1
  102. package/service/predefinedAdaptor/sql/types.ts +3 -0
  103. package/service/predefinedAdaptor/storage.adaptor.ts +11 -10
  104. package/service/serve.ts +3 -3
  105. package/service/serviceModule.ts +28 -24
  106. package/service/types.ts +3 -0
  107. package/signal/SignalFailure.ts +5 -2
  108. package/signal/agent/AgentCatalogue.ts +3 -2
  109. package/signal/agentMeter.ts +81 -9
  110. package/signal/agentRelayPolicy.ts +53 -2
  111. package/signal/agentTurnStream.ts +12 -1
  112. package/signal/guard.ts +2 -2
  113. package/signal/mcp/McpDocument.ts +3 -3
  114. package/signal/mcp/McpUriTemplate.ts +5 -5
  115. package/signal/middleware.ts +15 -1
  116. package/signal/roomKey.ts +53 -0
  117. package/signal/signalContext.ts +100 -40
  118. package/signal/slice.ts +27 -22
  119. package/signal/trace.ts +17 -6
  120. package/store/action.ts +20 -49
  121. package/store/actionTag.ts +2 -0
  122. package/store/agent/ScreenReader.ts +36 -4
  123. package/store/agent/ScreenTarget.ts +1 -1
  124. package/store/agent/StoreCatalogue.ts +7 -1
  125. package/store/agent/StoreSurfaceSource.ts +5 -0
  126. package/store/agentic/AgentConfirm.ts +30 -0
  127. package/store/agentic/StStateBuilder.ts +2 -2
  128. package/store/agentic/StToolBuilder.ts +7 -2
  129. package/store/agentic/formFields.ts +1 -2
  130. package/store/agentic/index.ts +1 -0
  131. package/store/agentic/useFieldTool.ts +4 -3
  132. package/store/deprecatedNames.ts +6 -0
  133. package/store/draftStore.ts +2 -2
  134. package/store/formUploadOf.ts +6 -5
  135. package/store/index.ts +1 -2
  136. package/store/sliceKeys.ts +9 -32
  137. package/store/state.ts +1 -1
  138. package/store/stateBuilder.ts +1 -1
  139. package/store/store.ts +1 -1
  140. package/store/storeInstance.ts +12 -8
  141. package/store/storeRegistry.ts +14 -2
  142. package/types/client/cookie.d.ts +7 -2
  143. package/types/client/csrTypes.d.ts +1 -1
  144. package/types/client/router.d.ts +4 -1
  145. package/types/common/akanNames.d.ts +149 -0
  146. package/types/common/codeAgentProfile.d.ts +4 -1
  147. package/types/common/index.d.ts +3 -1
  148. package/types/common/localeConfig.d.ts +8 -0
  149. package/types/common/localePath.d.ts +13 -0
  150. package/types/common/websocketContract.d.ts +7 -0
  151. package/types/constant/index.d.ts +1 -0
  152. package/types/constant/requestValue.d.ts +5 -0
  153. package/types/dictionary/base.dictionary.d.ts +1 -1
  154. package/types/dictionary/dictionary.d.ts +3 -3
  155. package/types/document/database.d.ts +1 -0
  156. package/types/document/documentQuery.d.ts +3 -0
  157. package/types/document/filterMeta.d.ts +4 -0
  158. package/types/fetch/client/sliceInitHandle.d.ts +1 -2
  159. package/types/fetch/index.d.ts +1 -0
  160. package/types/fetch/sessionCookie.d.ts +12 -0
  161. package/types/index.d.ts +8 -0
  162. package/types/server/akanApp.d.ts +1 -0
  163. package/types/server/assetHeaders.d.ts +5 -0
  164. package/types/server/cachePolicy.d.ts +2 -0
  165. package/types/server/devtools/devtoolsRouter.d.ts +1 -1
  166. package/types/server/imageOptimizer.d.ts +4 -1
  167. package/types/server/lifecycle/buildEnv.d.ts +2 -0
  168. package/types/server/lifecycle/loadBuildEnv.d.ts +1 -0
  169. package/types/server/logging/logStreamRoute.d.ts +1 -1
  170. package/types/server/logging/rotatingLogWriter.d.ts +3 -0
  171. package/types/server/machine/machineToken.d.ts +2 -0
  172. package/types/server/mcp/McpAuth.d.ts +2 -1
  173. package/types/server/mcp/McpExecutionContext.d.ts +1 -0
  174. package/types/server/oauth/OAuthRegistration.d.ts +2 -0
  175. package/types/server/ops/failureLimiter.d.ts +11 -0
  176. package/types/server/ops/opsRoute.d.ts +4 -2
  177. package/types/server/ops/opsToken.d.ts +2 -0
  178. package/types/server/ops/snapshotRestore.d.ts +3 -1
  179. package/types/server/ops/sqliteSnapshot.d.ts +7 -1
  180. package/types/server/ops/statusAccess.d.ts +8 -4
  181. package/types/server/proxy/hostBasePathWebProxy.d.ts +1 -0
  182. package/types/server/resolver/signal.resolver.d.ts +0 -1
  183. package/types/server/routeElementComposer.d.ts +10 -4
  184. package/types/server/routeShellGate.d.ts +12 -0
  185. package/types/server/routing/apiRouter.d.ts +6 -1
  186. package/types/server/routing/serverLimits.d.ts +23 -0
  187. package/types/server/routing/soloAppRoutes.d.ts +1 -1
  188. package/types/server/rscScrollRestoration.d.ts +1 -0
  189. package/types/server/ssrTypes.d.ts +5 -2
  190. package/types/server/staticPath.d.ts +1 -0
  191. package/types/server/systemPageDocument.d.ts +1 -0
  192. package/types/server/tunnel/TunnelAgent.d.ts +6 -0
  193. package/types/server/tunnel/TunnelDataSocket.d.ts +2 -0
  194. package/types/server/tunnel/TunnelTcpStream.d.ts +1 -1
  195. package/types/server/tunnel/tunnelStream.d.ts +1 -0
  196. package/types/server/types.d.ts +11 -2
  197. package/types/service/agent.service.d.ts +1 -1
  198. package/types/service/liveSyncHub.d.ts +1 -0
  199. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
  200. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +1 -0
  201. package/types/service/predefinedAdaptor/openaiDialect.d.ts +1 -1
  202. package/types/service/predefinedAdaptor/searchIndex.d.ts +5 -1
  203. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +1 -0
  204. package/types/service/predefinedAdaptor/sql/types.d.ts +3 -0
  205. package/types/service/types.d.ts +3 -0
  206. package/types/signal/SignalFailure.d.ts +1 -1
  207. package/types/signal/agentMeter.d.ts +4 -1
  208. package/types/signal/agentRelayPolicy.d.ts +12 -0
  209. package/types/signal/guard.d.ts +1 -1
  210. package/types/signal/roomKey.d.ts +8 -0
  211. package/types/signal/signalContext.d.ts +20 -6
  212. package/types/signal/slice.d.ts +1 -0
  213. package/types/signal/trace.d.ts +4 -0
  214. package/types/store/actionTag.d.ts +2 -0
  215. package/types/store/agent/ScreenReader.d.ts +2 -1
  216. package/types/store/agent/StoreCatalogue.d.ts +5 -0
  217. package/types/store/agentic/AgentConfirm.d.ts +15 -0
  218. package/types/store/agentic/StToolBuilder.d.ts +3 -1
  219. package/types/store/agentic/index.d.ts +1 -0
  220. package/types/store/deprecatedNames.d.ts +5 -0
  221. package/types/store/index.d.ts +1 -2
  222. package/types/store/sliceKeys.d.ts +12 -2
  223. package/types/ui/Agent/agentSessionOf.d.ts +2 -0
  224. package/types/ui/Agent/useAccountBoundSession.d.ts +2 -0
  225. package/types/ui/Agent/useScreenActions.d.ts +1 -1
  226. package/types/ui/Icon.d.ts +15 -0
  227. package/types/ui/animated.d.ts +3 -3
  228. package/types/ui/sliceNamesOf.d.ts +4 -4
  229. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +10 -0
  230. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +6 -0
  231. package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +6 -1
  232. package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +7 -0
  233. package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +2 -0
  234. package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +2 -0
  235. package/types/vendor/use-agentic/AgentSession.d.ts +6 -1
  236. package/types/vendor/use-agentic/ToolRunner.d.ts +7 -2
  237. package/types/vendor/use-agentic/types.d.ts +2 -0
  238. package/types/vendor/use-agentic/useAgentTool.d.ts +2 -1
  239. package/types/webkit/useCsrValues.d.ts +3 -3
  240. package/ui/Agent/Chat.tsx +2 -0
  241. package/ui/Agent/Zone.tsx +2 -0
  242. package/ui/Agent/agentSessionOf.ts +54 -5
  243. package/ui/Agent/useAccountBoundSession.ts +26 -0
  244. package/ui/Agent/useScreenActions.ts +1 -1
  245. package/ui/BottomSheet.tsx +1 -1
  246. package/ui/Button.tsx +5 -5
  247. package/ui/Data/Item.tsx +2 -8
  248. package/ui/Data/ListContainer.tsx +4 -4
  249. package/ui/Data/QueryMaker.tsx +3 -2
  250. package/ui/Data/RefPicker.tsx +3 -2
  251. package/ui/Data/UnlistedEditModal.tsx +2 -1
  252. package/ui/Data/ViewActions.tsx +4 -4
  253. package/ui/Data/dataExport.ts +3 -0
  254. package/ui/Data/useViewModal.ts +10 -15
  255. package/ui/Dialog/LegacyModal.tsx +1 -1
  256. package/ui/Dialog/Provider.tsx +2 -2
  257. package/ui/DragAction.tsx +1 -1
  258. package/ui/Dropdown.tsx +2 -2
  259. package/ui/Field/lightRefCache.ts +2 -2
  260. package/ui/Field/useRelationOptions.ts +3 -10
  261. package/ui/Icon.tsx +87 -0
  262. package/ui/KeyboardAvoiding.tsx +1 -1
  263. package/ui/Layout/Sider.tsx +3 -3
  264. package/ui/Link/Anchor.tsx +4 -1
  265. package/ui/Load/Edit.tsx +2 -2
  266. package/ui/Load/Edit_Client.tsx +3 -3
  267. package/ui/Load/Pagination.tsx +3 -3
  268. package/ui/Load/View.tsx +15 -14
  269. package/ui/Loading/ProgressBar.tsx +1 -1
  270. package/ui/Model/DraftBar.tsx +7 -14
  271. package/ui/Model/EditModal.tsx +9 -40
  272. package/ui/Model/EditWrapper.tsx +3 -5
  273. package/ui/Model/NewWrapper_Client.tsx +3 -7
  274. package/ui/Model/Remove.tsx +2 -2
  275. package/ui/Model/RemoveWrapper.tsx +2 -4
  276. package/ui/Model/SureToRemove.tsx +2 -2
  277. package/ui/Model/ViewEditModal.tsx +9 -20
  278. package/ui/Model/ViewModal.tsx +4 -11
  279. package/ui/Model/ViewWrapper.tsx +4 -6
  280. package/ui/ScreenNavigator.tsx +2 -2
  281. package/ui/Signal/WebSocket.tsx +2 -2
  282. package/ui/System/CSR.tsx +3 -3
  283. package/ui/System/Reconnect.tsx +2 -2
  284. package/ui/System/SSR.tsx +2 -2
  285. package/ui/System/SelectLanguage.tsx +1 -1
  286. package/ui/System/ThemeToggle.tsx +1 -1
  287. package/ui/Tab/Provider.tsx +1 -0
  288. package/ui/Toast.tsx +7 -15
  289. package/ui/index.ts +0 -1
  290. package/ui/sliceNamesOf.ts +25 -15
  291. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +2 -1
  292. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +10 -2
  293. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +4 -0
  294. package/vendor/@akanjs/native/native/desktop/src/acl.rs +166 -0
  295. package/vendor/@akanjs/native/native/desktop/src/lib.rs +25 -6
  296. package/vendor/@akanjs/native/native/desktop/src/navigation.rs +22 -10
  297. package/vendor/@akanjs/native/native/desktop/src/routes.rs +2 -2
  298. package/vendor/@akanjs/native/native/desktop/src/vectors.rs +28 -0
  299. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +1 -1
  300. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +10 -2
  301. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +4 -0
  302. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +1 -0
  303. package/vendor/@akanjs/native/packages/cli/src/config.ts +10 -0
  304. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +2 -0
  305. package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +5 -3
  306. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +4 -0
  307. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +2 -0
  308. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +30 -0
  309. package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +1 -1
  310. package/vendor/@akanjs/native/packages/core/src/kernel.ts +18 -2
  311. package/vendor/@akanjs/native/packages/core/vectors/navigation.json +22 -1
  312. package/vendor/@akanjs/native/packages/core/vectors/routes.json +4 -0
  313. package/vendor/@akanjs/native/packages/desktop/src/main.ts +5 -1
  314. package/vendor/@akanjs/native/packages/desktop/src/paths.ts +24 -1
  315. package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +4 -5
  316. package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +6 -8
  317. package/vendor/@akanjs/native/plugins/filesystem/src/desktop.ts +4 -2
  318. package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +22 -3
  319. package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +6 -0
  320. package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +6 -0
  321. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +106 -18
  322. package/vendor/use-agentic/AgentSession.ts +7 -2
  323. package/vendor/use-agentic/ToolRunner.ts +44 -5
  324. package/vendor/use-agentic/types.ts +2 -0
  325. package/vendor/use-agentic/useAgentTool.ts +4 -2
  326. package/webkit/McpAppAgent.ts +2 -1
  327. package/webkit/bootCsr.tsx +1 -1
  328. package/webkit/useCsrValues.ts +5 -1
  329. package/webkit/usePageTool.tsx +1 -0
  330. package/store/databaseStateNames.ts +0 -24
  331. package/store/formSetterNames.ts +0 -18
  332. package/types/store/databaseStateNames.d.ts +0 -18
  333. package/types/store/formSetterNames.d.ts +0 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.1.1",
3
+ "version": "3.1.2-rc.0",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -206,6 +206,11 @@
206
206
  "import": "./server/lifecycle/portInUse.ts",
207
207
  "default": "./server/lifecycle/portInUse.ts"
208
208
  },
209
+ "./server/lifecycle/buildEnv": {
210
+ "types": "./types/server/lifecycle/buildEnv.d.ts",
211
+ "import": "./server/lifecycle/buildEnv.ts",
212
+ "default": "./server/lifecycle/buildEnv.ts"
213
+ },
209
214
  "./server/artifact/routeSeedIndexStore": {
210
215
  "types": "./types/server/artifact/routeSeedIndexStore.d.ts",
211
216
  "import": "./server/artifact/routeSeedIndexStore.ts",
@@ -250,32 +255,31 @@
250
255
  "dependencies": {
251
256
  "@lexical/plain-text": "^0.52.0",
252
257
  "@noble/ciphers": "^2.4.0",
258
+ "@react-spring/web": "^10.1.2",
259
+ "@use-gesture/react": "^10.3.1",
253
260
  "dayjs": "^1.11.23",
254
261
  "immer": "^11.1.21",
255
262
  "lexical": "^0.52.0",
263
+ "react-refresh": "^0.19.0",
264
+ "scheduler": "^0.28.0",
256
265
  "tailwind-merge": "^3.7.0",
266
+ "tailwind-scrollbar": "^4.0.2",
257
267
  "tailwind-variants": "^3.3.1"
258
268
  },
259
269
  "peerDependencies": {
260
- "@happy-dom/global-registrator": "^21.0.3",
270
+ "@happy-dom/global-registrator": "^21.0.4",
261
271
  "@playwright/test": "^1.64.1",
262
- "@react-spring/web": "^10.1.2",
263
- "@use-gesture/react": "^10.3.1",
264
272
  "bullmq": "^5.81.5",
265
273
  "chance": "^1.1.13",
266
274
  "firebase": "^13.0.0",
267
- "happy-dom": "^21.0.3",
275
+ "happy-dom": "^21.0.4",
268
276
  "ioredis": "^5.11.1",
269
277
  "postgres": "^3.4.9",
270
278
  "protobufjs": "^8.8.1",
271
279
  "react": "19.3.1",
272
280
  "react-dom": "19.3.1",
273
281
  "react-icons": "^5.7.0",
274
- "react-refresh": "^0.19.0",
275
282
  "react-server-dom-webpack": "19.3.1",
276
- "react-spring": "^10.0.4",
277
- "scheduler": "^0.28.0",
278
- "tailwind-scrollbar": "^4.0.2",
279
283
  "typescript": "^6.0.3"
280
284
  },
281
285
  "peerDependenciesMeta": {
@@ -285,12 +289,6 @@
285
289
  "@playwright/test": {
286
290
  "optional": true
287
291
  },
288
- "@react-spring/web": {
289
- "optional": true
290
- },
291
- "@use-gesture/react": {
292
- "optional": true
293
- },
294
292
  "bullmq": {
295
293
  "optional": true
296
294
  },
@@ -321,21 +319,9 @@
321
319
  "react-icons": {
322
320
  "optional": true
323
321
  },
324
- "react-refresh": {
325
- "optional": true
326
- },
327
322
  "react-server-dom-webpack": {
328
323
  "optional": true
329
324
  },
330
- "react-spring": {
331
- "optional": true
332
- },
333
- "scheduler": {
334
- "optional": true
335
- },
336
- "tailwind-scrollbar": {
337
- "optional": true
338
- },
339
325
  "typescript": {
340
326
  "optional": true
341
327
  }
package/server/akanApp.ts CHANGED
@@ -1,14 +1,16 @@
1
1
 
2
+
3
+ import "./lifecycle/loadBuildEnv";
2
4
  import { mkdir, readdir, rm } from "node:fs/promises";
3
5
  import path from "node:path";
4
- import { Logger } from "akanjs/common";
6
+ import { Logger, TrustedProxy } from "akanjs/common";
5
7
  import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport, AkanUpstream } from "akanjs/service";
6
8
  import { getApiPrefix, getWsPrefix, normalizeRoutePrefix, resetEnvCache } from "../base/baseEnv";
7
9
  import { CrossSiteGuard } from "../signal/CrossSiteGuard";
8
10
  import { isTraceEnabled } from "../signal/trace";
9
11
  import { AKAN_CHILD_HOST, makeAkanChildProxyHeaders } from "./akanAppHeaders";
10
12
  import type { BuilderCsrReq, BuilderCsrRes, BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
11
- import { type AssetHeaderOptions, assetHeaders } from "./assetHeaders";
13
+ import { type AssetHeaderOptions, applySecurityHeaders, assetHeaders } from "./assetHeaders";
12
14
  import { compressResponse, compressStreamResponse, encodedFileResponse } from "./contentEncoding";
13
15
  import { isPortInUseError } from "./lifecycle/portInUse";
14
16
  import { resolveRuntimeDir } from "./lifecycle/runtimeDir";
@@ -24,6 +26,7 @@ import type { OpsRoute } from "./ops/opsRoute";
24
26
  import { StatusAccess } from "./ops/statusAccess";
25
27
  import { ProcessMetricsCollector } from "./processMetricsCollector";
26
28
  import { HostAllowlist } from "./routing/hostAllowlist";
29
+ import { ServerLimits } from "./routing/serverLimits";
27
30
  import { WsUpgrade } from "./routing/wsUpgrade";
28
31
  import { SelfExec } from "./selfExec";
29
32
  import { resolveStaticPath } from "./staticPath";
@@ -59,6 +62,9 @@ interface SoloServer {
59
62
  interface GatewayWsData {
60
63
  childIdx: number;
61
64
  upstream: WebSocket;
65
+ clientIp: string | null;
66
+ pending: (string | ArrayBuffer)[];
67
+ pendingBytes: number;
62
68
  }
63
69
 
64
70
  type GatewayUpstream = {
@@ -147,6 +153,16 @@ export class AkanApp {
147
153
  #logStream: LogStreamRoute | null = null;
148
154
  #ops: OpsRoute | null = null;
149
155
  #hostAllowlist = HostAllowlist.fromEnv();
156
+ readonly #limits = new ServerLimits();
157
+
158
+ static readonly #wsPendingLimitBytes = 1024 * 1024;
159
+ static readonly #wsUpstreamOpenTimeoutMs = 10_000;
160
+ readonly #wsCaps = {
161
+ total: AkanApp.#positiveIntEnv("AKAN_MAX_WEBSOCKETS"),
162
+ perIp: AkanApp.#positiveIntEnv("AKAN_MAX_WEBSOCKETS_PER_IP"),
163
+ };
164
+ #wsOpen = 0;
165
+ readonly #wsOpenByIp = new Map<string, number>();
150
166
  readonly #statusAccess = StatusAccess.fromEnv();
151
167
  static readonly #ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
152
168
  #gatewayMetrics: AkanMetricsReport = {};
@@ -600,12 +616,14 @@ export class AkanApp {
600
616
 
601
617
  #listen() {
602
618
  this.#server = Bun.serve({
603
- idleTimeout: 0,
619
+ idleTimeout: this.#limits.idleTimeout,
620
+ maxRequestBodySize: this.#limits.uploadBodyBytes,
604
621
  port: this.#port,
605
- hostname: process.env.AKAN_LISTEN_HOST || undefined,
606
- fetch: (req, server) => this.#handleFetch(req, server),
622
+ hostname: ServerLimits.listenHostname(),
623
+ fetch: (req, server) =>
624
+ ServerLimits.holdOpen(server, req, this.#limits.idleTimeout, () => this.#handleFetch(req, server)),
607
625
  websocket: {
608
- idleTimeout: 0,
626
+ idleTimeout: this.#limits.wsIdleTimeout,
609
627
  maxPayloadLength: 16 * 1024 * 1024,
610
628
  backpressureLimit: 1024 * 1024,
611
629
  closeOnBackpressureLimit: true,
@@ -620,19 +638,28 @@ export class AkanApp {
620
638
 
621
639
  async #handleFetch(req: Request, server: Bun.Server<GatewayWsData>): Promise<Response | undefined> {
622
640
  if (this.#hostAllowlist && !this.#hostAllowlist.allows(req)) return this.#hostAllowlist.refuse();
641
+ const oversized = this.#limits.refuseDeclared(req);
642
+ if (oversized) return oversized;
623
643
  const url = new URL(req.url);
624
- if (url.pathname === "/_akan/app/health") return this.#statusAccess.health(req, () => this.#getHealthStatus());
625
- if (url.pathname === "/_akan/app/metrics") return this.#statusAccess.metrics(req, () => this.#getMetricsStatus());
626
- if (url.pathname === LogStreamRoute.path && this.#logStream) return this.#logStream.handle(req);
644
+ if (url.pathname === "/_akan/app/health")
645
+ return this.#statusAccess.health(req, () => this.#getHealthStatus(), AkanApp.#clientOf(req, server));
646
+ if (url.pathname === "/_akan/app/metrics")
647
+ return this.#statusAccess.metrics(req, () => this.#getMetricsStatus(), AkanApp.#clientOf(req, server));
648
+ if (url.pathname === LogStreamRoute.path && this.#logStream)
649
+ return this.#logStream.handle(req, AkanApp.#clientOf(req, server));
627
650
  if (url.pathname === "/_akan/bench/ping") return new Response("ok");
628
651
  if (url.pathname === AppInfo.publicPath) return AppInfo.handlePublic();
629
- if (this.#ops?.matches(url.pathname)) return await this.#ops.handle(req);
652
+ if (this.#ops?.matches(url.pathname)) return await this.#ops.handle(req, AkanApp.#clientOf(req, server));
630
653
  if (this.#isWebSocketPath(url.pathname)) return this.#upgradeWebSocket(req, server);
631
654
  const assetResponse = await this.#serveImmutableArtifact(req, url);
632
655
  if (assetResponse) return assetResponse;
633
656
  return await this.#proxyHttp(req, server);
634
657
  }
635
658
 
659
+ static #clientOf(req: Request, server: Bun.Server<GatewayWsData>) {
660
+ return TrustedProxy.clientAddress(req.headers, server.requestIP(req)?.address);
661
+ }
662
+
636
663
  #isWebSocketPath(pathname: string) {
637
664
  return pathname === `${this.#prefix}${this.#websocketPrefix}` || pathname === "/_akan/hmr";
638
665
  }
@@ -672,40 +699,68 @@ export class AkanApp {
672
699
  return new Response("Forbidden", { status: 403 });
673
700
  }
674
701
  if (!WsUpgrade.isRequested(req)) return WsUpgrade.required();
702
+ const peer = server.requestIP(req) ?? undefined;
703
+ const clientIp = TrustedProxy.clientAddress(req.headers, peer?.address);
704
+ const refused = this.#refuseWebSocket(clientIp);
705
+ if (refused) return refused;
675
706
  const upstreamWs = new WebSocket(`ws://${upstream.host}:${upstream.port}${url.pathname}${url.search}`, {
676
- headers: makeAkanChildProxyHeaders(req, child.idx, server.requestIP(req)),
707
+ headers: makeAkanChildProxyHeaders(req, child.idx, peer),
677
708
  } as unknown as string[]);
678
709
 
679
- const upgraded = server.upgrade(req, { data: { childIdx: child.idx, upstream: upstreamWs } });
710
+ const upgraded = server.upgrade(req, {
711
+ data: { childIdx: child.idx, upstream: upstreamWs, clientIp, pending: [], pendingBytes: 0 },
712
+ });
680
713
  if (!upgraded) {
681
714
  upstreamWs.close();
682
715
  return new Response("WebSocket upgrade failed", { status: 500 });
683
716
  }
684
717
  child.metrics.activeWebSockets = (child.metrics.activeWebSockets ?? 0) + 1;
718
+ this.#wsOpen += 1;
719
+ if (clientIp) this.#wsOpenByIp.set(clientIp, (this.#wsOpenByIp.get(clientIp) ?? 0) + 1);
685
720
  return undefined;
686
721
  }
687
722
 
723
+ #refuseWebSocket(clientIp: string | null): Response | null {
724
+ const headers = { "cache-control": "no-store" };
725
+ if (this.#wsCaps.total && this.#wsOpen >= this.#wsCaps.total)
726
+ return new Response("Too many open websockets", { status: 503, headers });
727
+ if (this.#wsCaps.perIp && clientIp && (this.#wsOpenByIp.get(clientIp) ?? 0) >= this.#wsCaps.perIp)
728
+ return new Response("Too many open websockets from this address", { status: 429, headers });
729
+ return null;
730
+ }
731
+
732
+ static #positiveIntEnv(name: string): number | null {
733
+ const value = Number(process.env[name]);
734
+ return Number.isInteger(value) && value > 0 ? value : null;
735
+ }
736
+
688
737
  #handleWsOpen(ws: Bun.ServerWebSocket<GatewayWsData>) {
689
738
  const upstream = ws.data.upstream;
690
- const pending: (string | ArrayBuffer)[] = [];
739
+ const openTimer =
740
+ upstream.readyState === WebSocket.CONNECTING
741
+ ? setTimeout(() => {
742
+ if (upstream.readyState === WebSocket.OPEN) return;
743
+ upstream.close();
744
+ ws.close(1013, "upstream websocket did not open");
745
+ }, AkanApp.#wsUpstreamOpenTimeoutMs)
746
+ : null;
691
747
  upstream.addEventListener("open", () => {
692
- for (const message of pending.splice(0)) upstream.send(message);
748
+ if (openTimer) clearTimeout(openTimer);
749
+ for (const message of ws.data.pending.splice(0)) upstream.send(message);
750
+ ws.data.pendingBytes = 0;
693
751
  });
694
752
  upstream.addEventListener("message", (event) => {
695
753
  const result = ws.send(event.data as string | ArrayBuffer);
696
754
  if (result === 0) upstream.close();
697
755
  });
698
756
  upstream.addEventListener("close", (event) => {
757
+ if (openTimer) clearTimeout(openTimer);
699
758
  ws.close(relayableCloseCode(event.code), event.reason);
700
759
  });
701
760
  upstream.addEventListener("error", () => ws.close(1011, "upstream websocket error"));
702
- Object.assign(ws.data, { pending });
703
761
  }
704
762
 
705
- #handleWsMessage(
706
- ws: Bun.ServerWebSocket<GatewayWsData & { pending?: (string | ArrayBuffer)[] }>,
707
- message: string | ArrayBuffer | Uint8Array,
708
- ) {
763
+ #handleWsMessage(ws: Bun.ServerWebSocket<GatewayWsData>, message: string | ArrayBuffer | Uint8Array) {
709
764
  const upstream = ws.data.upstream;
710
765
  const payload =
711
766
  typeof message === "string"
@@ -713,14 +768,29 @@ export class AkanApp {
713
768
  : message instanceof ArrayBuffer
714
769
  ? message
715
770
  : new Uint8Array(message).slice().buffer;
716
- if (upstream.readyState === WebSocket.OPEN) upstream.send(payload);
717
- else ws.data.pending?.push(payload as string | ArrayBuffer);
771
+ if (upstream.readyState === WebSocket.OPEN) {
772
+ upstream.send(payload);
773
+ return;
774
+ }
775
+ const size = typeof payload === "string" ? Buffer.byteLength(payload) : payload.byteLength;
776
+ if (ws.data.pendingBytes + size > AkanApp.#wsPendingLimitBytes) {
777
+ ws.data.pending.length = 0;
778
+ ws.close(1009, "too much data before the upstream opened");
779
+ return;
780
+ }
781
+ ws.data.pending.push(payload as string | ArrayBuffer);
782
+ ws.data.pendingBytes += size;
718
783
  }
719
784
 
720
785
  #handleWsClose(ws: Bun.ServerWebSocket<GatewayWsData>, code: number, reason: string) {
721
786
  ws.data.upstream.close(relayableCloseCode(code), reason);
722
787
  const child = this.#children.get(ws.data.childIdx);
723
788
  if (child) child.metrics.activeWebSockets = Math.max(0, (child.metrics.activeWebSockets ?? 1) - 1);
789
+ this.#wsOpen = Math.max(0, this.#wsOpen - 1);
790
+ const { clientIp } = ws.data;
791
+ const remaining = clientIp ? (this.#wsOpenByIp.get(clientIp) ?? 1) - 1 : 0;
792
+ if (clientIp && remaining > 0) this.#wsOpenByIp.set(clientIp, remaining);
793
+ else if (clientIp) this.#wsOpenByIp.delete(clientIp);
724
794
  }
725
795
 
726
796
  #getHealthStatus() {
@@ -779,23 +849,30 @@ export class AkanApp {
779
849
  const { socketPath } = child.upstream;
780
850
  const url = new URL(req.url);
781
851
  const upstreamUrl = `http://akan-child${url.pathname}${url.search}`;
782
- const headers = makeAkanChildProxyHeaders(req, child.idx, server.requestIP(req));
852
+ const headers = makeAkanChildProxyHeaders(req, child.idx, server.requestIP(req) ?? undefined);
783
853
  child.metrics.activeRequests = (child.metrics.activeRequests ?? 0) + 1;
784
854
  child.metrics.totalRequests = (child.metrics.totalRequests ?? 0) + 1;
785
855
  const traced = isTraceEnabled();
786
856
  const hopStart = traced ? performance.now() : 0;
857
+ const chunkedLimit = req.headers.has("content-length") ? null : this.#limits.bodyLimitOf(req);
787
858
  try {
788
859
  const upstreamRes = await fetch(upstreamUrl, {
789
860
  unix: socketPath,
790
861
  method: req.method,
791
862
  headers,
792
- body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body,
863
+ body:
864
+ req.method === "GET" || req.method === "HEAD"
865
+ ? undefined
866
+ : chunkedLimit !== null && req.body
867
+ ? ServerLimits.limitBody(req.body, chunkedLimit)
868
+ : req.body,
793
869
  signal: req.signal,
794
870
  redirect: "manual",
795
871
  decompress: false,
796
872
  });
797
873
  return await this.#proxyResponse(req, upstreamRes);
798
874
  } catch (error) {
875
+ if (ServerLimits.isPayloadTooLarge(error)) return ServerLimits.payloadTooLarge();
799
876
  if (AkanApp.#isUpstreamOpenFailure(error)) {
800
877
  this.logger.error(`Child ${child.idx}/${child.role} upstream is unreachable (${socketPath}); restarting`);
801
878
  this.#scheduleChildRestart(child, child.proc, "upstream-open-failed");
@@ -844,13 +921,10 @@ export class AkanApp {
844
921
  detail,
845
922
  note: "The replica is restarting — this page reloads itself as soon as it answers.",
846
923
  }),
847
- { status: 502, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } },
924
+ { status: 502, headers: AkanApp.#statusHeaders("text/html; charset=utf-8") },
848
925
  );
849
926
  }
850
- return new Response(detail, {
851
- status: 502,
852
- headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" },
853
- });
927
+ return new Response(detail, { status: 502, headers: AkanApp.#statusHeaders("text/plain; charset=utf-8") });
854
928
  }
855
929
 
856
930
  async #pickReadyFederationChild(req: Request): Promise<ChildState | null> {
@@ -895,7 +969,7 @@ export class AkanApp {
895
969
  if (req.headers.get("accept")?.includes("text/html")) {
896
970
  return new Response(AkanApp.#statusPageHtml(page), {
897
971
  status: 503,
898
- headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
972
+ headers: AkanApp.#statusHeaders("text/html; charset=utf-8"),
899
973
  });
900
974
  }
901
975
  return AkanApp.#unavailableResponse(req, page.text);
@@ -910,6 +984,11 @@ export class AkanApp {
910
984
  );
911
985
  }
912
986
 
987
+ static #statusHeaders(contentType: string): Headers {
988
+ const headers = new Headers({ "content-type": contentType, "cache-control": "no-store" });
989
+ return applySecurityHeaders(headers, { html: contentType.startsWith("text/html") });
990
+ }
991
+
913
992
  static #statusPageHtml({ heading, detail, note }: { heading: string; detail: string; note: string }) {
914
993
  return `<!doctype html>
915
994
  <html>
@@ -49,6 +49,7 @@ import type { AppWsData } from "./routing/appWsData";
49
49
  import { HostAllowlist } from "./routing/hostAllowlist";
50
50
  import { RoomReplay } from "./routing/roomReplay";
51
51
  import { RouteHosts } from "./routing/routeHosts";
52
+ import { ServerLimits } from "./routing/serverLimits";
52
53
  import { createSoloAppRoutes } from "./routing/soloAppRoutes";
53
54
  import {
54
55
  getWebConfigFromEnv,
@@ -443,6 +444,7 @@ export class AkanServer {
443
444
  this.logger.verbose(`${this.name} is serving on ${unix ? `unix://${unix}` : `port ${port}`}`);
444
445
  const { routes, routeOptions, wsRoutes, builtinRoutes, renderEnvRoutes, hmrHub, webRouter, webProxyRunner } =
445
446
  this.#prepared;
447
+ const limits = new ServerLimits();
446
448
  const websocketHandlers = {
447
449
  ...ApiRouter.buildWebsocketHandlers({
448
450
  wsRoutes,
@@ -459,11 +461,14 @@ export class AkanServer {
459
461
  logger: this.logger,
460
462
  onDrain: () => this.#binaryPubsub.flush(),
461
463
  }),
464
+ idleTimeout: limits.wsIdleTimeout,
462
465
  data: {},
463
466
  } as Bun.WebSocketHandler<AppWsData | HmrWsData>;
464
467
 
465
468
  const hostAllowlist = unix ? null : HostAllowlist.fromEnv();
466
- const hostname = process.env.AKAN_LISTEN_HOST || undefined;
469
+ const hostname = ServerLimits.listenHostname();
470
+
471
+ const idleTimeout = unix ? 0 : limits.idleTimeout;
467
472
  const buildRoutes = (upgradeAppWs: ApiRouteInputs["upgradeAppWs"], allowlist: HostAllowlist | null) =>
468
473
  ApiRouter.buildRoutes({
469
474
  prefix: this.prefix,
@@ -475,13 +480,19 @@ export class AkanServer {
475
480
  upgradeAppWs,
476
481
  webProxyRunner,
477
482
  hostAllowlist: allowlist,
483
+ bodyLimits: limits,
484
+ countChunkedBodies: !unix,
485
+ idleTimeout,
478
486
  });
479
- this.#server = Bun.serve({
480
- idleTimeout: 0,
481
- ...(unix ? { unix } : { port, hostname }),
482
- routes: buildRoutes((req, data) => this.#server?.upgrade(req, { data }) ?? false, hostAllowlist),
483
- websocket: websocketHandlers,
484
- } as Parameters<typeof Bun.serve>[0]);
487
+ const serve = () =>
488
+ Bun.serve({
489
+ idleTimeout,
490
+ maxRequestBodySize: limits.uploadBodyBytes,
491
+ ...(unix ? { unix } : { port, hostname }),
492
+ routes: buildRoutes((req, data) => this.#server?.upgrade(req, { data }) ?? false, hostAllowlist),
493
+ websocket: websocketHandlers,
494
+ } as Parameters<typeof Bun.serve>[0]);
495
+ this.#server = unix ? ServerLimits.serveOwnerOnly(unix, serve) : serve();
485
496
  if (unix && process.env.AKAN_CHILD_WS_PORT) {
486
497
  const preferredWsPort = Number(process.env.AKAN_CHILD_WS_PORT);
487
498
 
@@ -7,10 +7,29 @@ export interface AssetHeaderOptions {
7
7
  export const applySecurityHeaders = (headers: Headers, { html = false } = {}): Headers => {
8
8
  headers.set("X-Content-Type-Options", "nosniff");
9
9
  headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
10
- if (html) headers.set("X-Frame-Options", "SAMEORIGIN");
10
+ if (!html) return headers;
11
+ headers.set("X-Frame-Options", "SAMEORIGIN");
12
+ const policy = htmlContentSecurityPolicy();
13
+ if (policy) headers.set("Content-Security-Policy", policy);
11
14
  return headers;
12
15
  };
13
16
 
17
+ export const isOff = (value: string) => ["off", "false", "0", "none"].includes(value.toLowerCase());
18
+
19
+ export const DEFAULT_HTML_CONTENT_SECURITY_POLICY = "frame-ancestors 'self'; base-uri 'self'";
20
+
21
+ export const htmlContentSecurityPolicy = (raw = process.env.AKAN_CONTENT_SECURITY_POLICY?.trim()): string | null => {
22
+ if (!raw || /[\r\n]/.test(raw)) return DEFAULT_HTML_CONTENT_SECURITY_POLICY;
23
+ return isOff(raw) ? null : raw;
24
+ };
25
+
26
+ export const DEFAULT_STRICT_TRANSPORT_SECURITY = "max-age=31536000";
27
+
28
+ export const strictTransportSecurity = (raw = process.env.AKAN_HSTS?.trim()): string | null => {
29
+ if (!raw || /[\r\n]/.test(raw)) return DEFAULT_STRICT_TRANSPORT_SECURITY;
30
+ return isOff(raw) ? null : raw;
31
+ };
32
+
14
33
  export const assetHeaders = ({ contentType, cacheControl, anyOrigin }: AssetHeaderOptions): Headers => {
15
34
  const headers = new Headers({ "Content-Type": contentType });
16
35
  if (cacheControl) headers.set("Cache-Control", cacheControl);
@@ -1,5 +1,6 @@
1
1
  import { CookieJar, originFromRequest } from "akanjs/common";
2
2
  import type { AkanDynamicUsage, AkanRequestPolicy } from "akanjs/fetch";
3
+ import { isOff } from "./assetHeaders";
3
4
 
4
5
  export const DEFAULT_ROUTE_CACHE_TTL_SECONDS = 30;
5
6
 
@@ -88,14 +89,26 @@ export function getClientFacingOrigin(request: Request, url = new URL(request.ur
88
89
  return originFromRequest(request.headers, url);
89
90
  }
90
91
 
91
- export function isPublicRouteCacheableRequest(request: Request): boolean {
92
- if (request.method !== "GET") return false;
93
- if (request.headers.has("authorization")) return false;
92
+ export function carriesCredentials(request: Request): boolean {
93
+ if (request.headers.has("authorization")) return true;
94
94
  const cookie = request.headers.get("cookie");
95
- if (!cookie) return true;
95
+ if (!cookie) return false;
96
96
  return CookieJar.parse(cookie)
97
97
  .names()
98
- .every((name) => name === "theme");
98
+ .some((name) => name !== "theme");
99
+ }
100
+
101
+ export function isPublicRouteCacheableRequest(request: Request): boolean {
102
+ return request.method === "GET" && !carriesCredentials(request);
103
+ }
104
+
105
+ export function resolveHtmlCacheControl(
106
+ request: Request,
107
+ raw = process.env.AKAN_HTML_CACHE_CONTROL?.trim(),
108
+ ): string | null {
109
+ if (raw && isOff(raw)) return null;
110
+ if (carriesCredentials(request)) return "private, no-cache";
111
+ return raw && !/[\r\n]/.test(raw) ? raw : "no-cache";
99
112
  }
100
113
 
101
114
  const isAtOrUnder = (value: string, prefix: string) =>
@@ -1,4 +1,5 @@
1
1
  import { type BaseEnv, INJECT_META } from "akanjs/base";
2
+ import { modelNames } from "akanjs/common";
2
3
  import type { AdaptorCls, InjectInfo, ServiceCls } from "akanjs/service";
3
4
  import type { DiLifecycle } from "../di/diLifecycle";
4
5
  import type { DepEdge, DepEdgeKind, DepNode, DepNodeKind, DepsData } from "./types";
@@ -126,7 +127,7 @@ export class DepsSerializer {
126
127
  const kind = injectInfo.type as DepEdgeKind;
127
128
  switch (kind) {
128
129
  case "database":
129
- return { from, to: this.#node("adaptor", `${injectInfo.parentRefName}Model`), kind, prop };
130
+ return { from, to: this.#node("adaptor", modelNames(injectInfo.parentRefName).modelModel), kind, prop };
130
131
  case "service":
131
132
  return prop.endsWith("Service")
132
133
  ? { from, to: this.#node("service", prop.slice(0, -"Service".length)), kind, prop }
@@ -1,6 +1,7 @@
1
1
  import type { BaseEnv } from "akanjs/base";
2
2
  import { Logger } from "akanjs/common";
3
3
  import { DictionaryRegistry, type RootDictionary } from "akanjs/dictionary";
4
+ import { isLocalDevRuntime } from "../../signal/trace";
4
5
  import type { DiLifecycle } from "../di/diLifecycle";
5
6
  import type { HttpRoutes } from "../types";
6
7
  import { ConstantSerializer } from "./constantSerializer";
@@ -24,12 +25,12 @@ export class DevtoolsRouter {
24
25
  static readonly basePath = "/_akan";
25
26
  static readonly logger = new Logger("DevtoolsRouter");
26
27
 
27
- /** Local-only unless `AKAN_DEVTOOLS` ("true"/"1", "false"/"0") overrides it, e.g. to debug a deployed env. */
28
+ /** A local dev runtime only, unless `AKAN_DEVTOOLS` ("true"/"1", "false"/"0") overrides it, e.g. to debug a deployed env. */
28
29
  static isEnabled(env: BaseEnv): boolean {
29
30
  const override = process.env.AKAN_DEVTOOLS;
30
31
  if (override === "false" || override === "0") return false;
31
32
  if (override === "true" || override === "1") return true;
32
- return env.environment === "local";
33
+ return isLocalDevRuntime(env);
33
34
  }
34
35
 
35
36
  readonly #context: DevtoolsRouterContext;
@@ -6,7 +6,7 @@ import {
6
6
  type PrimitiveScalar,
7
7
  SLICE_META,
8
8
  } from "akanjs/base";
9
- import { capitalize } from "akanjs/common";
9
+ import { sliceNames } from "akanjs/common";
10
10
  import { ConstantRegistry, type ConstantType } from "akanjs/constant";
11
11
  import type {
12
12
  ArgInfo,
@@ -148,10 +148,10 @@ export class SignalSerializer {
148
148
  static #sliceDerivedKeys(refName: string, sliceCls: SliceCls | undefined): Set<string> {
149
149
  const sliceMeta = (sliceCls?.[SLICE_META] ?? {}) as Record<string, SliceInfo>;
150
150
  return new Set(
151
- Object.keys(sliceMeta).flatMap((key) => [
152
- `${refName}List${capitalize(key)}`,
153
- `${refName}Insight${capitalize(key)}`,
154
- ]),
151
+ Object.keys(sliceMeta).flatMap((key) => {
152
+ const { list, insight } = sliceNames(refName, key).endpoint;
153
+ return [list, insight];
154
+ }),
155
155
  );
156
156
  }
157
157
 
@@ -15,7 +15,8 @@ export class ClientReport {
15
15
  static readonly #fieldLimit = 4_000;
16
16
  static readonly #stackLines = 6;
17
17
 
18
- static readonly #controlChars = /[\u0000-\u0008\u000b-\u001f\u007f]/g;
18
+ static readonly #controlChars = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
19
+ static readonly #lineBreaks = /[\n\u2028\u2029]/g;
19
20
 
20
21
  static parse(value: unknown): HmrClientReport | null {
21
22
  if (!value || typeof value !== "object") return null;
@@ -27,12 +28,12 @@ export class ClientReport {
27
28
  return {
28
29
  type: "client-report",
29
30
  kind: report.kind,
30
- message: ClientReport.#clean(report.message),
31
- href: ClientReport.#clean(report.href),
31
+ message: ClientReport.#line(report.message),
32
+ href: ClientReport.#line(report.href),
32
33
  client: report.client,
33
34
  ...(typeof report.stack === "string" && report.stack ? { stack: ClientReport.#clean(report.stack) } : {}),
34
- ...(typeof report.platform === "string" ? { platform: ClientReport.#clean(report.platform) } : {}),
35
- ...(typeof report.target === "string" ? { target: ClientReport.#clean(report.target) } : {}),
35
+ ...(typeof report.platform === "string" ? { platform: ClientReport.#line(report.platform) } : {}),
36
+ ...(typeof report.target === "string" ? { target: ClientReport.#line(report.target) } : {}),
36
37
  };
37
38
  }
38
39
 
@@ -67,4 +68,8 @@ export class ClientReport {
67
68
  static #clean(text: string) {
68
69
  return Logger.stripAnsi(text).replace(ClientReport.#controlChars, "").slice(0, ClientReport.#fieldLimit);
69
70
  }
71
+
72
+ static #line(text: string) {
73
+ return ClientReport.#clean(text).replace(ClientReport.#lineBreaks, "\\n");
74
+ }
70
75
  }