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
@@ -1,7 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { lookup } from "node:dns/promises";
2
3
  import fs from "node:fs/promises";
4
+ import { isIP } from "node:net";
3
5
  import path from "node:path";
4
- import { Logger } from "akanjs/common";
6
+ import { Logger, TrustedProxy } from "akanjs/common";
5
7
  import { isAnimatedImage } from "./animatedImage";
6
8
  import { matchesIfNoneMatch } from "./contentEncoding";
7
9
  import { ImageOptimizerError } from "./imageOptimizerError";
@@ -13,6 +15,7 @@ import {
13
15
  type AkanImagePattern,
14
16
  getAkanImageWidths,
15
17
  mergeAkanImageConfig,
18
+ type ResolvedAkanImageConfig,
16
19
  } from "./types";
17
20
 
18
21
  export interface ImageOptimizerOptions {
@@ -20,6 +23,7 @@ export interface ImageOptimizerOptions {
20
23
  cacheDir: string;
21
24
  prodMode: boolean;
22
25
  config?: Partial<AkanImageConfig>;
26
+ resolveHost?: (hostname: string) => Promise<{ address: string }[]>;
23
27
  }
24
28
 
25
29
  interface ParsedImageRequest {
@@ -67,12 +71,17 @@ export class ImageOptimizer {
67
71
  #publicDir: string;
68
72
  #cacheDir: string;
69
73
  #prodMode: boolean;
70
- #config: AkanImageConfig;
74
+ #config: ResolvedAkanImageConfig;
71
75
  #semaphore: Semaphore;
76
+ readonly #resolveHost: (hostname: string) => Promise<{ address: string }[]>;
77
+ readonly #encodeSlots: number;
78
+ #encoding = 0;
79
+ #cacheBytes: number | null = null;
80
+ #pruning: Promise<void> | null = null;
72
81
  readonly #inflight = new Map<string, Promise<OptimizedImage>>();
73
82
  #logger = new Logger("ImageOptimizer");
74
83
 
75
- constructor({ publicDir, cacheDir, prodMode, config }: ImageOptimizerOptions) {
84
+ constructor({ publicDir, cacheDir, prodMode, config, resolveHost }: ImageOptimizerOptions) {
76
85
  this.#publicDir = publicDir;
77
86
  this.#cacheDir = cacheDir;
78
87
  this.#prodMode = prodMode;
@@ -83,7 +92,18 @@ export class ImageOptimizer {
83
92
  this.#logger.warn(`${dropped.join(", ")} needs an OS codec this platform does not have; serving webp instead`);
84
93
  }
85
94
  this.#config = { ...merged, formats: formats.length ? formats : ["image/webp"] };
86
- this.#semaphore = new Semaphore(ImageOptimizer.#resolveConcurrency(this.#config.maxConcurrency));
95
+ const concurrency = ImageOptimizer.#resolveConcurrency(this.#config.maxConcurrency);
96
+ this.#semaphore = new Semaphore(concurrency);
97
+ this.#encodeSlots = this.#config.maxQueue > 0 ? concurrency + this.#config.maxQueue : Number.POSITIVE_INFINITY;
98
+ this.#resolveHost =
99
+ resolveHost ??
100
+ (async (hostname) =>
101
+ await Promise.race([
102
+ lookup(hostname, { all: true }),
103
+ new Promise<never>((_resolve, reject) =>
104
+ setTimeout(() => reject(new Error("DNS lookup timed out")), this.#config.fetchTimeoutMs).unref(),
105
+ ),
106
+ ]));
87
107
  }
88
108
 
89
109
  static #resolveConcurrency(configured: number): number {
@@ -105,7 +125,7 @@ export class ImageOptimizer {
105
125
  } catch (error) {
106
126
  const message = error instanceof ImageOptimizerError ? error.message : "Unable to optimize image";
107
127
  const status = error instanceof ImageOptimizerError ? error.status : 500;
108
- return new Response(message, { status });
128
+ return new Response(message, { status, headers: status === 503 ? { "retry-after": "1" } : undefined });
109
129
  }
110
130
  }
111
131
 
@@ -145,9 +165,8 @@ export class ImageOptimizer {
145
165
  return null;
146
166
  }
147
167
  if (!pointer.file || pointer.expireAt <= Date.now()) return null;
148
- const file = Bun.file(pointer.file);
149
- if (!(await file.exists())) return null;
150
- const buffer = Buffer.from(await file.arrayBuffer());
168
+ const buffer = await ImageOptimizer.#readCached(pointer.file);
169
+ if (!buffer) return null;
151
170
  return {
152
171
  buffer,
153
172
  contentType: pointer.contentType,
@@ -165,7 +184,7 @@ export class ImageOptimizer {
165
184
  maxAge: optimized.maxAge,
166
185
  expireAt: Date.now() + optimized.maxAge * 1000,
167
186
  };
168
- await Bun.write(this.#getPointerPath(parsed), JSON.stringify(pointer));
187
+ await this.#writeCache(this.#getPointerPath(parsed), JSON.stringify(pointer));
169
188
  }
170
189
 
171
190
  #getPointerPath(parsed: ParsedImageRequest) {
@@ -191,7 +210,7 @@ export class ImageOptimizer {
191
210
  let href: string;
192
211
  let isRemote = false;
193
212
  if (url.startsWith("/")) {
194
- href = url;
213
+ href = ImageOptimizer.#localHref(this.#config.localPatterns, url);
195
214
  const pathname = ImageOptimizer.#safeDecodePathname(url);
196
215
  if (!pathname) return { error: '"url" parameter is invalid' };
197
216
  if (pathname === ImageOptimizer.#optimizerPath || pathname.startsWith(`${ImageOptimizer.#optimizerPath}/`)) {
@@ -263,6 +282,7 @@ export class ImageOptimizer {
263
282
  if (!ImageOptimizer.#hasRemoteMatch(this.#config.remotePatterns, current)) {
264
283
  throw new ImageOptimizerError(400, '"url" parameter is not allowed');
265
284
  }
285
+ await this.#assertPublicHost(current);
266
286
  const controller = new AbortController();
267
287
  const timeout = setTimeout(() => controller.abort(), this.#config.fetchTimeoutMs);
268
288
  try {
@@ -277,7 +297,7 @@ export class ImageOptimizer {
277
297
  current = new URL(location, current);
278
298
  continue;
279
299
  }
280
- if (!res.ok) throw new ImageOptimizerError(res.status, "Remote image request failed");
300
+ if (!res.ok) throw new ImageOptimizerError(502, "Remote image request failed");
281
301
  const buffer = await ImageOptimizer.#readResponseBuffer(res, this.#config.maxRemoteBytes);
282
302
  return {
283
303
  buffer,
@@ -293,6 +313,22 @@ export class ImageOptimizer {
293
313
  throw new ImageOptimizerError(400, "Remote image exceeded maximum redirects");
294
314
  }
295
315
 
316
+ async #assertPublicHost(url: URL) {
317
+ if (this.#config.dangerouslyAllowLocalIP) return;
318
+ const host = url.hostname.replace(/^\[|\]$/g, "");
319
+ let addresses: string[];
320
+ if (isIP(host)) addresses = [host];
321
+ else {
322
+ try {
323
+ addresses = (await this.#resolveHost(host)).map(({ address }) => address);
324
+ } catch {
325
+ throw new ImageOptimizerError(502, "Remote image request failed");
326
+ }
327
+ }
328
+ if (!addresses.length || addresses.some((address) => TrustedProxy.isPrivateAddress(address)))
329
+ throw new ImageOptimizerError(400, '"url" parameter is not allowed');
330
+ }
331
+
296
332
  async #getOptimizedImage(source: ImageSource, params: ParsedImageRequest): Promise<OptimizedImage> {
297
333
  const inputType = ImageOptimizer.#detectContentType(source.buffer) || source.contentType;
298
334
  if (!inputType.startsWith("image/") || inputType.includes(",")) {
@@ -320,13 +356,12 @@ export class ImageOptimizer {
320
356
  outputType,
321
357
  upstreamTag: source.upstreamTag,
322
358
  });
323
- const cached = Bun.file(cachePath);
324
- if (await cached.exists()) {
325
- const buffer = Buffer.from(await cached.arrayBuffer());
359
+ const cached = await ImageOptimizer.#readCached(cachePath);
360
+ if (cached) {
326
361
  return {
327
- buffer,
362
+ buffer: cached,
328
363
  contentType: outputType,
329
- etag: ImageOptimizer.#hashBuffer(buffer),
364
+ etag: ImageOptimizer.#hashBuffer(cached),
330
365
  maxAge,
331
366
  cacheFile: cachePath,
332
367
  };
@@ -336,12 +371,15 @@ export class ImageOptimizer {
336
371
  let contentType = inputType;
337
372
  let cacheable = true;
338
373
  if (shouldEncode) {
374
+ if (this.#encoding >= this.#encodeSlots) throw new ImageOptimizerError(503, "Image optimizer is busy");
375
+ this.#encoding += 1;
339
376
  try {
340
377
  buffer = await this.#semaphore.run(() =>
341
378
  ImageOptimizer.#optimize(source.buffer, {
342
379
  width: params.width,
343
380
  quality: params.quality,
344
381
  contentType: outputType,
382
+ maxPixels: this.#config.maxInputPixels,
345
383
  }),
346
384
  );
347
385
  contentType = outputType;
@@ -349,13 +387,12 @@ export class ImageOptimizer {
349
387
  buffer = source.buffer;
350
388
  contentType = inputType;
351
389
  cacheable = false;
390
+ } finally {
391
+ this.#encoding -= 1;
352
392
  }
353
393
  }
354
394
 
355
- if (cacheable) {
356
- await fs.mkdir(this.#cacheDir, { recursive: true });
357
- await Bun.write(cachePath, buffer);
358
- }
395
+ if (cacheable) await this.#writeCache(cachePath, buffer);
359
396
  return {
360
397
  buffer,
361
398
  contentType,
@@ -365,6 +402,51 @@ export class ImageOptimizer {
365
402
  };
366
403
  }
367
404
 
405
+ static async #readCached(cachePath: string): Promise<Buffer | null> {
406
+ try {
407
+ const file = Bun.file(cachePath);
408
+ return (await file.exists()) ? Buffer.from(await file.arrayBuffer()) : null;
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+
414
+ async #writeCache(file: string, data: Buffer | string) {
415
+ await fs.mkdir(this.#cacheDir, { recursive: true });
416
+ this.#cacheBytes ??= (await this.#listCache()).reduce((total, entry) => total + entry.size, 0);
417
+ await Bun.write(file, data);
418
+ this.#cacheBytes += Buffer.byteLength(data);
419
+ const ceiling = this.#config.maxCacheBytes;
420
+ if (ceiling <= 0 || this.#cacheBytes <= ceiling) return;
421
+ this.#pruning ??= this.#pruneCache(ceiling).finally(() => {
422
+ this.#pruning = null;
423
+ });
424
+ await this.#pruning;
425
+ }
426
+
427
+ async #listCache(): Promise<{ file: string; size: number; mtimeMs: number }[]> {
428
+ const names = await fs.readdir(this.#cacheDir).catch(() => [] as string[]);
429
+ const entries = await Promise.all(
430
+ names.map(async (name) => {
431
+ const file = path.join(this.#cacheDir, name);
432
+ const stat = await fs.stat(file).catch(() => null);
433
+ return stat?.isFile() ? { file, size: stat.size, mtimeMs: stat.mtimeMs } : null;
434
+ }),
435
+ );
436
+ return entries.filter((entry) => entry !== null);
437
+ }
438
+
439
+ async #pruneCache(ceiling: number) {
440
+ const entries = (await this.#listCache()).sort((a, b) => a.mtimeMs - b.mtimeMs);
441
+ let total = entries.reduce((sum, entry) => sum + entry.size, 0);
442
+ for (const entry of entries) {
443
+ if (total <= ceiling * 0.8) break;
444
+ await fs.rm(entry.file, { force: true });
445
+ total -= entry.size;
446
+ }
447
+ this.#cacheBytes = total;
448
+ }
449
+
368
450
  #getCachePath(input: Record<string, string | number>) {
369
451
  const contentType = String(input.outputType);
370
452
  return path.join(
@@ -389,9 +471,11 @@ export class ImageOptimizer {
389
471
 
390
472
  static async #optimize(
391
473
  buffer: Buffer,
392
- options: { width: number; quality: number; contentType: string },
474
+ options: { width: number; quality: number; contentType: string; maxPixels: number },
393
475
  ): Promise<Buffer> {
394
- const image = new Bun.Image(buffer).resize(options.width, undefined, { withoutEnlargement: true });
476
+ const image = new Bun.Image(buffer, { maxPixels: options.maxPixels }).resize(options.width, undefined, {
477
+ withoutEnlargement: true,
478
+ });
395
479
 
396
480
  if (options.contentType === ImageOptimizer.#avif) return await image.avif({ quality: options.quality }).toBuffer();
397
481
  if (options.contentType === ImageOptimizer.#webp) return await image.webp({ quality: options.quality }).toBuffer();
@@ -411,6 +495,17 @@ export class ImageOptimizer {
411
495
  });
412
496
  }
413
497
 
498
+ static #localHref(patterns: Pick<AkanImagePattern, "pathname" | "search">[], href: string): string {
499
+ const url = new URL(href, "http://local.akan");
500
+ const namesQuery = patterns.some(
501
+ (pattern) =>
502
+ pattern.search !== undefined &&
503
+ pattern.search === url.search &&
504
+ (!pattern.pathname || ImageOptimizer.#globMatch(pattern.pathname, url.pathname)),
505
+ );
506
+ return namesQuery ? `${url.pathname}${url.search}` : url.pathname;
507
+ }
508
+
414
509
  static #hasLocalMatch(patterns: Pick<AkanImagePattern, "pathname" | "search">[], href: string): boolean {
415
510
  const url = new URL(href, "http://local.akan");
416
511
  return patterns.some((pattern) => {
@@ -0,0 +1,11 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const BUILD_ENV_FILE = "akan.env.json";
5
+
6
+ export const applyBuildEnv = (dir: string = path.dirname(Bun.main), env: NodeJS.ProcessEnv = process.env): void => {
7
+ const file = path.join(dir, BUILD_ENV_FILE);
8
+ if (!existsSync(file)) return;
9
+ const defaults = JSON.parse(readFileSync(file, "utf8")) as Record<string, string>;
10
+ for (const [key, value] of Object.entries(defaults)) env[key] ??= value;
11
+ };
@@ -0,0 +1,3 @@
1
+ import { applyBuildEnv } from "./buildEnv";
2
+
3
+ applyBuildEnv();
@@ -63,21 +63,27 @@ export class LogControlSocket {
63
63
 
64
64
  if (await LogControlSocket.#answers(this.path)) throw new Error("another server is listening on it");
65
65
  await rm(this.path, { force: true });
66
- this.#server = Bun.listen<ClientState>({
67
- unix: this.path,
68
- socket: {
69
- open: (socket) => {
70
- socket.data = { inbox: "", pending: "", droppedWhilePending: 0, subscriptions: new Map() };
71
- },
72
- data: (socket, chunk) => this.#onData(socket, chunk),
73
- drain: (socket) => this.#drain(socket),
74
- close: (socket) => this.#release(socket),
75
- error: (socket, error) => {
76
- this.#logger.debug(`control socket client error: ${error.message}`);
77
- this.#release(socket);
66
+
67
+ const umask = process.platform === "win32" ? null : process.umask(0o177);
68
+ try {
69
+ this.#server = Bun.listen<ClientState>({
70
+ unix: this.path,
71
+ socket: {
72
+ open: (socket) => {
73
+ socket.data = { inbox: "", pending: "", droppedWhilePending: 0, subscriptions: new Map() };
74
+ },
75
+ data: (socket, chunk) => this.#onData(socket, chunk),
76
+ drain: (socket) => this.#drain(socket),
77
+ close: (socket) => this.#release(socket),
78
+ error: (socket, error) => {
79
+ this.#logger.debug(`control socket client error: ${error.message}`);
80
+ this.#release(socket);
81
+ },
78
82
  },
79
- },
80
- });
83
+ });
84
+ } finally {
85
+ if (umask !== null) process.umask(umask);
86
+ }
81
87
  await chmod(this.path, 0o600);
82
88
  }
83
89
 
@@ -1,5 +1,7 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import { bearerTokenOf, EventStream, Logger } from "akanjs/common";
3
+ import { FailureLimiter } from "../ops/failureLimiter";
4
+ import { StatusAccess } from "../ops/statusAccess";
3
5
  import type { LogHub, LogHubEntry } from "./logHub";
4
6
  import { LogQueryMatcher } from "./logQuery";
5
7
  import { LogStdoutWriter } from "./logStdoutWriter";
@@ -10,11 +12,12 @@ export class LogStreamRoute {
10
12
  static readonly heartbeatMs = 15_000;
11
13
 
12
14
  readonly #hub: () => LogHub | null;
13
- readonly #token: Buffer;
15
+ readonly #digest: Buffer;
16
+ readonly #failures = new FailureLimiter({ limit: 10 });
14
17
 
15
18
  constructor(hub: () => LogHub | null, token: string) {
16
19
  this.#hub = hub;
17
- this.#token = Buffer.from(token);
20
+ this.#digest = StatusAccess.digest(token);
18
21
  }
19
22
 
20
23
  /** Without a token the route is not registered at all, rather than mounted to answer "forbidden". */
@@ -31,8 +34,8 @@ export class LogStreamRoute {
31
34
  return new LogStreamRoute(hub, token);
32
35
  }
33
36
 
34
- handle(req: Request): Response {
35
- if (!this.#authorized(req))
37
+ handle(req: Request, client?: string | null): Response {
38
+ if (!this.#authorized(req, client))
36
39
  return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": "Bearer" } });
37
40
  const hub = this.#hub();
38
41
  if (!hub) return new Response("Log hub is not running", { status: 503 });
@@ -53,11 +56,12 @@ export class LogStreamRoute {
53
56
  return stream.response();
54
57
  }
55
58
 
56
- #authorized(req: Request): boolean {
59
+ #authorized(req: Request, client: string | null | undefined): boolean {
57
60
  const token = bearerTokenOf(req.headers.get("authorization"));
58
- if (!token) return false;
59
- const presented = Buffer.from(token);
60
- return presented.length === this.#token.length && timingSafeEqual(presented, this.#token);
61
+ if (!token || this.#failures.blocked(client)) return false;
62
+ const matched = timingSafeEqual(StatusAccess.digest(token), this.#digest);
63
+ if (!matched) this.#failures.fail(client);
64
+ return matched;
61
65
  }
62
66
 
63
67
  static #resume(hub: LogHub, stream: EventStream, matcher: LogQueryMatcher, lastEventId: number) {
@@ -22,6 +22,7 @@ export interface RotatingLogWriterOptions {
22
22
  operationMode: string;
23
23
  maxSizeBytes?: number;
24
24
  maxFiles?: number;
25
+ fileMode?: number;
25
26
  now?: () => Date;
26
27
  warn?: (message: string) => void;
27
28
  }
@@ -29,11 +30,13 @@ export interface RotatingLogWriterOptions {
29
30
  export class RotatingLogWriter {
30
31
  static readonly defaultMaxSizeBytes = 50 * 1024 * 1024;
31
32
  static readonly defaultMaxFiles = 100;
33
+ static readonly defaultFileMode = 0o600;
32
34
 
33
35
  readonly #logDir: string;
34
36
  readonly #basePrefix: string;
35
37
  readonly #maxSizeBytes: number;
36
38
  readonly #maxFiles: number;
39
+ readonly #fileMode: number;
37
40
  readonly #now: () => Date;
38
41
  readonly #warn: (message: string) => void;
39
42
  readonly #states = new Map<string, RotatingLogWriterState>();
@@ -50,6 +53,7 @@ export class RotatingLogWriter {
50
53
  ].join("-");
51
54
  this.#maxSizeBytes = options.maxSizeBytes ?? RotatingLogWriter.defaultMaxSizeBytes;
52
55
  this.#maxFiles = options.maxFiles ?? RotatingLogWriter.defaultMaxFiles;
56
+ this.#fileMode = options.fileMode ?? RotatingLogWriter.defaultFileMode;
53
57
  this.#now = options.now ?? (() => new Date());
54
58
  this.#warn = options.warn ?? RotatingLogWriter.#defaultWarn;
55
59
  }
@@ -65,6 +69,7 @@ export class RotatingLogWriter {
65
69
  operationMode: env.operationMode,
66
70
  maxSizeBytes: RotatingLogWriter.#parseMaxSizeBytes(process.env.AKAN_LOG_MAX_SIZE_MB),
67
71
  maxFiles: RotatingLogWriter.#parseMaxFiles(process.env.AKAN_LOG_MAX_FILES),
72
+ fileMode: RotatingLogWriter.fileMode(),
68
73
  });
69
74
  } catch (error) {
70
75
  RotatingLogWriter.#defaultWarn(
@@ -109,7 +114,7 @@ export class RotatingLogWriter {
109
114
  }
110
115
 
111
116
  const filePath = this.#getFilePath(processKey, state.date, state.sequence);
112
- await appendFile(filePath, chunk);
117
+ await appendFile(filePath, chunk, { mode: this.#fileMode });
113
118
  state.sizeBytes += byteLength;
114
119
  if (createdState) await this.#cleanupRetention(processKey);
115
120
  }
@@ -182,6 +187,15 @@ export class RotatingLogWriter {
182
187
  return parsed;
183
188
  }
184
189
 
190
+ static fileMode(value = process.env.AKAN_LOG_FILE_MODE, warn = RotatingLogWriter.#defaultWarn) {
191
+ const raw = value?.trim();
192
+ if (!raw) return RotatingLogWriter.defaultFileMode;
193
+ const parsed = /^[0-7]{3,4}$/.test(raw) ? Number.parseInt(raw, 8) : 0;
194
+ if (parsed > 0 && parsed <= 0o777) return parsed;
195
+ warn(`AKAN_LOG_FILE_MODE="${raw}" is not an octal mode such as 644; using 600`);
196
+ return RotatingLogWriter.defaultFileMode;
197
+ }
198
+
185
199
  static #defaultWarn(message: string) {
186
200
  process.stderr.write(`[AkanLog] ${message}\n`);
187
201
  }
@@ -70,6 +70,10 @@ export class MachineToken {
70
70
  return !!MachineToken.#keyPair().publicKey;
71
71
  }
72
72
 
73
+ static publicKey(): KeyObject | null {
74
+ return MachineToken.#keyPair().publicKey;
75
+ }
76
+
73
77
  static issue(identity: MachineIdentity, { expiresAt, audience }: MachineTokenIssueOptions = {}): string {
74
78
  const { privateKey } = MachineToken.#keyPair();
75
79
  if (!privateKey) throw new Error("MachineToken.issue needs AKAN_MACHINE_PRIVATE_KEY on the server that issues it");
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { PromiseOrObject } from "akanjs/base";
3
- import { bearerTokenOf, decodeJwtPayload } from "akanjs/common";
3
+ import { AppHosts, bearerTokenOf, decodeJwtPayload, IpLiteral } from "akanjs/common";
4
4
  import { SignalContext } from "../../signal/signalContext";
5
5
  import type { HttpRoutes } from "../types";
6
6
 
@@ -90,7 +90,7 @@ export class McpAuth {
90
90
 
91
91
  if (this.#props.authorizationServers?.length && !audience.length)
92
92
  return { status: 401, error: "invalid_token", description: "The access token names no resource." };
93
- if (audience.length && !audience.includes(this.#resource(req)))
93
+ if (audience.length && !(this.answersTo(req) && audience.includes(this.#resource(req))))
94
94
  return { status: 401, error: "invalid_token", description: "The access token was issued for another resource." };
95
95
  const missing = this.#missingScopes(claims);
96
96
 
@@ -149,6 +149,14 @@ export class McpAuth {
149
149
  return McpAuth.origin(req);
150
150
  }
151
151
 
152
+ answersTo(req: Request): boolean {
153
+ if (this.#props.resource) return true;
154
+ const host = URL.parse(McpAuth.origin(req))?.host;
155
+ const name = host ? AppHosts.nameOf(host) : null;
156
+ if (!host || !name) return false;
157
+ return !!IpLiteral.parse(host) || name === "localhost" || name.endsWith(".localhost") || AppHosts.allows(host);
158
+ }
159
+
152
160
  static origin(req: Request) {
153
161
  const url = new URL(req.url);
154
162
  const forwarded = (header: string) => req.headers.get(header)?.split(",")[0]?.trim();
@@ -156,10 +164,10 @@ export class McpAuth {
156
164
  return `${forwarded("x-forwarded-proto") ?? url.protocol.replace(":", "")}://${host}`;
157
165
  }
158
166
 
159
- static callerKey(req: Request): string {
167
+ callerKey(req: Request): string {
160
168
  const token = McpAuth.#bearer(req);
161
169
 
162
- if (!token) return `ip:${SignalContext.clientIpOf(req) ?? "anonymous"}`;
170
+ if (!token || !this.#props.verify) return `ip:${SignalContext.clientIpOf(req) ?? "anonymous"}`;
163
171
  const claims = McpAuth.#claims(token);
164
172
  const id = [claims?.sid, claims?.jti, claims?.sub].find((value) => typeof value === "string" && value);
165
173
  return typeof id === "string" ? `token:${id}` : `token:${createHash("sha256").update(token).digest("base64url")}`;
@@ -19,6 +19,10 @@ export class McpExecutionContext extends HttpExecutionContext {
19
19
  this.#arguments = args;
20
20
  }
21
21
 
22
+ override defersBody(): boolean {
23
+ return false;
24
+ }
25
+
22
26
  override async readArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
23
27
  const declared = new Set(endpointInfo.args.filter(McpExecutionContext.#describable).map((arg) => arg.name));
24
28
  const undeclared = Object.keys(this.#arguments).find((name) => !declared.has(name));
@@ -68,6 +68,7 @@ interface McpCall {
68
68
  era: McpEra;
69
69
  req: Request;
70
70
  ui: boolean;
71
+ inProcess?: boolean;
71
72
  }
72
73
 
73
74
  interface McpErrorOptions {
@@ -322,19 +323,19 @@ export class McpRouter {
322
323
  discoverCache,
323
324
  );
324
325
  case "tools/list":
325
- return await this.#list(call, "tools", [
326
+ return await this.#list(call, "tools", async () => [
326
327
  ...document.tools,
327
328
  ...(await this.#pages.tools(call.ui, document, call.req)),
328
329
  ]);
329
330
  case "resources/list":
330
- return await this.#list(call, "resources", [
331
+ return await this.#list(call, "resources", async () => [
331
332
  ...document.resources,
332
333
  ...(await this.#pages.resources(call.ui, call.req)),
333
334
  ]);
334
335
  case "resources/templates/list":
335
- return await this.#list(call, "resourceTemplates", document.resourceTemplates);
336
+ return await this.#list(call, "resourceTemplates", () => document.resourceTemplates);
336
337
  case "prompts/list":
337
- return await this.#list(call, "prompts", await this.#pages.prompts(call.req));
338
+ return await this.#list(call, "prompts", () => this.#pages.prompts(call.req));
338
339
  case "tools/call": {
339
340
  const slot = await this.#acquire(call);
340
341
  if ("refused" in slot) return slot.refused;
@@ -361,12 +362,15 @@ export class McpRouter {
361
362
  }
362
363
  }
363
364
 
364
- async #list<T extends { name: string }>(call: McpCall, key: string, items: T[]) {
365
- const visible = await this.#dispatcher.filterForAccount(items, call.req);
366
- const page = McpRouter.#page(visible, call.params.cursor, this.#props.pageSize ?? defaultPageSize);
367
- if (!page) return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Invalid cursor.");
368
- const result = { [key]: page.items, ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) };
369
- return this.#result(call, result, listCache);
365
+ async #list<T extends { name: string }>(call: McpCall, key: string, itemsOf: () => Promise<T[]> | T[]) {
366
+ const list = async () => {
367
+ const visible = await this.#dispatcher.filterForAccount(await itemsOf(), call.req);
368
+ const page = McpRouter.#page(visible, call.params.cursor, this.#props.pageSize ?? defaultPageSize);
369
+ if (!page) return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Invalid cursor.");
370
+ const result = { [key]: page.items, ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) };
371
+ return this.#result(call, result, listCache);
372
+ };
373
+ return call.inProcess ? await list() : await this.#limited(call, list);
370
374
  }
371
375
 
372
376
  /** The catalogue as built, before any caller filter: refusals, undescribed entries, `findTool`. */
@@ -394,7 +398,15 @@ export class McpRouter {
394
398
  const items: unknown[] = [];
395
399
  let cursor: string | undefined;
396
400
  do {
397
- const call: McpCall = { id: 0, method, params: cursor ? { cursor } : {}, era: "legacy", req, ui };
401
+ const call: McpCall = {
402
+ id: 0,
403
+ method,
404
+ params: cursor ? { cursor } : {},
405
+ era: "legacy",
406
+ req,
407
+ ui,
408
+ inProcess: true,
409
+ };
398
410
  const body = (await (await this.#dispatch(call)).json()) as McpJsonRpcResponse;
399
411
  const result = body.result as Record<string, unknown> | undefined;
400
412
  if (!result) throw new Error(`MCP ${method} failed: ${body.error?.message ?? "no result"}`);
@@ -416,7 +428,7 @@ export class McpRouter {
416
428
 
417
429
  async #acquire(call: McpCall): Promise<{ refused: Response } | { release: () => void }> {
418
430
  if (!this.#limiter) return { release: () => {} };
419
- const verdict = await this.#limiter.acquire(McpAuth.callerKey(call.req));
431
+ const verdict = await this.#limiter.acquire(this.#auth.callerKey(call.req));
420
432
  if (verdict.ok) return { release: verdict.release };
421
433
  const seconds = Math.max(1, Math.ceil(verdict.retryAfterMs / 1000));
422
434
  const message =
@@ -577,7 +589,7 @@ export class McpRouter {
577
589
  if ((this.#props.allowedOrigins ?? []).includes(origin)) return true;
578
590
  try {
579
591
 
580
- return new URL(origin).host === new URL(this.#auth.publicOrigin(req)).host;
592
+ return new URL(origin).host === new URL(this.#auth.publicOrigin(req)).host && this.#auth.answersTo(req);
581
593
  } catch {
582
594
  return false;
583
595
  }
@@ -1,5 +1,5 @@
1
1
  import { PrimitiveRegistry } from "akanjs/base";
2
- import { Logger } from "akanjs/common";
2
+ import { Logger, sliceNames } from "akanjs/common";
3
3
  import { agentRead, ConstantRegistry, type ConstantType, type MaskModel } from "akanjs/constant";
4
4
  import type { McpDocument } from "../../signal/mcp/McpDocument";
5
5
  import { Msg, type PromptMessage } from "../../signal/mcp/Msg";
@@ -151,14 +151,11 @@ export class PagePromptComposer {
151
151
  const refName = argName.replace(/Id$/, "");
152
152
  if (!refName || refName === argName) return undefined;
153
153
  const document = this.#props.document;
154
+ const rootList = sliceNames(refName).endpoint.list;
154
155
  const listTools = document.tools
155
156
  .map((tool) => tool.name)
156
- .filter((name) => document.findTool(name)?.refName === refName && name.startsWith(`${refName}List`));
157
- return (
158
- listTools.find((name) => /Search/.test(name)) ??
159
- listTools.find((name) => name === `${refName}List`) ??
160
- listTools[0]
161
- );
157
+ .filter((name) => document.findTool(name)?.refName === refName && name.startsWith(rootList));
158
+ return listTools.find((name) => /Search/.test(name)) ?? listTools.find((name) => name === rootList) ?? listTools[0];
162
159
  }
163
160
 
164
161
  static #resource(attachment: Attachment): PromptMessage {