akanjs 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (388) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/base/symbols.ts +4 -0
  4. package/client/clientRuntime.ts +8 -0
  5. package/client/cn.ts +7 -0
  6. package/client/csrTypes.ts +2 -0
  7. package/client/frameDebug.ts +3 -2
  8. package/common/Logger.ts +4 -0
  9. package/common/deepObjectify.ts +21 -7
  10. package/common/index.ts +14 -0
  11. package/common/mcpExposure.ts +99 -0
  12. package/common/pathSet.ts +17 -5
  13. package/common/routeConvention.ts +30 -0
  14. package/constant/crystalize.ts +3 -1
  15. package/constant/fieldInfo.ts +6 -0
  16. package/constant/getDefault.ts +37 -9
  17. package/constant/immerify.ts +1 -1
  18. package/constant/index.ts +2 -0
  19. package/constant/labelOf.ts +17 -0
  20. package/constant/mask.ts +60 -0
  21. package/constant/purify.ts +17 -11
  22. package/constant/types.ts +14 -12
  23. package/dictionary/DictionaryLookup.ts +30 -0
  24. package/dictionary/agent.dictionary.ts +31 -0
  25. package/dictionary/agentTurn.dictionary.ts +29 -0
  26. package/dictionary/base.dictionary.ts +8 -0
  27. package/dictionary/dictInfo.ts +8 -0
  28. package/dictionary/dictionary.ts +9 -3
  29. package/dictionary/index.ts +1 -0
  30. package/document/database.ts +21 -0
  31. package/document/filterMeta.ts +7 -0
  32. package/document/index.ts +1 -0
  33. package/document/into.ts +7 -1
  34. package/document/noDocumentError.ts +12 -0
  35. package/fetch/agentTurn.ts +19 -0
  36. package/fetch/client/fetchClient.ts +71 -13
  37. package/fetch/client/wsClient.ts +17 -2
  38. package/fetch/fetchType/endpointFetch.type.ts +10 -4
  39. package/fetch/index.ts +1 -0
  40. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  41. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  42. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  43. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
  44. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  45. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
  46. package/package.json +11 -9
  47. package/server/akanApp.ts +30 -34
  48. package/server/akanOption.ts +65 -7
  49. package/server/akanServer.ts +172 -7
  50. package/server/animatedImage.ts +83 -0
  51. package/server/artifact/routeClientCache.ts +14 -0
  52. package/server/assetEncoding.ts +47 -0
  53. package/server/devtools/signalSerializer.ts +2 -2
  54. package/server/devtools/types.ts +2 -2
  55. package/server/di/diLifecycle.ts +31 -3
  56. package/server/di/predefinedAdaptor.ts +6 -0
  57. package/server/imageOptimizer.ts +33 -46
  58. package/server/index.ts +1 -2
  59. package/server/mcp/McpAuth.ts +177 -0
  60. package/server/mcp/McpDispatcher.ts +245 -0
  61. package/server/mcp/McpEventStream.ts +76 -0
  62. package/server/mcp/McpExecutionContext.ts +105 -0
  63. package/server/mcp/McpRouter.ts +591 -0
  64. package/server/mcp/index.ts +5 -0
  65. package/server/resolver/CascadeRunner.ts +1 -1
  66. package/server/resolver/database.resolver.ts +42 -5
  67. package/server/resolver/service.resolver.ts +29 -0
  68. package/server/resolver/signal.resolver.ts +16 -1
  69. package/server/routeTreeBuilder.ts +2 -44
  70. package/server/rscWorker.tsx +6 -2
  71. package/server/subRouteIndexDocument.tsx +61 -0
  72. package/server/systemPageDocument.tsx +2 -2
  73. package/server/systemPages.tsx +49 -16
  74. package/server/vendor/akanjs-fetch.ts +1 -0
  75. package/server/webRouter.ts +159 -35
  76. package/service/agent.service.ts +14 -0
  77. package/service/base.service.ts +2 -1
  78. package/service/index.ts +1 -0
  79. package/service/predefinedAdaptor/database.adaptor.ts +78 -24
  80. package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
  81. package/service/predefinedAdaptor/index.ts +3 -0
  82. package/service/predefinedAdaptor/insightQuery.ts +183 -0
  83. package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
  84. package/service/predefinedAdaptor/role.adaptor.ts +2 -0
  85. package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
  86. package/service/serviceModule.ts +17 -0
  87. package/service/types.ts +4 -0
  88. package/signal/agent/AgentCatalogue.ts +165 -0
  89. package/signal/agent/index.ts +1 -0
  90. package/signal/agent.signal.ts +42 -0
  91. package/signal/agentTurn.ts +8 -0
  92. package/signal/agentTurnStream.ts +48 -0
  93. package/signal/base.signal.ts +2 -1
  94. package/signal/endpointInfo.ts +38 -2
  95. package/signal/guard.ts +13 -1
  96. package/signal/guards.ts +52 -1
  97. package/signal/index.ts +6 -0
  98. package/signal/mcp/McpDocument.ts +286 -0
  99. package/signal/mcp/McpProgress.ts +106 -0
  100. package/signal/mcp/McpUriTemplate.ts +85 -0
  101. package/signal/mcp/Msg.ts +368 -0
  102. package/signal/mcp/index.ts +5 -0
  103. package/signal/mcp/mcpProtocol.ts +120 -0
  104. package/signal/middleware.ts +7 -3
  105. package/signal/openapi/openapi.ts +32 -171
  106. package/signal/schema/JsonSchemaBuilder.ts +211 -0
  107. package/signal/schema/index.ts +1 -0
  108. package/signal/signalContext.ts +85 -9
  109. package/signal/slice.ts +9 -9
  110. package/signal/types.ts +1 -1
  111. package/store/action.ts +11 -22
  112. package/store/actionTag.ts +33 -0
  113. package/store/agent/AgentBridge.ts +323 -0
  114. package/store/agent/AgentContext.ts +74 -0
  115. package/store/agent/AgentPrompts.ts +82 -0
  116. package/store/agent/AgentVisibility.ts +68 -0
  117. package/store/agent/ScreenReader.ts +208 -0
  118. package/store/agent/StoreCatalogue.ts +351 -0
  119. package/store/agent/StoreSurfaceSource.ts +122 -0
  120. package/store/agent/index.ts +9 -0
  121. package/store/agent/storeSurface.ts +28 -0
  122. package/store/agent/types.ts +50 -0
  123. package/store/agentic/StToolBuilder.ts +199 -0
  124. package/store/agentic/attachAgentic.ts +22 -0
  125. package/store/agentic/index.ts +5 -0
  126. package/store/agentic/readableValue.ts +17 -0
  127. package/store/agentic/useStExpose.ts +18 -0
  128. package/store/agentic/useStState.ts +35 -0
  129. package/store/baseSt.ts +7 -0
  130. package/store/databaseStateNames.ts +31 -0
  131. package/store/formSetterNames.ts +21 -0
  132. package/store/index.ts +8 -0
  133. package/store/rootStore.ts +3 -1
  134. package/store/sliceRole.ts +36 -0
  135. package/store/state.ts +2 -12
  136. package/store/store.ts +19 -1
  137. package/store/storeInstance.ts +165 -20
  138. package/store/storeRegistry.ts +16 -1
  139. package/store/types.ts +18 -0
  140. package/store/withSelector.ts +7 -8
  141. package/test/registerDom.ts +4 -0
  142. package/types/base/symbols.d.ts +4 -0
  143. package/types/client/cn.d.ts +5 -0
  144. package/types/client/csrTypes.d.ts +2 -0
  145. package/types/common/Logger.d.ts +2 -0
  146. package/types/common/deepObjectify.d.ts +4 -2
  147. package/types/common/index.d.ts +2 -1
  148. package/types/common/mcpExposure.d.ts +60 -0
  149. package/types/common/routeConvention.d.ts +8 -0
  150. package/types/constant/fieldInfo.d.ts +1 -0
  151. package/types/constant/getDefault.d.ts +5 -0
  152. package/types/constant/index.d.ts +2 -0
  153. package/types/constant/labelOf.d.ts +6 -0
  154. package/types/constant/mask.d.ts +34 -0
  155. package/types/constant/purify.d.ts +2 -2
  156. package/types/constant/types.d.ts +2 -2
  157. package/types/dictionary/DictionaryLookup.d.ts +14 -0
  158. package/types/dictionary/agent.dictionary.d.ts +1 -0
  159. package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
  160. package/types/dictionary/base.dictionary.d.ts +1 -1
  161. package/types/dictionary/dictInfo.d.ts +6 -0
  162. package/types/dictionary/dictionary.d.ts +11 -9
  163. package/types/dictionary/index.d.ts +1 -0
  164. package/types/document/database.d.ts +20 -0
  165. package/types/document/filterMeta.d.ts +1 -0
  166. package/types/document/index.d.ts +1 -0
  167. package/types/document/into.d.ts +6 -1
  168. package/types/document/noDocumentError.d.ts +12 -0
  169. package/types/fetch/agentTurn.d.ts +593 -0
  170. package/types/fetch/client/fetchClient.d.ts +10 -0
  171. package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
  172. package/types/fetch/index.d.ts +1 -0
  173. package/types/server/akanOption.d.ts +35 -7
  174. package/types/server/akanServer.d.ts +44 -1
  175. package/types/server/animatedImage.d.ts +7 -0
  176. package/types/server/artifact/routeClientCache.d.ts +6 -0
  177. package/types/server/assetEncoding.d.ts +7 -0
  178. package/types/server/devtools/types.d.ts +2 -2
  179. package/types/server/di/predefinedAdaptor.d.ts +4 -1
  180. package/types/server/index.d.ts +0 -2
  181. package/types/server/mcp/McpAuth.d.ts +62 -0
  182. package/types/server/mcp/McpDispatcher.d.ts +54 -0
  183. package/types/server/mcp/McpEventStream.d.ts +18 -0
  184. package/types/server/mcp/McpExecutionContext.d.ts +44 -0
  185. package/types/server/mcp/McpRouter.d.ts +60 -0
  186. package/types/server/mcp/index.d.ts +5 -0
  187. package/types/server/mcp.d.ts +1 -0
  188. package/types/server/subRouteIndexDocument.d.ts +8 -0
  189. package/types/server/systemPageDocument.d.ts +1 -0
  190. package/types/server/systemPages.d.ts +5 -0
  191. package/types/server/vendor/akanjs-fetch.d.ts +1 -0
  192. package/types/service/agent.service.d.ts +12 -0
  193. package/types/service/base.service.d.ts +4 -0
  194. package/types/service/index.d.ts +1 -0
  195. package/types/service/predefinedAdaptor/database.adaptor.d.ts +24 -2
  196. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
  197. package/types/service/predefinedAdaptor/index.d.ts +3 -0
  198. package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
  199. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
  200. package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
  201. package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
  202. package/types/service/types.d.ts +4 -1
  203. package/types/signal/agent/AgentCatalogue.d.ts +100 -0
  204. package/types/signal/agent/index.d.ts +1 -0
  205. package/types/signal/agent.d.ts +1 -0
  206. package/types/signal/agent.signal.d.ts +29 -0
  207. package/types/signal/agentTurn.d.ts +9 -0
  208. package/types/signal/agentTurnStream.d.ts +16 -0
  209. package/types/signal/base.signal.d.ts +1 -1
  210. package/types/signal/endpointInfo.d.ts +21 -3
  211. package/types/signal/guard.d.ts +11 -0
  212. package/types/signal/guards.d.ts +22 -1
  213. package/types/signal/index.d.ts +6 -0
  214. package/types/signal/mcp/McpDocument.d.ts +65 -0
  215. package/types/signal/mcp/McpProgress.d.ts +40 -0
  216. package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
  217. package/types/signal/mcp/Msg.d.ts +143 -0
  218. package/types/signal/mcp/index.d.ts +5 -0
  219. package/types/signal/mcp/mcpProtocol.d.ts +107 -0
  220. package/types/signal/mcp.d.ts +1 -0
  221. package/types/signal/openapi/openapi.d.ts +3 -3
  222. package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
  223. package/types/signal/schema/index.d.ts +1 -0
  224. package/types/signal/schema.d.ts +1 -0
  225. package/types/signal/signalContext.d.ts +16 -1
  226. package/types/signal/slice.d.ts +8 -8
  227. package/types/signal/types.d.ts +1 -1
  228. package/types/store/actionTag.d.ts +21 -0
  229. package/types/store/agent/AgentBridge.d.ts +77 -0
  230. package/types/store/agent/AgentContext.d.ts +16 -0
  231. package/types/store/agent/AgentPrompts.d.ts +31 -0
  232. package/types/store/agent/AgentVisibility.d.ts +21 -0
  233. package/types/store/agent/ScreenReader.d.ts +11 -0
  234. package/types/store/agent/StoreCatalogue.d.ts +26 -0
  235. package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
  236. package/types/store/agent/index.d.ts +9 -0
  237. package/types/store/agent/storeSurface.d.ts +14 -0
  238. package/types/store/agent/types.d.ts +49 -0
  239. package/types/store/agent.d.ts +1 -0
  240. package/types/store/agentic/StToolBuilder.d.ts +41 -0
  241. package/types/store/agentic/attachAgentic.d.ts +14 -0
  242. package/types/store/agentic/index.d.ts +5 -0
  243. package/types/store/agentic/readableValue.d.ts +3 -0
  244. package/types/store/agentic/useStExpose.d.ts +8 -0
  245. package/types/store/agentic/useStState.d.ts +12 -0
  246. package/types/store/agentic.d.ts +1 -0
  247. package/types/store/baseSt.d.ts +29 -22
  248. package/types/store/databaseStateNames.d.ts +25 -0
  249. package/types/store/formSetterNames.d.ts +16 -0
  250. package/types/store/index.d.ts +7 -0
  251. package/types/store/rootStore.d.ts +5 -1
  252. package/types/store/sliceRole.d.ts +25 -0
  253. package/types/store/store.d.ts +7 -2
  254. package/types/store/storeInstance.d.ts +33 -1
  255. package/types/store/storeRegistry.d.ts +5 -0
  256. package/types/store/types.d.ts +15 -0
  257. package/types/store/withSelector.d.ts +7 -8
  258. package/types/test/registerDom.d.ts +1 -0
  259. package/types/ui/Agent/Approval.d.ts +7 -0
  260. package/types/ui/Agent/Bubble.d.ts +13 -0
  261. package/types/ui/Agent/Chat.d.ts +24 -0
  262. package/types/ui/Agent/Context.d.ts +9 -0
  263. package/types/ui/Agent/Dock.d.ts +16 -0
  264. package/types/ui/Agent/Guide.d.ts +9 -0
  265. package/types/ui/Agent/Section.d.ts +10 -0
  266. package/types/ui/Agent/StateKey.d.ts +16 -0
  267. package/types/ui/Agent/Tool.d.ts +15 -0
  268. package/types/ui/Agent/Transcript.d.ts +13 -0
  269. package/types/ui/Agent/Zone.d.ts +23 -0
  270. package/types/ui/Agent/fetchRunner.d.ts +12 -0
  271. package/types/ui/Agent/index.d.ts +17 -0
  272. package/types/ui/Agent/index_.d.ts +1 -0
  273. package/types/ui/Agent/sessionHistory.d.ts +12 -0
  274. package/types/ui/Agent.d.ts +1 -0
  275. package/types/ui/Badge.d.ts +1 -1
  276. package/types/ui/Button.d.ts +1 -1
  277. package/types/ui/CsrImage.d.ts +1 -1
  278. package/types/ui/Dropdown.d.ts +2 -0
  279. package/types/ui/Image.d.ts +3 -3
  280. package/types/ui/Layout/BottomInset.d.ts +2 -1
  281. package/types/ui/Layout/index.d.ts +1 -1
  282. package/types/ui/Signal/style.d.ts +4 -1
  283. package/types/ui/UiOverride/context.d.ts +14 -2
  284. package/types/ui/agentAttrs.d.ts +14 -0
  285. package/types/ui/index.d.ts +4 -1
  286. package/types/ui/overlayLayer.d.ts +24 -0
  287. package/types/ui/recipe/badgeRecipe.d.ts +7 -3
  288. package/types/ui/recipe/buttonRecipe.d.ts +7 -3
  289. package/types/ui/recipe/inputRecipe.d.ts +5 -1
  290. package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
  291. package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
  292. package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
  293. package/types/vendor/use-agentic/Agentic.d.ts +21 -0
  294. package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
  295. package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
  296. package/types/vendor/use-agentic/index.d.ts +13 -0
  297. package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
  298. package/types/vendor/use-agentic/types.d.ts +133 -0
  299. package/types/vendor/use-agentic/useAgent.d.ts +4 -0
  300. package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
  301. package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
  302. package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
  303. package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
  304. package/types/vendor/use-agentic.d.ts +1 -0
  305. package/types/webkit/index.d.ts +3 -0
  306. package/types/webkit/lazy.d.ts +12 -0
  307. package/types/webkit/useCsrValues.d.ts +3 -3
  308. package/types/webkit/useEscapeKey.d.ts +5 -0
  309. package/types/webkit/useFrameRuntime.d.ts +6 -1
  310. package/types/webkit/useScreenScope.d.ts +18 -0
  311. package/ui/Agent/Approval.tsx +29 -0
  312. package/ui/Agent/Bubble.tsx +74 -0
  313. package/ui/Agent/Chat.tsx +225 -0
  314. package/ui/Agent/Context.tsx +38 -0
  315. package/ui/Agent/Dock.tsx +72 -0
  316. package/ui/Agent/Guide.tsx +16 -0
  317. package/ui/Agent/Section.tsx +24 -0
  318. package/ui/Agent/StateKey.tsx +44 -0
  319. package/ui/Agent/Tool.tsx +66 -0
  320. package/ui/Agent/Transcript.tsx +33 -0
  321. package/ui/Agent/Zone.tsx +48 -0
  322. package/ui/Agent/fetchRunner.ts +30 -0
  323. package/ui/Agent/index.ts +12 -0
  324. package/ui/Agent/index_.tsx +4 -0
  325. package/ui/Agent/sessionHistory.ts +33 -0
  326. package/ui/Badge.tsx +3 -3
  327. package/ui/BottomSheet.tsx +5 -0
  328. package/ui/Button.tsx +6 -2
  329. package/ui/Constant/Doc.tsx +1 -1
  330. package/ui/CsrImage.tsx +1 -1
  331. package/ui/Data/ListContainer.tsx +1 -1
  332. package/ui/DatePicker.tsx +5 -4
  333. package/ui/Dialog/Modal.tsx +12 -15
  334. package/ui/DraggableList.tsx +3 -1
  335. package/ui/Dropdown.tsx +33 -11
  336. package/ui/Field.tsx +30 -16
  337. package/ui/Image.tsx +3 -3
  338. package/ui/Input.tsx +14 -5
  339. package/ui/Layout/BottomInset.tsx +6 -1
  340. package/ui/Load/Units.tsx +11 -2
  341. package/ui/Load/View.tsx +7 -2
  342. package/ui/Loading/ProgressBar.tsx +8 -1
  343. package/ui/Menu.tsx +7 -8
  344. package/ui/Model/EditModal.tsx +37 -2
  345. package/ui/Model/SureToRemove.tsx +2 -1
  346. package/ui/Model/index_.tsx +42 -15
  347. package/ui/ObjectId.tsx +3 -4
  348. package/ui/Pagination.tsx +3 -4
  349. package/ui/Popconfirm.tsx +8 -7
  350. package/ui/Select.tsx +6 -4
  351. package/ui/Signal/RestApi.tsx +43 -16
  352. package/ui/Signal/WebSocket.tsx +1 -1
  353. package/ui/Signal/style.ts +6 -1
  354. package/ui/Switch.tsx +2 -0
  355. package/ui/System/CSR.tsx +6 -2
  356. package/ui/System/ThemeToggle.tsx +55 -33
  357. package/ui/ToggleSelect.tsx +4 -3
  358. package/ui/Tooltip.tsx +2 -1
  359. package/ui/UiOverride/context.ts +14 -2
  360. package/ui/agentAttrs.ts +19 -0
  361. package/ui/index.ts +10 -1
  362. package/ui/overlayLayer.ts +39 -0
  363. package/ui/recipe/badgeRecipe.ts +22 -3
  364. package/ui/recipe/buttonRecipe.ts +51 -2
  365. package/ui/recipe/factory.ts +2 -2
  366. package/ui/recipe/inputRecipe.ts +18 -4
  367. package/vendor/use-agentic/AgentProvider.tsx +39 -0
  368. package/vendor/use-agentic/AgentScope.tsx +20 -0
  369. package/vendor/use-agentic/AgentSession.ts +292 -0
  370. package/vendor/use-agentic/Agentic.tsx +44 -0
  371. package/vendor/use-agentic/AgenticSurface.ts +294 -0
  372. package/vendor/use-agentic/WIRE.md +57 -0
  373. package/vendor/use-agentic/httpRunner.ts +115 -0
  374. package/vendor/use-agentic/index.ts +14 -0
  375. package/vendor/use-agentic/surfaceContext.ts +9 -0
  376. package/vendor/use-agentic/types.ts +144 -0
  377. package/vendor/use-agentic/useAgent.ts +17 -0
  378. package/vendor/use-agentic/useAgentGuide.ts +16 -0
  379. package/vendor/use-agentic/useAgentResource.ts +30 -0
  380. package/vendor/use-agentic/useAgentState.ts +61 -0
  381. package/vendor/use-agentic/useAgentTool.ts +55 -0
  382. package/webkit/bootCsr.tsx +2 -33
  383. package/webkit/index.ts +3 -0
  384. package/webkit/lazy.tsx +27 -3
  385. package/webkit/useCsrValues.ts +121 -4
  386. package/webkit/useEscapeKey.tsx +42 -0
  387. package/webkit/useFrameRuntime.ts +65 -32
  388. package/webkit/useScreenScope.tsx +49 -0
package/server/akanApp.ts CHANGED
@@ -7,6 +7,7 @@ import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport,
7
7
  import { isTraceEnabled } from "akanjs/signal";
8
8
  import { makeAkanChildProxyHeaders } from "./akanAppHeaders";
9
9
  import type { BuilderCsrReq, BuilderCsrRes, BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
10
+ import { resolveEncodedSidecar } from "./assetEncoding";
10
11
  import { isPortInUseError } from "./lifecycle/portInUse";
11
12
  import { RotatingLogWriter } from "./logging/rotatingLogWriter";
12
13
  import { ProcessMetricsCollector } from "./processMetricsCollector";
@@ -75,6 +76,8 @@ export class AkanApp {
75
76
  /** Hosted by `akan start`: crash loops should yield to the dev host, which restarts on file edits. */
76
77
  readonly #devHosted = process.env.AKAN_COMMAND_TYPE === "start";
77
78
  readonly #healthTimeoutMs = AkanApp.#parseHealthTimeoutMs();
79
+ /** Child stderr is bundler/runtime noise the gateway cannot act on; it still reaches the rotating log file. */
80
+ readonly #printChildStderr = process.env.AKAN_CHILD_STDERR === "1";
78
81
  readonly #serverPath: string;
79
82
  readonly #artifactDir: string;
80
83
  readonly #replica: AkanReplicaConfig;
@@ -156,10 +159,16 @@ export class AkanApp {
156
159
  return Math.max(min, parsed);
157
160
  }
158
161
 
162
+ /**
163
+ * The command that started this gateway outranks an inherited `NODE_ENV`: the command is a statement about
164
+ * this run, while the env var can arrive from a shell export, a CI image, or the workspace `.env` Bun loads
165
+ * on its own. Handing a dev child `NODE_ENV=production` makes it serve from the production route cache and
166
+ * throw on every route, since only `akan build` writes the routes manifest that cache expects.
167
+ */
159
168
  static #defaultChildNodeEnv() {
160
- if (process.env.NODE_ENV) return process.env.NODE_ENV;
161
169
  if (process.env.AKAN_COMMAND_TYPE === "start") return "development";
162
170
  if (process.env.AKAN_COMMAND_TYPE === "build") return "production";
171
+ if (process.env.NODE_ENV) return process.env.NODE_ENV;
163
172
  return Bun.main.endsWith(".js") ? "production" : "development";
164
173
  }
165
174
 
@@ -585,7 +594,11 @@ export class AkanApp {
585
594
  const result = ws.send(event.data as string | ArrayBuffer);
586
595
  if (result === 0) upstream.close();
587
596
  });
588
- upstream.addEventListener("close", (event) => ws.close(event.code, event.reason));
597
+ upstream.addEventListener("close", (event) => {
598
+ const code = AkanApp.#sendableCloseCode(event.code);
599
+ if (code === undefined) ws.close();
600
+ else ws.close(code, event.reason);
601
+ });
589
602
  upstream.addEventListener("error", () => ws.close(1011, "upstream websocket error"));
590
603
  Object.assign(ws.data, { pending });
591
604
  }
@@ -605,8 +618,15 @@ export class AkanApp {
605
618
  else ws.data.pending?.push(payload as string | ArrayBuffer);
606
619
  }
607
620
 
621
+ static #sendableCloseCode(code: number): number | undefined {
622
+ const sendable = (code >= 1000 && code <= 1014 && (code < 1004 || code > 1006)) || (code >= 3000 && code <= 4999);
623
+ return sendable ? code : undefined;
624
+ }
625
+
608
626
  #handleWsClose(ws: Bun.ServerWebSocket<GatewayWsData>, code: number, reason: string) {
609
- ws.data.upstream.close(code, reason);
627
+ const upstreamCode = AkanApp.#sendableCloseCode(code);
628
+ if (upstreamCode === undefined) ws.data.upstream.close();
629
+ else ws.data.upstream.close(upstreamCode, reason);
610
630
  const child = this.#children.get(ws.data.childIdx);
611
631
  if (child) child.metrics.activeWebSockets = Math.max(0, (child.metrics.activeWebSockets ?? 1) - 1);
612
632
  }
@@ -808,41 +828,17 @@ export class AkanApp {
808
828
  const headers = new Headers({ "Content-Type": options.contentType });
809
829
  if (options.cacheControl) headers.set("Cache-Control", options.cacheControl);
810
830
 
811
- const gzipPath = `${filePath}.gz`;
812
- if (this.#acceptsGzip(req) && this.#isCompressible(options.contentType)) {
813
- const gzipFile = Bun.file(gzipPath);
814
- if (await gzipFile.exists()) {
815
- const gzipBytes = await gzipFile.bytes();
816
- headers.set("Content-Encoding", "gzip");
817
- headers.set("Content-Length", String(gzipBytes.byteLength));
818
- headers.set("Vary", "Accept-Encoding");
819
- return new Response(this.#toArrayBuffer(gzipBytes), { headers });
820
- }
831
+ const sidecar = await resolveEncodedSidecar(req, filePath, options.contentType);
832
+ if (sidecar) {
833
+ headers.set("Content-Encoding", sidecar.encoding);
834
+ headers.set("Content-Length", String(sidecar.bytes.byteLength));
835
+ headers.set("Vary", "Accept-Encoding");
836
+ return new Response(sidecar.bytes, { headers });
821
837
  }
822
838
 
823
839
  return new Response(Bun.file(filePath).stream(), { headers });
824
840
  }
825
841
 
826
- #acceptsGzip(req: Request): boolean {
827
- const acceptEncoding = req.headers.get("accept-encoding") ?? "";
828
- return /\bgzip\b/.test(acceptEncoding);
829
- }
830
-
831
- #isCompressible(contentType: string): boolean {
832
- const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
833
- return (
834
- type.startsWith("text/") ||
835
- type === "application/javascript" ||
836
- type === "application/json" ||
837
- type === "application/manifest+json" ||
838
- type === "image/svg+xml"
839
- );
840
- }
841
-
842
- #toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
843
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
844
- }
845
-
846
842
  #safeResolve(baseDir: string, urlPath: string): string | null {
847
843
  let decoded: string;
848
844
  try {
@@ -1240,7 +1236,7 @@ export class AkanApp {
1240
1236
 
1241
1237
  #writeChildOutputLineRaw(idx: number, role: AkanChildRole, type: "stdout" | "stderr", line: string) {
1242
1238
  const prefixedLine = `[child:${idx} ${role}] [${type}] ${line}`;
1243
- process[type].write(prefixedLine);
1239
+ if (type === "stdout" || this.#printChildStderr) process[type].write(prefixedLine);
1244
1240
  this.#logWriter?.write(`${idx}-${role}`, AkanApp.#stripAnsi(prefixedLine));
1245
1241
  }
1246
1242
 
@@ -1,17 +1,35 @@
1
- import type { BaseEnv, PromiseOrObject } from "akanjs/base";
2
- import type { MiddlewareCls } from "akanjs/signal";
1
+ import type { BackendEnv, PromiseOrObject } from "akanjs/base";
2
+ import type { Adaptor, AdaptorCls, LlmOption } from "akanjs/service";
3
+ import type { AgentRelayPolicy, MiddlewareCls } from "akanjs/signal";
4
+ import type { McpServerOption } from "./akanServer";
3
5
  import type { WebProxyRegistration } from "./proxy";
4
6
  import { HostBasePathWebProxy, LocaleWebProxy } from "./proxy";
5
7
 
6
- /** App/library server option builder for use objects, signal middleware, and web proxies. */
7
- export class AkanOption<Env extends BaseEnv = BaseEnv> {
8
+ export interface AdaptorOverride {
9
+ role: AdaptorCls;
10
+ adaptor: AdaptorCls;
11
+ }
12
+
13
+ /**
14
+ * App/library server option builder: use objects, signal middleware, adaptor overrides, web proxies, and the
15
+ * server settings an app owns — MCP, the agent relay's access policy, and the LLM the relay speaks to.
16
+ */
17
+ export class AkanOption<Env = unknown> {
8
18
  readonly #getUses: ((env: Env) => Record<string, PromiseOrObject<unknown>>)[];
9
19
  readonly #middlewares: MiddlewareCls[] = [];
20
+ readonly #adaptorOverrides: AdaptorOverride[] = [];
10
21
  readonly #webProxies: WebProxyRegistration[] = [];
22
+ readonly #getLlms: ((env: Env) => LlmOption)[] = [];
23
+ #mcp: boolean | McpServerOption | undefined;
24
+ #agentAccess: AgentRelayPolicy | null | undefined;
11
25
  constructor() {
12
26
  this.#getUses = [];
13
27
  }
14
- use(fnOrObject: ((env: Env) => Record<string, PromiseOrObject<unknown>>) | Record<string, PromiseOrObject<unknown>>) {
28
+ use(
29
+ fnOrObject:
30
+ | ((env: Env & BackendEnv) => Record<string, PromiseOrObject<unknown>>)
31
+ | Record<string, PromiseOrObject<unknown>>,
32
+ ) {
15
33
  if (typeof fnOrObject === "function")
16
34
  this.#getUses.push(fnOrObject as (env: Env) => Record<string, PromiseOrObject<unknown>>);
17
35
  else this.#getUses.push(() => fnOrObject);
@@ -21,22 +39,62 @@ export class AkanOption<Env extends BaseEnv = BaseEnv> {
21
39
  this.#middlewares.push(...middlewares);
22
40
  return this;
23
41
  }
42
+ /** Rebinds a predefined adaptor role (e.g. `LlmAdaptorRole`) to the app's own implementation. Last writer wins. */
43
+ applyAdaptor<T extends Adaptor>(role: AdaptorCls<T>, adaptor: AdaptorCls<T>) {
44
+ this.#adaptorOverrides.push({ role: role as AdaptorCls, adaptor: adaptor as AdaptorCls });
45
+ return this;
46
+ }
24
47
  applyWebProxy(...proxies: WebProxyRegistration[]) {
25
48
  this.#webProxies.push(...proxies);
26
49
  return this;
27
50
  }
28
- getUses(env: Env): Record<string, PromiseOrObject<unknown>> {
51
+ /**
52
+ * MCP server settings for the app mounting this option, merged over the `AKAN_MCP_*` environment and under an
53
+ * option the server is constructed with. The app's own `option.ts` is the last lib the server reads, so it wins
54
+ * over every library it depends on.
55
+ */
56
+ setMcp(mcp: boolean | McpServerOption = true) {
57
+ this.#mcp = mcp;
58
+ return this;
59
+ }
60
+ /**
61
+ * Who may spend the LLM key through the `runAgentTurn` relay — the `AgentRelayAccess` policy, which defaults to
62
+ * allowing everyone because the framework has no account model to gate on. `null` reopens it.
63
+ */
64
+ setAgentAccess(policy: AgentRelayPolicy | null) {
65
+ this.#agentAccess = policy;
66
+ return this;
67
+ }
68
+ /** Settings for whichever adaptor fills `LlmAdaptorRole`, injected into it as the `llmOption` use. */
69
+ setLlm(llmOrFn: LlmOption | ((env: Env) => LlmOption)) {
70
+ if (typeof llmOrFn === "function") this.#getLlms.push(llmOrFn);
71
+ else this.#getLlms.push(() => llmOrFn);
72
+ return this;
73
+ }
74
+ getUses(env: Env & BackendEnv): Record<string, PromiseOrObject<unknown>> {
29
75
  const uses = this.#getUses.map((fn) => fn(env));
30
76
  return Object.assign({}, ...uses);
31
77
  }
32
78
  getMiddlewares(): MiddlewareCls[] {
33
79
  return this.#middlewares;
34
80
  }
81
+ getAdaptorOverrides(): AdaptorOverride[] {
82
+ return this.#adaptorOverrides;
83
+ }
35
84
  getWebProxies(): WebProxyRegistration[] {
36
85
  return this.#webProxies;
37
86
  }
87
+ getMcp(): boolean | McpServerOption | undefined {
88
+ return this.#mcp;
89
+ }
90
+ getAgentAccess(): AgentRelayPolicy | null | undefined {
91
+ return this.#agentAccess;
92
+ }
93
+ getLlm(env: Env & BackendEnv): LlmOption {
94
+ return Object.assign({}, ...this.#getLlms.map((fn) => fn(env)));
95
+ }
38
96
  }
39
97
 
40
98
  export function createDefaultAkanOption() {
41
- return new AkanOption<BaseEnv>().applyWebProxy(LocaleWebProxy, HostBasePathWebProxy);
99
+ return new AkanOption().applyWebProxy(LocaleWebProxy, HostBasePathWebProxy);
42
100
  }
@@ -1,5 +1,6 @@
1
1
  import { type BaseEnv, getEnv } from "akanjs/base";
2
2
  import { Logger } from "akanjs/common";
3
+ import { DictionaryLookup } from "akanjs/dictionary";
3
4
  import type {
4
5
  Adaptor,
5
6
  AdaptorCls,
@@ -10,6 +11,7 @@ import type {
10
11
  SolidConfig,
11
12
  } from "akanjs/service";
12
13
  import type { ServerSignal, ServerSignalCls, WebsocketPublishData } from "akanjs/signal";
14
+ import { AgentRelayAccess } from "../signal/guards";
13
15
  import { createOpenApiDocument } from "../signal/openapi";
14
16
  import { FetchSerializer } from "../signal/serializer";
15
17
  import type { AkanLib, AkanLibProps } from "./akanLib";
@@ -19,6 +21,7 @@ import { DiLifecycle } from "./di/diLifecycle";
19
21
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
20
22
  import { isPortInUseError } from "./lifecycle/portInUse";
21
23
  import { ShutdownManager } from "./lifecycle/shutdownManager";
24
+ import { type McpAuthOption, McpRouter } from "./mcp";
22
25
  import { ProcessMetricsCollector } from "./processMetricsCollector";
23
26
  import { WebProxyRunner } from "./proxy";
24
27
  import { SignalResolver } from "./resolver";
@@ -36,6 +39,44 @@ export interface AkanServerProps extends AkanLibProps {
36
39
 
37
40
  export interface AkanServerOptions {
38
41
  openapi?: boolean;
42
+ /** `/mcp` is mounted by default; `false` takes it off, and the object form carries the rest of its settings. */
43
+ mcp?: boolean | McpServerOption;
44
+ }
45
+
46
+ export interface McpServerOption {
47
+ enabled?: boolean;
48
+ /**
49
+ * Drops every mutation from the catalogue whatever its guards allow — for a deployment that must not be able to
50
+ * write, such as a read replica or a demo. Off by default, and reported per endpoint in the boot log, because a
51
+ * switch that silently unlists a published endpoint cannot tell its author why it vanished.
52
+ */
53
+ readOnly?: boolean;
54
+ /**
55
+ * Mount path, `/mcp` by default. The published OAuth resource identifier follows it, so changing it changes
56
+ * the `aud` a token has to carry.
57
+ */
58
+ path?: string;
59
+ /** Reported as `serverInfo.version`. Defaults to `0.0.0`, the same placeholder the OpenAPI document uses. */
60
+ version?: string;
61
+ /**
62
+ * Free-text usage guidance handed to the model alongside the tool list — the one place to say what this app
63
+ * is for and which tool to reach for first. Defaults to a bare one-liner naming the app.
64
+ */
65
+ instructions?: string;
66
+ /**
67
+ * Extra origins allowed past the DNS-rebinding check, beyond the server's own host. Needed only for a
68
+ * browser-hosted MCP client; native ones send no `Origin` at all.
69
+ */
70
+ allowedOrigins?: string[];
71
+ /** Entries per catalogue page. A client that wants the whole list follows `nextCursor` until it stops. */
72
+ pageSize?: number;
73
+ /**
74
+ * The one language the catalogue and its error text are written in, `en` by default. Server-wide on purpose:
75
+ * the document is built once at boot and cached by clients, and it is read by a model rather than by a person.
76
+ */
77
+ language?: string;
78
+ /** OAuth resource-server identity: which issuers a client may authenticate with, and the scopes to demand. */
79
+ auth?: McpAuthOption;
39
80
  }
40
81
 
41
82
  interface AkanAppPrepared {
@@ -76,6 +117,10 @@ export class AkanServer {
76
117
  prefix = "/api";
77
118
  websocketPrefix = "/ws";
78
119
  openapi = AkanServer.#isOpenApiEnvEnabled();
120
+ mcp = AkanServer.#isEnvOn("AKAN_MCP", "AKAN_PUBLIC_MCP");
121
+ mcpReadOnly = AkanServer.#isEnvEnabled("AKAN_MCP_READONLY", "AKAN_PUBLIC_MCP_READONLY");
122
+ mcpAuth: McpAuthOption = AkanServer.#mcpAuthFromEnv();
123
+ mcpOption: Omit<McpServerOption, "enabled" | "readOnly" | "auth"> = AkanServer.#mcpOptionFromEnv();
79
124
  serverMode: "federation" | "batch" | "all";
80
125
  shutdownTimeoutMs = AkanServer.#defaultShutdownTimeoutMs();
81
126
 
@@ -84,7 +129,7 @@ export class AkanServer {
84
129
  #metricsTimer: Timer | null = null;
85
130
  constructor(
86
131
  name = "AkanServer",
87
- env: BaseEnv = getEnv(),
132
+ env = {},
88
133
  serverMode: "federation" | "batch" | "all" = (process.env.SERVER_MODE as
89
134
  | "federation"
90
135
  | "batch"
@@ -96,8 +141,16 @@ export class AkanServer {
96
141
  this.name = name;
97
142
  this.logger = new Logger(name);
98
143
  this.libs = libs;
99
- this.env = { ...env };
144
+ this.env = Object.assign(getEnv(), env);
100
145
  this.openapi = options?.openapi ?? this.openapi;
146
+
147
+ libs.forEach((lib) => {
148
+ const mcp = lib.option.getMcp();
149
+ if (mcp !== undefined) this.setMcp(mcp);
150
+ const agentAccess = lib.option.getAgentAccess();
151
+ if (agentAccess !== undefined) AgentRelayAccess.use(agentAccess);
152
+ });
153
+ this.setMcp(options?.mcp ?? this.mcp);
101
154
  this.serverMode = serverMode;
102
155
  this.#di = new DiLifecycle(this.env, serverMode, ...libs);
103
156
  }
@@ -114,6 +167,16 @@ export class AkanServer {
114
167
  this.openapi = openapi;
115
168
  return this;
116
169
  }
170
+ setMcp(mcp: boolean | McpServerOption = true) {
171
+ if (this.status !== "stopped") throw new Error("MCP config must be set before app initialization.");
172
+ this.mcp = typeof mcp === "boolean" ? mcp : (mcp.enabled ?? true);
173
+ if (typeof mcp === "boolean") return this;
174
+ const { enabled: _enabled, readOnly, auth, ...rest } = mcp;
175
+ if (readOnly !== undefined) this.mcpReadOnly = readOnly;
176
+ if (auth) this.mcpAuth = { ...this.mcpAuth, ...AkanServer.#defined(auth) };
177
+ this.mcpOption = { ...this.mcpOption, ...AkanServer.#defined(rest) };
178
+ return this;
179
+ }
117
180
  setDatabaseConfig(database: DatabaseConfig) {
118
181
  if (this.status !== "stopped") throw new Error("Database config must be set before app initialization.");
119
182
  this.env.database = database;
@@ -460,12 +523,30 @@ export class AkanServer {
460
523
  title: `${this.env.appName} API`,
461
524
  version: "0.0.0",
462
525
  servers: this.#getOpenApiServers(),
526
+ resolveDescription: AkanServer.#createDescriptionResolver(),
463
527
  }),
464
528
  ),
465
529
  },
466
530
  }
467
531
  : {};
468
532
 
533
+ const mcpRouter =
534
+ this.mcp && this.serverMode !== "batch"
535
+ ? new McpRouter({
536
+ registry: this.#di.registry,
537
+ env: this.env,
538
+ live: this.#di.live,
539
+ middleware: new Map(this.#di.modules.middleware),
540
+ instructions: `Domain tools for the ${this.env.appName} app.`,
541
+ ...this.mcpOption,
542
+ readOnly: this.mcpReadOnly,
543
+ auth: this.mcpAuth,
544
+ })
545
+ : null;
546
+ const mcpRoutes: HttpRoutes = mcpRouter?.createRoutes() ?? {};
547
+
548
+ mcpRouter?.report();
549
+
469
550
  const devtoolsRoutes = new DevtoolsRouter({
470
551
  di: this.#di,
471
552
  env: this.env,
@@ -476,7 +557,17 @@ export class AkanServer {
476
557
  openapi: this.openapi,
477
558
  getStatus: () => this.status,
478
559
  }).createRoutes();
479
- return { ...openapiRoutes, ...devtoolsRoutes };
560
+ return { ...openapiRoutes, ...mcpRoutes, ...devtoolsRoutes };
561
+ }
562
+
563
+ /**
564
+ * Rebuilt per request rather than cached on the instance: libs register their dictionaries at module-evaluation
565
+ * time, and the document itself is already re-serialized per request, so a stale snapshot would be the only
566
+ * thing here that could disagree with the rest of the response.
567
+ */
568
+ static #createDescriptionResolver() {
569
+ const lookup = new DictionaryLookup();
570
+ return (key: string) => lookup.text(key);
480
571
  }
481
572
 
482
573
  #getOpenApiServers() {
@@ -494,14 +585,88 @@ export class AkanServer {
494
585
 
495
586
  static #isServerOptions(value: AkanLib | AkanServerOptions | undefined): value is AkanServerOptions {
496
587
  return Boolean(
497
- value && !("database" in value) && !("service" in value) && !("scalar" in value) && "openapi" in value,
588
+ value &&
589
+ !("database" in value) &&
590
+ !("service" in value) &&
591
+ !("scalar" in value) &&
592
+ ("openapi" in value || "mcp" in value),
498
593
  );
499
594
  }
500
595
 
501
596
  static #isOpenApiEnvEnabled() {
502
- return [process.env.AKAN_OPENAPI, process.env.AKAN_PUBLIC_OPENAPI].some(
503
- (value) => value === "true" || value === "1",
504
- );
597
+ return AkanServer.#isEnvEnabled("AKAN_OPENAPI", "AKAN_PUBLIC_OPENAPI");
598
+ }
599
+
600
+ static #isEnvEnabled(...names: string[]) {
601
+ return names.some((name) => process.env[name] === "true" || process.env[name] === "1");
602
+ }
603
+
604
+ /**
605
+ * On unless the env says otherwise, which is the opposite of `#isEnvEnabled`.
606
+ *
607
+ * MCP exposure follows an endpoint's guards rather than an opt-in, so there is nothing an app has to declare for
608
+ * its catalogue to be right — and a switch that must be found before anything works is a switch most deployments
609
+ * never find. `AKAN_MCP=false` is the way off.
610
+ */
611
+ static #isEnvOn(...names: string[]) {
612
+ return !names.some((name) => process.env[name] === "false" || process.env[name] === "0");
613
+ }
614
+
615
+ /** Both differ per environment, so they belong in env rather than in the app's source alongside the switch. */
616
+ static #mcpAuthFromEnv(): McpAuthOption {
617
+ const authorizationServers = AkanServer.#envList("AKAN_MCP_AUTH_SERVERS");
618
+ const scopes = AkanServer.#envList("AKAN_MCP_SCOPES");
619
+ return {
620
+ ...(authorizationServers?.length ? { authorizationServers } : {}),
621
+ ...(scopes?.length ? { scopes } : {}),
622
+ ...(process.env.AKAN_MCP_RESOURCE ? { resource: process.env.AKAN_MCP_RESOURCE } : {}),
623
+ };
624
+ }
625
+
626
+ /**
627
+ * The rest of `McpServerOption`, spelled as environment — the channel a deployment configures what the source
628
+ * does not through, and the only one a child of the gateway is handed. An app writes these in its `option.ts`,
629
+ * which merges over this: a value written in code wins over the env of the same name.
630
+ */
631
+ /**
632
+ * Keys whose value is `undefined` are dropped before the merge. "An option written in code wins over the env of
633
+ * the same name" is the contract, and a spread reads an explicitly-`undefined` property as a value — so
634
+ * `{ path: undefined }` from a caller assembling options conditionally erased what the environment supplied.
635
+ */
636
+ static #defined<T extends object>(source: T): Partial<T> {
637
+ return Object.fromEntries(Object.entries(source).filter(([, value]) => value !== undefined)) as Partial<T>;
638
+ }
639
+
640
+ static #mcpOptionFromEnv(): Omit<McpServerOption, "enabled" | "readOnly" | "auth"> {
641
+ const allowedOrigins = AkanServer.#envList("AKAN_MCP_ALLOWED_ORIGINS");
642
+ const pageSize = Number(process.env.AKAN_MCP_PAGE_SIZE);
643
+ return {
644
+ ...AkanServer.#mcpPathFromEnv(),
645
+ ...(process.env.AKAN_MCP_VERSION ? { version: process.env.AKAN_MCP_VERSION } : {}),
646
+ ...(process.env.AKAN_MCP_INSTRUCTIONS ? { instructions: process.env.AKAN_MCP_INSTRUCTIONS } : {}),
647
+ ...(allowedOrigins?.length ? { allowedOrigins } : {}),
648
+ ...(Number.isInteger(pageSize) && pageSize > 0 ? { pageSize } : {}),
649
+ ...(process.env.AKAN_MCP_LANGUAGE ? { language: process.env.AKAN_MCP_LANGUAGE } : {}),
650
+ };
651
+ }
652
+
653
+ /**
654
+ * Both consumers build their path by concatenation — the route key, and the RFC 9728 metadata path the resource
655
+ * identifier is inserted into — so `AKAN_MCP_PATH=mcp` served a route named `mcp` and published its metadata at
656
+ * `/.well-known/oauth-protected-resourcemcp`, which no client would ever look for. Normalized rather than
657
+ * rejected: the intent is unambiguous, and a server that refuses to boot over a missing slash is worse.
658
+ */
659
+ static #mcpPathFromEnv() {
660
+ const path = process.env.AKAN_MCP_PATH?.trim();
661
+ if (!path) return {};
662
+ return { path: path.startsWith("/") ? path : `/${path}` };
663
+ }
664
+
665
+ static #envList(name: string) {
666
+ return process.env[name]
667
+ ?.split(",")
668
+ .map((entry) => entry.trim())
669
+ .filter(Boolean);
505
670
  }
506
671
 
507
672
  /**
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `Bun.Image.metadata()` reports only `{ width, height, format }` — there is no frame count — and an
3
+ * animated GIF decodes to its first frame without erroring. So an optimizer that re-encodes whatever
4
+ * decodes would silently drop the animation. These readers answer the question from the container bytes
5
+ * instead, so an animated source can be passed through untouched.
6
+ */
7
+
8
+ /** Sub-blocks are a `[length][bytes]…[0]` chain, so a frame's payload cannot be skipped by size alone. */
9
+ const skipGifSubBlocks = (buffer: Buffer, start: number): number => {
10
+ let offset = start;
11
+ while (offset < buffer.length) {
12
+ const size = buffer[offset];
13
+ offset += 1;
14
+ if (size === undefined || size === 0) return offset;
15
+ offset += size;
16
+ }
17
+ return offset;
18
+ };
19
+
20
+ const colorTableSize = (packed: number): number => ((packed & 0x80) === 0 ? 0 : 3 * 2 ** ((packed & 0x07) + 1));
21
+
22
+ /**
23
+ * Walks the GIF block stream to count image descriptors. Counting raw `0x2C` bytes would overcount —
24
+ * the byte occurs freely inside colour tables and LZW payloads — so every block has to be stepped over.
25
+ */
26
+ const hasMultipleGifFrames = (buffer: Buffer): boolean => {
27
+ if (buffer.length < 13) return false;
28
+ let offset = 6;
29
+ offset += 7 + colorTableSize(buffer[offset + 4] ?? 0);
30
+ let frames = 0;
31
+ while (offset < buffer.length) {
32
+ const block = buffer[offset];
33
+ offset += 1;
34
+ if (block === 0x3b) break;
35
+ if (block === 0x21) {
36
+ offset = skipGifSubBlocks(buffer, offset + 1);
37
+ continue;
38
+ }
39
+ if (block !== 0x2c) break;
40
+ frames += 1;
41
+ if (frames > 1) return true;
42
+ offset += 9 + colorTableSize(buffer[offset + 8] ?? 0);
43
+ offset = skipGifSubBlocks(buffer, offset + 1);
44
+ }
45
+ return frames > 1;
46
+ };
47
+
48
+ /** An animated WebP is an extended (`VP8X`) file carrying an `ANIM` chunk; a still one never has it. */
49
+ const hasWebpAnimChunk = (buffer: Buffer): boolean => {
50
+ let offset = 12;
51
+ while (offset + 8 <= buffer.length) {
52
+ const id = buffer.toString("ascii", offset, offset + 4);
53
+ if (id === "ANIM") return true;
54
+ const size = buffer.readUInt32LE(offset + 4);
55
+ offset += 8 + size + (size % 2);
56
+ }
57
+ return false;
58
+ };
59
+
60
+ /** APNG marks itself with an `acTL` chunk, which the spec requires before the first `IDAT`. */
61
+ const hasPngAnimationControl = (buffer: Buffer): boolean => {
62
+ let offset = 8;
63
+ while (offset + 8 <= buffer.length) {
64
+ const size = buffer.readUInt32BE(offset);
65
+ const id = buffer.toString("ascii", offset + 4, offset + 8);
66
+ if (id === "acTL") return true;
67
+ if (id === "IDAT" || id === "IEND") return false;
68
+ offset += 12 + size;
69
+ }
70
+ return false;
71
+ };
72
+
73
+ export const isAnimatedImage = (buffer: Buffer, contentType: string): boolean => {
74
+ try {
75
+ if (contentType === "image/gif") return hasMultipleGifFrames(buffer);
76
+ if (contentType === "image/webp") return hasWebpAnimChunk(buffer);
77
+ if (contentType === "image/png") return hasPngAnimationControl(buffer);
78
+ return false;
79
+ } catch {
80
+
81
+ return true;
82
+ }
83
+ };
@@ -48,12 +48,22 @@ export class RouteClientCache {
48
48
  };
49
49
  readonly #buildRoute: RouteBuildFn;
50
50
  readonly #onMerge?: OnMergeFn;
51
+ #revision = 0;
51
52
 
52
53
  constructor({ buildRoute, onMerge }: RouteClientCacheOptions) {
53
54
  this.#buildRoute = buildRoute;
54
55
  this.#onMerge = onMerge;
55
56
  }
56
57
 
58
+ /**
59
+ * Bumped on every mutation of `merged`, including a delta merge that leaves `generation` where it was. It lets a
60
+ * consumer memoize work derived from the manifest — merging the runtime manifest over it, say — without having to
61
+ * copy the manifest to find out whether anything changed. In production nothing after `seed` moves it at all.
62
+ */
63
+ get revision(): number {
64
+ return this.#revision;
65
+ }
66
+
57
67
  #getEmptyDelta(): BuildRouteClientResult {
58
68
  return {
59
69
  manifestDelta: {},
@@ -75,6 +85,7 @@ export class RouteClientCache {
75
85
  Object.assign(this.merged.ssrManifest.moduleMap, manifest.ssrManifest.moduleMap);
76
86
  for (const abs of manifest.knownEntries) this.merged.knownEntries.add(abs);
77
87
  for (const routeId of manifest.routeIds) this.#built.set(routeId, this.#getEmptyDelta());
88
+ this.#revision += 1;
78
89
  }
79
90
 
80
91
  async ensure(routeId: string, seeds: string[]): Promise<MergedManifest> {
@@ -125,6 +136,7 @@ export class RouteClientCache {
125
136
  for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap))
126
137
  this.merged.ssrManifest.moduleMap[url] = byName;
127
138
  for (const entry of delta.newEntries) this.merged.knownEntries.add(entry);
139
+ this.#revision += 1;
128
140
  this.#built.set(routeId, delta);
129
141
  this.#logger.verbose(
130
142
  `[route-cache] build done routeId=${routeId} generation=${generation} entries=+${delta.newEntries.length} deps=${delta.clientDeps.length} in ${Date.now() - started}ms`,
@@ -176,6 +188,7 @@ export class RouteClientCache {
176
188
  }
177
189
  const nextGeneration = this.merged.generation + 1;
178
190
  this.merged = this.#getEmptyMerged(nextGeneration);
191
+ this.#revision += 1;
179
192
  this.#building.clear();
180
193
  this.#logger.verbose(`[route-cache] cleared generation=${nextGeneration} dropped=${dropped.length}`);
181
194
  return dropped;
@@ -209,6 +222,7 @@ export class RouteClientCache {
209
222
  ),
210
223
  };
211
224
  this.merged = next;
225
+ this.#revision += 1;
212
226
  }
213
227
 
214
228
  static #normalizePath(filePath: string): string {
@@ -0,0 +1,47 @@
1
+ const COMPRESSIBLE_TYPES = new Set([
2
+ "application/javascript",
3
+ "application/json",
4
+ "application/manifest+json",
5
+ "image/svg+xml",
6
+ ]);
7
+
8
+ /**
9
+ * br is tried first: it is ~15% smaller than gzip across the artifact and ~22% on the CSS bundle.
10
+ * The gzip sidecar stays the fallback because browsers only advertise `br` on secure origins, so a
11
+ * plain-http dev server or an intermediary that rewrites Accept-Encoding still gets a compressed body.
12
+ */
13
+ const SIDECAR_ENCODINGS = [
14
+ { encoding: "br", ext: ".br", accept: /(?:^|,)\s*(?:br|\*)(?![\w-])\s*(?:;\s*q=([\d.]+))?/i },
15
+ { encoding: "gzip", ext: ".gz", accept: /(?:^|,)\s*(?:gzip|\*)(?![\w-])\s*(?:;\s*q=([\d.]+))?/i },
16
+ ] as const;
17
+
18
+ export interface EncodedSidecar {
19
+ bytes: ArrayBuffer;
20
+ encoding: string;
21
+ }
22
+
23
+ export const isCompressibleContentType = (contentType: string): boolean => {
24
+ const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
25
+ return type.startsWith("text/") || COMPRESSIBLE_TYPES.has(type);
26
+ };
27
+
28
+ /** Picks the best precompressed sidecar the caller accepts, or null to serve the file as-is. */
29
+ export const resolveEncodedSidecar = async (
30
+ req: Request,
31
+ filePath: string,
32
+ contentType: string,
33
+ ): Promise<EncodedSidecar | null> => {
34
+ if (!isCompressibleContentType(contentType)) return null;
35
+ const acceptEncoding = req.headers.get("accept-encoding") ?? "";
36
+ for (const { encoding, ext, accept } of SIDECAR_ENCODINGS) {
37
+ const match = accept.exec(acceptEncoding);
38
+
39
+ if (!match || (match[1] !== undefined && Number.parseFloat(match[1]) <= 0)) continue;
40
+ const file = Bun.file(`${filePath}${ext}`);
41
+ if (!(await file.exists())) continue;
42
+ const bytes = await file.bytes();
43
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
44
+ return { bytes: buffer, encoding };
45
+ }
46
+ return null;
47
+ };