akanjs 3.0.0-alpha.99 → 3.0.0-beta.1

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 (286) hide show
  1. package/client/csrTypes.ts +3 -0
  2. package/client/index.ts +1 -0
  3. package/client/route/LayoutDefinition.ts +46 -0
  4. package/client/route/PageDefinition.ts +53 -0
  5. package/client/route/RootLayoutDefinition.ts +58 -0
  6. package/client/route/RouteDefinition.ts +204 -0
  7. package/client/route/index.ts +7 -0
  8. package/client/route/resolveRouteModule.ts +46 -0
  9. package/client/route/routeArgs.ts +51 -0
  10. package/client/route/routeBuilders.ts +10 -0
  11. package/common/TrustedProxy.ts +23 -1
  12. package/common/index.ts +0 -1
  13. package/common/mcpExposure.ts +5 -22
  14. package/constant/cascadePaths.ts +20 -20
  15. package/constant/deserialize.ts +30 -3
  16. package/constant/fieldInfo.ts +22 -16
  17. package/constant/purify.ts +2 -0
  18. package/constant/types.ts +6 -2
  19. package/dictionary/agent.dictionary.ts +8 -0
  20. package/dictionary/base.dictionary.ts +2 -0
  21. package/document/documentSchema.ts +20 -0
  22. package/document/filterMeta.ts +21 -0
  23. package/document/into.ts +0 -8
  24. package/fetch/client/fetchClient.ts +17 -8
  25. package/fetch/client/httpClient.ts +2 -30
  26. package/fetch/client/index.ts +1 -0
  27. package/fetch/client/remoteError.ts +57 -0
  28. package/fetch/client/wsClient.ts +2 -9
  29. package/fetch/requestStorage.ts +15 -0
  30. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  31. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  32. package/package.json +1 -1
  33. package/server/akanApp.ts +21 -1
  34. package/server/akanAppHeaders.ts +1 -1
  35. package/server/akanOption.ts +7 -6
  36. package/server/akanServer.ts +78 -7
  37. package/server/di/diLifecycle.ts +108 -7
  38. package/server/di/utils.ts +1 -1
  39. package/server/index.ts +1 -0
  40. package/server/mcp/McpAuth.ts +107 -29
  41. package/server/mcp/McpDispatcher.ts +6 -40
  42. package/server/mcp/McpExecutionContext.ts +6 -1
  43. package/server/mcp/McpRateLimiter.ts +88 -0
  44. package/server/mcp/McpRouter.ts +226 -50
  45. package/server/mcp/PagePromptComposer.ts +205 -0
  46. package/server/mcp/index.ts +1 -0
  47. package/server/oauth/OAuthAuthorize.ts +84 -0
  48. package/server/oauth/OAuthClientIdMetadata.ts +139 -0
  49. package/server/oauth/OAuthErrors.ts +77 -0
  50. package/server/oauth/OAuthMetadata.ts +59 -0
  51. package/server/oauth/OAuthPkce.ts +29 -0
  52. package/server/oauth/OAuthRedirect.ts +57 -0
  53. package/server/oauth/OAuthRegistration.ts +86 -0
  54. package/server/oauth/OAuthRevocation.ts +60 -0
  55. package/server/oauth/OAuthToken.ts +115 -0
  56. package/server/oauth/index.ts +10 -0
  57. package/server/oauth/oauthTypes.ts +79 -0
  58. package/server/resolver/CascadeRunner.ts +1 -1
  59. package/server/resolver/database.resolver.ts +70 -72
  60. package/server/resolver/service.resolver.ts +4 -14
  61. package/server/resolver/signal.resolver.ts +0 -8
  62. package/server/routeTreeBuilder.ts +45 -19
  63. package/server/routing/apiRouter.ts +1 -15
  64. package/server/rscPagePrompts.ts +175 -0
  65. package/server/rscWorker.tsx +41 -1
  66. package/server/rscWorkerHost.ts +54 -0
  67. package/server/webRouter.ts +9 -0
  68. package/service/agent.service.ts +31 -1
  69. package/service/predefinedAdaptor/anthropicLlm.ts +376 -0
  70. package/service/predefinedAdaptor/deepseekLlm.ts +21 -210
  71. package/service/predefinedAdaptor/index.ts +3 -0
  72. package/service/predefinedAdaptor/llm.adaptor.ts +20 -0
  73. package/service/predefinedAdaptor/openaiDialect.ts +237 -0
  74. package/service/predefinedAdaptor/openaiLlm.ts +91 -0
  75. package/signal/endpointInfo.ts +2 -38
  76. package/signal/exception.ts +18 -0
  77. package/signal/guard.ts +14 -1
  78. package/signal/mcp/McpDocument.ts +51 -69
  79. package/signal/mcp/McpUriTemplate.ts +22 -0
  80. package/signal/mcp/index.ts +2 -1
  81. package/signal/mcp/mcpProtocol.ts +2 -0
  82. package/signal/mcp/pagePrompt.ts +45 -0
  83. package/signal/openapi/openapi.ts +4 -19
  84. package/signal/schema/JsonSchemaBuilder.ts +55 -18
  85. package/signal/serializer/fetch.serializer.ts +15 -0
  86. package/signal/signalContext.ts +49 -29
  87. package/signal/types.ts +8 -1
  88. package/store/action.ts +15 -12
  89. package/store/agent/AgentBridge.ts +10 -2
  90. package/store/agent/AgentPrompts.ts +4 -57
  91. package/store/agent/ScreenReader.ts +31 -6
  92. package/store/agent/StoreSurfaceSource.ts +13 -4
  93. package/store/agentic/StToolBuilder.ts +49 -22
  94. package/store/agentic/formFields.ts +21 -0
  95. package/store/agentic/index.ts +1 -0
  96. package/store/agentic/useFileFieldTool.ts +221 -0
  97. package/store/agentic/useRelationFieldTool.ts +3 -13
  98. package/types/client/csrTypes.d.ts +3 -0
  99. package/types/client/index.d.ts +1 -0
  100. package/types/client/route/LayoutDefinition.d.ts +16 -0
  101. package/types/client/route/PageDefinition.d.ts +15 -0
  102. package/types/client/route/RootLayoutDefinition.d.ts +17 -0
  103. package/types/client/route/RouteDefinition.d.ts +46 -0
  104. package/types/client/route/index.d.ts +7 -0
  105. package/types/client/route/resolveRouteModule.d.ts +24 -0
  106. package/types/client/route/routeArgs.d.ts +40 -0
  107. package/types/client/route/routeBuilders.d.ts +9 -0
  108. package/types/client/route.d.ts +1 -0
  109. package/types/common/TrustedProxy.d.ts +12 -0
  110. package/types/common/index.d.ts +1 -1
  111. package/types/common/mcpExposure.d.ts +2 -7
  112. package/types/constant/cascadePaths.d.ts +4 -2
  113. package/types/constant/deserialize.d.ts +6 -3
  114. package/types/constant/fieldInfo.d.ts +15 -8
  115. package/types/constant/types.d.ts +5 -2
  116. package/types/dictionary/agent.dictionary.d.ts +1 -1
  117. package/types/dictionary/base.dictionary.d.ts +1 -1
  118. package/types/dictionary/dictionary.d.ts +9 -9
  119. package/types/document/documentSchema.d.ts +2 -0
  120. package/types/document/filterMeta.d.ts +8 -0
  121. package/types/document/into.d.ts +0 -8
  122. package/types/fetch/client/fetchClient.d.ts +2 -1
  123. package/types/fetch/client/httpClient.d.ts +1 -19
  124. package/types/fetch/client/index.d.ts +1 -0
  125. package/types/fetch/client/remoteError.d.ts +31 -0
  126. package/types/fetch/client/wsClient.d.ts +1 -1
  127. package/types/fetch/requestStorage.d.ts +15 -0
  128. package/types/server/akanApp.d.ts +11 -0
  129. package/types/server/akanOption.d.ts +4 -3
  130. package/types/server/akanServer.d.ts +33 -1
  131. package/types/server/di/diLifecycle.d.ts +12 -1
  132. package/types/server/di/utils.d.ts +1 -1
  133. package/types/server/index.d.ts +1 -0
  134. package/types/server/mcp/McpAuth.d.ts +52 -13
  135. package/types/server/mcp/McpDispatcher.d.ts +1 -13
  136. package/types/server/mcp/McpRateLimiter.d.ts +38 -0
  137. package/types/server/mcp/McpRouter.d.ts +17 -0
  138. package/types/server/mcp/PagePromptComposer.d.ts +34 -0
  139. package/types/server/mcp/index.d.ts +1 -0
  140. package/types/server/oauth/OAuthAuthorize.d.ts +26 -0
  141. package/types/server/oauth/OAuthClientIdMetadata.d.ts +53 -0
  142. package/types/server/oauth/OAuthErrors.d.ts +40 -0
  143. package/types/server/oauth/OAuthMetadata.d.ts +37 -0
  144. package/types/server/oauth/OAuthPkce.d.ts +9 -0
  145. package/types/server/oauth/OAuthRedirect.d.ts +20 -0
  146. package/types/server/oauth/OAuthRegistration.d.ts +21 -0
  147. package/types/server/oauth/OAuthRevocation.d.ts +31 -0
  148. package/types/server/oauth/OAuthToken.d.ts +35 -0
  149. package/types/server/oauth/index.d.ts +10 -0
  150. package/types/server/oauth/oauthTypes.d.ts +49 -0
  151. package/types/server/oauth.d.ts +1 -0
  152. package/types/server/routeTreeBuilder.d.ts +5 -2
  153. package/types/server/rscPagePrompts.d.ts +26 -0
  154. package/types/server/rscWorkerHost.d.ts +3 -0
  155. package/types/server/webRouter.d.ts +3 -0
  156. package/types/service/agent.service.d.ts +19 -0
  157. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +110 -0
  158. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +10 -67
  159. package/types/service/predefinedAdaptor/index.d.ts +3 -0
  160. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +20 -0
  161. package/types/service/predefinedAdaptor/openaiDialect.d.ts +91 -0
  162. package/types/service/predefinedAdaptor/openaiLlm.d.ts +24 -0
  163. package/types/signal/endpointInfo.d.ts +3 -21
  164. package/types/signal/exception.d.ts +11 -0
  165. package/types/signal/guard.d.ts +9 -0
  166. package/types/signal/mcp/McpDocument.d.ts +19 -11
  167. package/types/signal/mcp/McpUriTemplate.d.ts +6 -0
  168. package/types/signal/mcp/index.d.ts +2 -1
  169. package/types/signal/mcp/mcpProtocol.d.ts +2 -0
  170. package/types/signal/mcp/pagePrompt.d.ts +50 -0
  171. package/types/signal/schema/JsonSchemaBuilder.d.ts +24 -3
  172. package/types/signal/signalContext.d.ts +6 -0
  173. package/types/signal/types.d.ts +8 -1
  174. package/types/store/action.d.ts +2 -2
  175. package/types/store/agent/AgentPrompts.d.ts +4 -17
  176. package/types/store/agent/ScreenReader.d.ts +10 -2
  177. package/types/store/agentic/StToolBuilder.d.ts +18 -7
  178. package/types/store/agentic/formFields.d.ts +12 -0
  179. package/types/store/agentic/index.d.ts +1 -0
  180. package/types/store/agentic/useFileFieldTool.d.ts +41 -0
  181. package/types/store/agentic/useRelationFieldTool.d.ts +1 -1
  182. package/types/ui/Agent/Attach.d.ts +4 -1
  183. package/types/ui/Agent/Chat.d.ts +8 -2
  184. package/types/ui/Agent/ChatCommands.d.ts +2 -4
  185. package/types/ui/Agent/Composer.d.ts +3 -1
  186. package/types/ui/Agent/attachment.d.ts +25 -4
  187. package/types/ui/Agent/useChatAttachments.d.ts +4 -2
  188. package/types/ui/Agent/useChatQueue.d.ts +3 -1
  189. package/types/ui/Agent/useSlashMenu.d.ts +2 -8
  190. package/types/ui/BottomSheet.d.ts +15 -2
  191. package/types/ui/CsrImage.d.ts +2 -2
  192. package/types/ui/Data/ListContainer.d.ts +1 -1
  193. package/types/ui/Dialog/Modal.d.ts +3 -1
  194. package/types/ui/Dialog/index.d.ts +1 -1
  195. package/types/ui/Dropdown.d.ts +5 -3
  196. package/types/ui/Empty.d.ts +3 -1
  197. package/types/ui/Field/Label.d.ts +4 -3
  198. package/types/ui/Field/index.d.ts +2 -2
  199. package/types/ui/Image.d.ts +3 -3
  200. package/types/ui/InfiniteScroll.d.ts +4 -1
  201. package/types/ui/Layout/BottomTab.d.ts +4 -2
  202. package/types/ui/Layout/LeftSider.d.ts +3 -1
  203. package/types/ui/Layout/RightSider.d.ts +4 -2
  204. package/types/ui/Layout/Sider.d.ts +7 -1
  205. package/types/ui/Layout/index.d.ts +4 -4
  206. package/types/ui/Load/Units.d.ts +3 -1
  207. package/types/ui/Load/View.d.ts +3 -1
  208. package/types/ui/Loading/Area.d.ts +9 -1
  209. package/types/ui/Menu.d.ts +3 -1
  210. package/types/ui/Modal.d.ts +7 -3
  211. package/types/ui/Model/Edit.d.ts +2 -4
  212. package/types/ui/Model/EditModal.d.ts +1 -1
  213. package/types/ui/Model/New.d.ts +2 -4
  214. package/types/ui/Model/Remove.d.ts +7 -1
  215. package/types/ui/Model/SureToRemove.d.ts +10 -1
  216. package/types/ui/Model/ViewEditModal.d.ts +7 -1
  217. package/types/ui/Pagination.d.ts +10 -2
  218. package/types/ui/Popconfirm.d.ts +8 -4
  219. package/types/ui/Select.d.ts +5 -3
  220. package/types/ui/Tab/Menu.d.ts +1 -1
  221. package/types/ui/Table.d.ts +9 -1
  222. package/types/ui/Toast.d.ts +43 -0
  223. package/types/ui/ToggleSelect.d.ts +11 -1
  224. package/types/ui/UiOverride/context.d.ts +3 -0
  225. package/types/ui/Unauthorized.d.ts +3 -1
  226. package/types/ui/index.d.ts +3 -1
  227. package/types/ui/triggerSlot.d.ts +24 -0
  228. package/types/vendor/use-agentic/AgentSession.d.ts +1 -1
  229. package/types/vendor/use-agentic/ToolOutput.d.ts +11 -0
  230. package/types/vendor/use-agentic/ToolRunner.d.ts +70 -0
  231. package/types/vendor/use-agentic/index.d.ts +1 -0
  232. package/types/vendor/use-agentic/types.d.ts +7 -0
  233. package/types/webkit/bootCsr.d.ts +2 -2
  234. package/ui/Agent/Attach.tsx +13 -1
  235. package/ui/Agent/Chat.tsx +15 -47
  236. package/ui/Agent/ChatCommands.ts +2 -4
  237. package/ui/Agent/Composer.tsx +11 -2
  238. package/ui/Agent/attachment.ts +36 -8
  239. package/ui/Agent/useChatAttachments.ts +23 -8
  240. package/ui/Agent/useChatQueue.ts +6 -4
  241. package/ui/Agent/useSlashMenu.ts +9 -25
  242. package/ui/BottomSheet.tsx +72 -26
  243. package/ui/CsrImage.tsx +2 -2
  244. package/ui/Data/ListContainer.tsx +1 -1
  245. package/ui/Dialog/Modal.tsx +18 -9
  246. package/ui/Dropdown.tsx +21 -12
  247. package/ui/Empty.tsx +4 -2
  248. package/ui/Field/Label.tsx +5 -4
  249. package/ui/Field/Relation.tsx +4 -12
  250. package/ui/Field/index.tsx +2 -2
  251. package/ui/Image.tsx +3 -3
  252. package/ui/InfiniteScroll.tsx +6 -4
  253. package/ui/Layout/BottomTab.tsx +20 -12
  254. package/ui/Layout/LeftSider.tsx +17 -9
  255. package/ui/Layout/RightSider.tsx +18 -14
  256. package/ui/Layout/Sider.tsx +30 -23
  257. package/ui/Load/Units.tsx +14 -3
  258. package/ui/Load/View.tsx +8 -3
  259. package/ui/Loading/Area.tsx +19 -5
  260. package/ui/Menu.tsx +21 -14
  261. package/ui/Modal.tsx +17 -3
  262. package/ui/Model/Edit.tsx +12 -26
  263. package/ui/Model/EditModal.tsx +1 -1
  264. package/ui/Model/New.tsx +12 -21
  265. package/ui/Model/Remove.tsx +39 -16
  266. package/ui/Model/SureToRemove.tsx +45 -30
  267. package/ui/Model/ViewEditModal.tsx +34 -21
  268. package/ui/Pagination.tsx +18 -5
  269. package/ui/Popconfirm.tsx +30 -20
  270. package/ui/Select.tsx +218 -132
  271. package/ui/System/Messages.tsx +76 -122
  272. package/ui/Tab/Menu.tsx +1 -1
  273. package/ui/Table.tsx +16 -2
  274. package/ui/Toast.tsx +132 -0
  275. package/ui/ToggleSelect.tsx +15 -6
  276. package/ui/UiOverride/context.ts +4 -0
  277. package/ui/Unauthorized.tsx +10 -2
  278. package/ui/index.ts +18 -1
  279. package/ui/triggerSlot.tsx +52 -0
  280. package/vendor/use-agentic/AgentSession.ts +22 -92
  281. package/vendor/use-agentic/ToolOutput.ts +24 -0
  282. package/vendor/use-agentic/ToolRunner.ts +174 -0
  283. package/vendor/use-agentic/WIRE.md +6 -1
  284. package/vendor/use-agentic/index.ts +1 -0
  285. package/vendor/use-agentic/types.ts +7 -0
  286. package/webkit/bootCsr.tsx +8 -2
@@ -3,6 +3,7 @@ import type { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/sr
3
3
  import type { PromiseOrObject } from "akanjs/base";
4
4
  import { type ForwardRefExoticComponent, type ReactNode, type RefObject, useContext } from "react";
5
5
  import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } from "react-spring";
6
+ import type { RouteDefinition } from "./route/RouteDefinition";
6
7
  import type { RouterInstance } from "./router";
7
8
  import { sharedContext } from "./sharedContext";
8
9
  import type { ReactFont } from "./types";
@@ -134,6 +135,8 @@ export interface RouteRender {
134
135
  resolveHead?: ResolveHead;
135
136
  getPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
136
137
  getLayoutPageConfig?: () => PromiseOrObject<PageConfig | undefined>;
138
+ /** The `page()` chain behind a page render, when it was declared as one — what a page prompt is read off. */
139
+ getRouteDefinition?: () => PromiseOrObject<RouteDefinition | undefined>;
137
140
  }
138
141
  export interface WebAppManifestIcon {
139
142
  src: string;
package/client/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./frameConfig";
9
9
  export * from "./frameDebug";
10
10
  export * from "./locale";
11
11
  export * from "./makePageProto";
12
+ export * from "./route";
12
13
  export * from "./router";
13
14
  export * from "./rscNavigation";
14
15
  export * from "./sharedContext";
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from "react";
2
+ import type { LayoutErrorRender, LayoutModule, LayoutNotFoundRender, PageModule } from "../csrTypes";
3
+ import { type RouteArgsShape, RouteDefinition, type RouteKind } from "./RouteDefinition";
4
+ import type { RouteArgOption, RouteArgType, RouteArgValue, RouteSearchType } from "./routeArgs";
5
+
6
+ export interface LayoutExtra {
7
+ children: ReactNode;
8
+ }
9
+
10
+ export class LayoutDefinition<Args extends RouteArgsShape = Record<never, never>> extends RouteDefinition<
11
+ Args,
12
+ LayoutExtra
13
+ > {
14
+ readonly kind: RouteKind = "layout";
15
+ #notFound?: LayoutNotFoundRender;
16
+ #error?: LayoutErrorRender;
17
+
18
+ param<Name extends string, Type extends RouteArgType>(name: Name, type: Type, option: RouteArgOption = {}) {
19
+ this.declare({ kind: "param", name, type, list: false, desc: option.desc });
20
+ return this as unknown as LayoutDefinition<Args & { [Key in Name]: RouteArgValue<Type> }>;
21
+ }
22
+
23
+ search<Name extends string, Type extends RouteSearchType>(name: Name, type: Type, option: RouteArgOption = {}) {
24
+ const list = Array.isArray(type);
25
+ this.declare({ kind: "search", name, type: (list ? type[0] : type) as RouteArgType, list, desc: option.desc });
26
+ return this as unknown as LayoutDefinition<Args & { [Key in Name]?: RouteArgValue<Type> }>;
27
+ }
28
+
29
+ notFound(render: LayoutNotFoundRender) {
30
+ this.#notFound = render;
31
+ return this;
32
+ }
33
+
34
+ error(render: LayoutErrorRender) {
35
+ this.#error = render;
36
+ return this;
37
+ }
38
+
39
+ protected override extendModule(module: PageModule & LayoutModule): PageModule & LayoutModule {
40
+ return {
41
+ ...module,
42
+ ...(this.#notFound ? { NotFound: this.#notFound } : {}),
43
+ ...(this.#error ? { Error: this.#error } : {}),
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,53 @@
1
+ import { type RouteArgsShape, RouteDefinition } from "./RouteDefinition";
2
+ import type {
3
+ RouteArgOption,
4
+ RouteArgType,
5
+ RouteArgValue,
6
+ RoutePromptArgument,
7
+ RoutePromptMeta,
8
+ RouteSearchType,
9
+ } from "./routeArgs";
10
+
11
+ interface PagePrompt {
12
+ name: string;
13
+ description: string;
14
+ }
15
+
16
+ export class PageDefinition<Args extends RouteArgsShape = Record<never, never>> extends RouteDefinition<Args> {
17
+ /** MCP names an entry by `^[A-Za-z0-9_-]{1,64}$`; a prompt name outside it never reaches a client's slash menu. */
18
+ static readonly promptName = /^[A-Za-z0-9_-]{1,64}$/;
19
+ /** Appended to a list argument's description: the flat string map of `prompts/get` has no other spelling. */
20
+ static readonly commaSeparated = "Comma-separated list.";
21
+
22
+ readonly kind = "page" as const;
23
+ #prompt?: PagePrompt;
24
+
25
+ param<Name extends string, Type extends RouteArgType>(name: Name, type: Type, option: RouteArgOption = {}) {
26
+ this.declare({ kind: "param", name, type, list: false, desc: option.desc });
27
+ return this as unknown as PageDefinition<Args & { [Key in Name]: RouteArgValue<Type> }>;
28
+ }
29
+
30
+ search<Name extends string, Type extends RouteSearchType>(name: Name, type: Type, option: RouteArgOption = {}) {
31
+ const list = Array.isArray(type);
32
+ this.declare({ kind: "search", name, type: (list ? type[0] : type) as RouteArgType, list, desc: option.desc });
33
+ return this as unknown as PageDefinition<Args & { [Key in Name]?: RouteArgValue<Type> }>;
34
+ }
35
+
36
+ /** Publishes this screen as an MCP prompt: the description is the whole instruction a model receives. English. */
37
+ prompt(name: string, description: string) {
38
+ if (!PageDefinition.promptName.test(name))
39
+ throw new Error(`[route-convention] prompt name "${name}" must match ${PageDefinition.promptName}`);
40
+ if (!description.trim()) throw new Error(`[route-convention] prompt "${name}" needs a description`);
41
+ this.#prompt = { name, description };
42
+ return this;
43
+ }
44
+
45
+ get promptMeta(): RoutePromptMeta | undefined {
46
+ if (!this.#prompt) return undefined;
47
+ const args: RoutePromptArgument[] = this.args.map((arg) => {
48
+ const description = [arg.desc, arg.list ? PageDefinition.commaSeparated : undefined].filter(Boolean).join(" ");
49
+ return { name: arg.name, ...(description ? { description } : {}), required: arg.kind === "param" };
50
+ });
51
+ return { ...this.#prompt, arguments: args };
52
+ }
53
+ }
@@ -0,0 +1,58 @@
1
+ import type { LayoutModule, PageModule, WebAppManifest } from "../csrTypes";
2
+ import type { ReactFont } from "../types";
3
+ import { LayoutDefinition } from "./LayoutDefinition";
4
+ import type { RouteArgsShape, RouteKind } from "./RouteDefinition";
5
+
6
+ /** What only the root `_layout.tsx` of an app (or of a basePath) may set — the generated root layout reads it. */
7
+ export class RootLayoutDefinition<Args extends RouteArgsShape = Record<never, never>> extends LayoutDefinition<Args> {
8
+ override readonly kind: RouteKind = "rootLayout";
9
+ #fonts?: ReactFont[];
10
+ #manifest?: WebAppManifest;
11
+ #theme?: string;
12
+ #reconnect?: boolean;
13
+ #wsConnect?: boolean;
14
+ #layoutStyle?: "mobile" | "web";
15
+ #gaTrackingId?: string;
16
+
17
+ fonts(fonts: ReactFont[]) {
18
+ this.#fonts = fonts;
19
+ return this;
20
+ }
21
+ manifest(manifest: WebAppManifest) {
22
+ this.#manifest = manifest;
23
+ return this;
24
+ }
25
+ theme(theme: string) {
26
+ this.#theme = theme;
27
+ return this;
28
+ }
29
+ reconnect(on = true) {
30
+ this.#reconnect = on;
31
+ return this;
32
+ }
33
+ wsConnect(on = true) {
34
+ this.#wsConnect = on;
35
+ return this;
36
+ }
37
+ layoutStyle(style: "mobile" | "web") {
38
+ this.#layoutStyle = style;
39
+ return this;
40
+ }
41
+ gaTrackingId(id: string) {
42
+ this.#gaTrackingId = id;
43
+ return this;
44
+ }
45
+
46
+ protected override extendModule(module: PageModule & LayoutModule): PageModule & LayoutModule {
47
+ return {
48
+ ...super.extendModule(module),
49
+ ...(this.#fonts ? { fonts: this.#fonts } : {}),
50
+ ...(this.#manifest ? { manifest: this.#manifest } : {}),
51
+ ...(this.#theme !== undefined ? { theme: this.#theme } : {}),
52
+ ...(this.#reconnect !== undefined ? { reconnect: this.#reconnect } : {}),
53
+ ...(this.#wsConnect !== undefined ? { wsConnect: this.#wsConnect } : {}),
54
+ ...(this.#layoutStyle ? { layoutStyle: this.#layoutStyle } : {}),
55
+ ...(this.#gaTrackingId ? { gaTrackingId: this.#gaTrackingId } : {}),
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,204 @@
1
+ import {
2
+ type Cls,
3
+ type EnumInstance,
4
+ isEnum,
5
+ PrimitiveRegistry,
6
+ type PrimitiveScalar,
7
+ type PromiseOrObject,
8
+ } from "akanjs/base";
9
+ import { parseAkanI18nEnv } from "akanjs/common";
10
+ import { deserialize } from "akanjs/constant";
11
+ import type { ReactNode } from "react";
12
+ import type { AkanMetadata, Head, LayoutModule, PageConfig, PageModule } from "../csrTypes";
13
+ import { AkanNotFoundError } from "../router";
14
+ import { RouteArgError, type RouteArgInfo, type RouteArgInput, type RouteBaseArgs } from "./routeArgs";
15
+
16
+ export type RouteArgsShape = Record<string, unknown>;
17
+ export type RouteKind = "page" | "layout" | "rootLayout";
18
+ /** Interned: a definition built inside the pages bundle must be recognised by a loader bundled apart from it. */
19
+ export const routeDefinitionMarker = Symbol.for("akan.routeDefinition");
20
+
21
+ type HeadStage<Args> = Head | ((args: Args) => PromiseOrObject<Head | null | undefined>);
22
+ type MetadataStage<Args> = AkanMetadata | ((args: Args) => PromiseOrObject<AkanMetadata | null | undefined>);
23
+
24
+ interface RouteRenderProps {
25
+ params: Record<string, string>;
26
+ searchParams?: Record<string, string | string[]>;
27
+ children?: ReactNode;
28
+ }
29
+
30
+ export interface RouteArgResolveOption {
31
+ /** A prompt's arguments are strings a person typed: a list is comma-separated and a bad value is refused. */
32
+ strict?: boolean;
33
+ }
34
+
35
+ /**
36
+ * What a route file declares in place of named exports. The chain is the only surface — the module exports the
37
+ * finished definition and the loader turns it back into the module shape every renderer already reads.
38
+ */
39
+ export abstract class RouteDefinition<
40
+ Args extends RouteArgsShape = Record<never, never>,
41
+ Extra extends object = Record<never, never>,
42
+ > {
43
+ readonly [routeDefinitionMarker] = true;
44
+ abstract readonly kind: RouteKind;
45
+ readonly args: RouteArgInfo[] = [];
46
+ #config?: PageConfig;
47
+
48
+ #head?: HeadStage<never>;
49
+ #metadata?: MetadataStage<never>;
50
+ #loading?: (args: never) => PromiseOrObject<ReactNode>;
51
+ #render?: (args: never) => PromiseOrObject<ReactNode>;
52
+
53
+ config(config: PageConfig) {
54
+ this.#config = config;
55
+ return this;
56
+ }
57
+ head(head: HeadStage<RouteBaseArgs & Args>) {
58
+ this.#head = head as HeadStage<never>;
59
+ return this;
60
+ }
61
+ metadata(metadata: MetadataStage<RouteBaseArgs & Args>) {
62
+ this.#metadata = metadata as MetadataStage<never>;
63
+ return this;
64
+ }
65
+ loading(render: (args: RouteBaseArgs & Args & Extra) => PromiseOrObject<ReactNode>) {
66
+ this.#loading = render as (args: never) => PromiseOrObject<ReactNode>;
67
+ return this;
68
+ }
69
+ render(render: (args: RouteBaseArgs & Args & Extra) => PromiseOrObject<ReactNode>) {
70
+ this.#render = render as (args: never) => PromiseOrObject<ReactNode>;
71
+ return this;
72
+ }
73
+
74
+ get pageConfig() {
75
+ return this.#config;
76
+ }
77
+
78
+ protected declare(arg: RouteArgInfo) {
79
+ if (arg.name === "lang")
80
+ throw new Error(`[route-convention] ${this.kind}() receives "lang" on every route; it is never declared`);
81
+ if (this.args.some((existing) => existing.name === arg.name))
82
+ throw new Error(`[route-convention] ${this.kind}() declares "${arg.name}" twice`);
83
+ this.args.push(arg);
84
+ }
85
+
86
+ /**
87
+ * Typed values out of the URL halves the router matched. A path value the declared type refuses is a URL that
88
+ * matched the pattern and nothing under it, so a page answers not-found; a search value that fails is dropped
89
+ * the way an absent one is. Under `strict` both are refused by name, which is what a prompt caller can act on.
90
+ */
91
+ resolveArgs(input: RouteArgInput, { strict = false }: RouteArgResolveOption = {}): Record<string, unknown> {
92
+ const resolved: Record<string, unknown> = {};
93
+ for (const arg of this.args) {
94
+ const raw = arg.kind === "param" ? input.params[arg.name] : input.searchParams[arg.name];
95
+ if (raw === undefined || raw === "") {
96
+ if (arg.kind === "param") throw new RouteArgError(arg, `Missing required argument "${arg.name}".`);
97
+ continue;
98
+ }
99
+ const value = RouteDefinition.#lift(arg, raw, strict);
100
+ try {
101
+ resolved[arg.name] = RouteDefinition.#parse(arg, value);
102
+ } catch {
103
+ if (strict) throw new RouteArgError(arg, RouteDefinition.#invalidMessage(arg));
104
+ if (arg.kind === "param") throw new AkanNotFoundError();
105
+ }
106
+ }
107
+ return resolved;
108
+ }
109
+
110
+ /**
111
+ * The `[x]` segments of the file's path against the `.param()` stages. A page must name every segment it sits
112
+ * under, so nothing reaches its body undeclared; a layout may name a subset, since most read none of them.
113
+ */
114
+ assertPattern(pattern: string, key: string) {
115
+ const inPath = pattern
116
+ .split("/")
117
+ .filter((part) => part.startsWith(":"))
118
+ .map((part) => part.slice(1))
119
+ .filter((name) => name !== "lang");
120
+ const declared = this.args.filter((arg) => arg.kind === "param").map((arg) => arg.name);
121
+ const unknown = declared.find((name) => !inPath.includes(name));
122
+ if (unknown)
123
+ throw new Error(
124
+ `[route-convention] ${key} declares .param("${unknown}") but no [${unknown}] segment is in its path`,
125
+ );
126
+ const undeclared = inPath.find((name) => !declared.includes(name));
127
+ if (this.kind === "page" && undeclared)
128
+ throw new Error(
129
+ `[route-convention] ${key} sits under [${undeclared}] but declares no .param("${undeclared}") — a page reads only what it declares`,
130
+ );
131
+ }
132
+
133
+ /** The module shape every route loader reads, so a chain and a legacy module walk one path from here on. */
134
+ toRouteModule(): PageModule & LayoutModule {
135
+ const render = this.#render;
136
+ if (!render)
137
+ throw new Error(`[route-convention] a ${this.kind}() chain ends with .render(), and this one has none`);
138
+ const head = this.#head;
139
+ const metadata = this.#metadata;
140
+ const loading = this.#loading;
141
+ const module: PageModule & LayoutModule = {
142
+ default: (async (props: RouteRenderProps) => await render(this.#argsOf(props) as never)) as never,
143
+ ...(this.#config ? { pageConfig: this.#config } : {}),
144
+ ...(head === undefined
145
+ ? {}
146
+ : typeof head === "function"
147
+ ? { generateHead: async (props: RouteRenderProps) => await head(this.#argsOf(props) as never) }
148
+ : { head }),
149
+ ...(metadata === undefined
150
+ ? {}
151
+ : typeof metadata === "function"
152
+ ? { generateMetadata: async (props: RouteRenderProps) => await metadata(this.#argsOf(props) as never) }
153
+ : { metadata }),
154
+ ...(loading
155
+ ? {
156
+ Loading: ((props: RouteRenderProps) =>
157
+ loading(this.#argsOf({ ...props, searchParams: {} }) as never)) as never,
158
+ }
159
+ : {}),
160
+ };
161
+ return this.extendModule(module);
162
+ }
163
+
164
+ protected extendModule(module: PageModule & LayoutModule): PageModule & LayoutModule {
165
+ return module;
166
+ }
167
+
168
+ #argsOf(props: RouteRenderProps): RouteBaseArgs & Args & Extra {
169
+ const { lang = parseAkanI18nEnv().defaultLocale } = props.params;
170
+ const resolved = this.resolveArgs({ params: props.params, searchParams: props.searchParams ?? {} });
171
+ const args = { lang, ...resolved };
172
+ return (this.kind === "page" ? args : { ...args, children: props.children }) as RouteBaseArgs & Args & Extra;
173
+ }
174
+
175
+ /**
176
+ * A single value where a list was declared is a one-item list: the URL spells `?tags=a` for one tag. A prompt
177
+ * argument is one string whatever it holds, so under `strict` a list is read comma-separated.
178
+ */
179
+ static #lift(arg: RouteArgInfo, raw: string | string[], strict: boolean): unknown {
180
+ if (!arg.list) return Array.isArray(raw) ? raw[0] : raw;
181
+ if (Array.isArray(raw)) return raw;
182
+ return strict
183
+ ? raw
184
+ .split(",")
185
+ .map((item) => item.trim())
186
+ .filter(Boolean)
187
+ : [raw];
188
+ }
189
+
190
+ static #parse(arg: RouteArgInfo, value: unknown): unknown {
191
+ const enumRef = isEnum(arg.type as Cls) ? (arg.type as EnumInstance) : undefined;
192
+ const scalar = (enumRef ? enumRef.type : arg.type) as typeof PrimitiveScalar;
193
+ return deserialize(scalar as never, arg.list ? 1 : 0, value, { key: arg.name, enum: enumRef });
194
+ }
195
+
196
+ static #invalidMessage(arg: RouteArgInfo): string {
197
+ if (isEnum(arg.type as Cls))
198
+ return `Invalid argument "${arg.name}": expected one of ${(arg.type as EnumInstance).values.join(", ")}.`;
199
+ const name = PrimitiveRegistry.has(arg.type as Cls)
200
+ ? PrimitiveRegistry.getName(arg.type as typeof PrimitiveScalar)
201
+ : "a value";
202
+ return `Invalid argument "${arg.name}": expected ${name}${arg.list ? "[]" : ""}.`;
203
+ }
204
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./LayoutDefinition";
2
+ export * from "./PageDefinition";
3
+ export * from "./RootLayoutDefinition";
4
+ export * from "./RouteDefinition";
5
+ export * from "./resolveRouteModule";
6
+ export * from "./routeArgs";
7
+ export * from "./routeBuilders";
@@ -0,0 +1,46 @@
1
+ import type { RouteModule } from "../csrTypes";
2
+ import { type RouteDefinition, routeDefinitionMarker } from "./RouteDefinition";
3
+
4
+ export interface ResolveRouteModuleOption {
5
+ /** The file kind the route convention derived from the path, so `page()` in a `_layout.tsx` is caught here. */
6
+ kind?: "page" | "layout" | "overrides";
7
+ /** The matched route pattern (`/:lang/project/:projectId`), checked against the chain's `.param()` stages. */
8
+ pattern?: string;
9
+ }
10
+
11
+ /** What a route file's module may be: the named-export shape, or one default export holding a chain. */
12
+ export type RouteModuleSource = RouteModule | { default: RouteDefinition };
13
+
14
+ export interface ResolvedRouteModule {
15
+ module: RouteModule;
16
+ /** Present when the module's default export was a `page()` / `layout()` / `rootLayout()` chain. */
17
+ definition?: RouteDefinition;
18
+ }
19
+
20
+ export const isRouteDefinition = (value: unknown): value is RouteDefinition =>
21
+ typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[routeDefinitionMarker] === true;
22
+
23
+ /**
24
+ * Every loader — the RSC worker, the CSR boot, the generated root layout — reads route modules by their named
25
+ * exports. A chain module has one export, so it is unfolded into that shape here and nothing downstream learns
26
+ * which of the two it was reading; a legacy module passes through untouched.
27
+ */
28
+ export const resolveRouteModule = (
29
+ mod: RouteModuleSource,
30
+ key: string,
31
+ { kind, pattern }: ResolveRouteModuleOption = {},
32
+ ): ResolvedRouteModule => {
33
+ const definition: unknown = mod.default;
34
+ if (!isRouteDefinition(definition)) return { module: mod as RouteModule };
35
+ const named = Object.keys(mod).filter((name) => name !== "default");
36
+ if (named.length)
37
+ throw new Error(
38
+ `[route-convention] ${key} exports ${named.join(", ")} beside its ${definition.kind}() chain — every route setting is a stage of the chain`,
39
+ );
40
+ if (kind === "page" && definition.kind !== "page")
41
+ throw new Error(`[route-convention] ${key} is a page file but exports ${definition.kind}()`);
42
+ if (kind === "layout" && definition.kind === "page")
43
+ throw new Error(`[route-convention] ${key} is a layout file but exports page()`);
44
+ if (pattern) definition.assertPattern(pattern, key);
45
+ return { module: definition.toRouteModule(), definition };
46
+ };
@@ -0,0 +1,51 @@
1
+ import type { FieldToValue, ParamFieldType } from "akanjs/constant";
2
+
3
+ /** A scalar (`ID`, `String`, `Int`, `Float`, `Boolean`, `Date`) or an `enumOf` class — what a URL segment can carry. */
4
+ export type RouteArgType = ParamFieldType;
5
+ /** A search argument may also be a flat list, written `[String]`; the URL repeats the key or comma-separates it. */
6
+ export type RouteSearchType = RouteArgType | [RouteArgType];
7
+ export type RouteArgValue<T> = T extends [infer Item] ? FieldToValue<Item>[] : FieldToValue<T>;
8
+
9
+ export interface RouteArgOption {
10
+ /** What the value is, read by the person filling the MCP prompt form in — so English. */
11
+ desc?: string;
12
+ }
13
+
14
+ /** Every route sits under `/:lang`, so the locale segment reaches each stage as `lang` with no `.param()` for it. */
15
+ export interface RouteBaseArgs {
16
+ lang: string;
17
+ }
18
+
19
+ export interface RouteArgInfo {
20
+ kind: "param" | "search";
21
+ name: string;
22
+ type: RouteArgType;
23
+ list: boolean;
24
+ desc?: string;
25
+ }
26
+
27
+ export interface RouteArgInput {
28
+ params: Record<string, string>;
29
+ searchParams: Record<string, string | string[]>;
30
+ }
31
+
32
+ /** What the page prompt catalogue publishes for one declared argument. */
33
+ export interface RoutePromptArgument {
34
+ name: string;
35
+ description?: string;
36
+ required: boolean;
37
+ }
38
+
39
+ export interface RoutePromptMeta {
40
+ name: string;
41
+ description: string;
42
+ arguments: RoutePromptArgument[];
43
+ }
44
+
45
+ export class RouteArgError extends Error {
46
+ readonly arg: RouteArgInfo;
47
+ constructor(arg: RouteArgInfo, message: string) {
48
+ super(message);
49
+ this.arg = arg;
50
+ }
51
+ }
@@ -0,0 +1,10 @@
1
+ import { LayoutDefinition } from "./LayoutDefinition";
2
+ import { PageDefinition } from "./PageDefinition";
3
+ import { RootLayoutDefinition } from "./RootLayoutDefinition";
4
+
5
+ /** `export default page().param(…).search(…).config(…).prompt(…).render(async (args) => …)` in a route page file. */
6
+ export const page = () => new PageDefinition();
7
+ /** `export default layout().render(({ children }) => …)` in a `_layout.tsx`. */
8
+ export const layout = () => new LayoutDefinition();
9
+ /** The app's (or a basePath's) root `_layout.tsx`: `layout()` plus fonts, manifest, theme and the socket switches. */
10
+ export const rootLayout = () => new RootLayoutDefinition();
@@ -51,9 +51,14 @@ export class TrustedProxy {
51
51
  /**
52
52
  * The caller's address as recorded by a proxy, but only when the peer is one. Falls back to the peer itself,
53
53
  * which is the right answer for a process nothing is proxying, and `null` when there is no peer either.
54
+ *
55
+ * `null` and `undefined` mean different things here. `null` is what `Server.requestIP` answers for a socket
56
+ * that has no address — a unix socket, which is how a gateway reaches its children — and only a process on
57
+ * this machine can open one, so the headers it wrote are a local proxy's and are believed. `undefined` is
58
+ * "nobody asked": no resolver was registered, and a header from an unknown peer is a header the client wrote.
54
59
  */
55
60
  static clientAddress(headers: Headers, peerAddress: string | null | undefined): string | null {
56
- if (TrustedProxy.isTrusted(peerAddress)) {
61
+ if (peerAddress === null || TrustedProxy.isTrusted(peerAddress)) {
57
62
  const realIp = headers.get("x-real-ip")?.trim();
58
63
  if (realIp) return normalizeIpAddress(realIp);
59
64
  const forwarded = headers.get("x-forwarded-for")?.split(",")[0]?.trim();
@@ -62,6 +67,23 @@ export class TrustedProxy {
62
67
  return peerAddress ? normalizeIpAddress(peerAddress) : null;
63
68
  }
64
69
 
70
+ /**
71
+ * Whether an address is one no public name should resolve to: the local ranges `isTrusted` reads, plus the
72
+ * unspecified address, the carrier-grade NAT block `100.64/10`, multicast and the reserved top of IPv4. Used
73
+ * to refuse a fetch whose destination a caller chose by name, so it judges the address alone and never the
74
+ * configured proxy list — a trusted proxy is still not somewhere to send an outbound request.
75
+ */
76
+ static isPrivateAddress(address: string): boolean {
77
+ const bytes = TrustedProxy.#toBytes(normalizeIpAddress(address));
78
+ if (!bytes) return true;
79
+ if (TrustedProxy.#isLocalRange(bytes)) return true;
80
+ if (bytes.length === 4) {
81
+ const [a = 0, b = 0] = bytes;
82
+ return a === 0 || (a === 100 && b >= 64 && b <= 127) || a >= 224;
83
+ }
84
+ return bytes.every((byte) => byte === 0) || bytes[0] === 0xff;
85
+ }
86
+
65
87
  /** `10/8`, `172.16/12`, `192.168/16`, `127/8`, `169.254/16`, and their IPv6 counterparts `::1`, `fc00::/7`, `fe80::/10`. */
66
88
  static #isLocalRange(bytes: Uint8Array): boolean {
67
89
  if (bytes.length === 4) {
package/common/index.ts CHANGED
@@ -71,7 +71,6 @@ export {
71
71
  type McpExposureOption,
72
72
  mcpBaseVerbOf,
73
73
  mcpHintsOf,
74
- mcpPromptRefusalOf,
75
74
  mcpRefusalOf,
76
75
  } from "./mcpExposure";
77
76
  export { mergeVersion } from "./mergeVersion";
@@ -17,6 +17,8 @@ export interface McpExposureEndpoint {
17
17
  guards?: string[];
18
18
  fileUpload?: boolean;
19
19
  mcp?: boolean;
20
+ /** `false` when a guard declares `static agents = false`: no model may ever pass, whatever else the guards say. */
21
+ agents?: boolean;
20
22
  }
21
23
 
22
24
  export interface McpExposureOption {
@@ -68,12 +70,14 @@ export const mcpRefusalOf = (
68
70
  if (endpoint.mcp === false)
69
71
  return "it declares `mcp: false`, so it is deliberately off the agent shelf. HTTP still serves it.";
70
72
 
73
+ if (endpoint.agents === false)
74
+ return `its guards (${(endpoint.guards ?? []).join(", ")}) admit no agent — an act reserved for a person, so it is off the agent shelf. HTTP still serves it.`;
75
+
71
76
  if (!endpoint.guards?.length)
72
77
  return "it declares no guards, and exposure follows them — write `guards: [Public]` if anonymous access is the intent.";
73
78
 
74
79
  if (key === `light${capitalize(refName)}`)
75
80
  return `it reads the same document as \`${refName}\` in a smaller shape — call \`${refName}\` instead.`;
76
- if (endpoint.type === "prompt") return mcpPromptRefusalOf(endpoint);
77
81
  if (endpoint.type === "pubsub" || endpoint.type === "message")
78
82
  return `\`${endpoint.type}\` rides the websocket, and its internal arguments read a socket an MCP request does not have.`;
79
83
  if (readOnly && endpoint.type !== "query")
@@ -91,24 +95,3 @@ export const mcpRefusalOf = (
91
95
  return `its required argument \`${opaque.name}\` is typed \`Any\`, which is left out of the published schema — expose a named filter slice instead.`;
92
96
  return null;
93
97
  };
94
-
95
- /**
96
- * A prompt is a read exposed on the same terms as a query, so every rejection here is one thing: an argument
97
- * `prompts/get` cannot carry. Its `arguments` is a flat string map — one string per name, and no schema beside it
98
- * — which rules out the argument *kinds* the builder already refuses and, just as surely, two argument *types* it
99
- * accepts. A tool escapes both because it publishes a real JSON Schema.
100
- */
101
- export const mcpPromptRefusalOf = (endpoint: McpExposureEndpoint): string | null => {
102
- const carried = endpoint.args.find((arg) => arg.type === "body" || arg.type === "msg" || arg.type === "room");
103
- if (carried)
104
- return `a prompt's arguments travel as a flat string map, so its \`${carried.type}\` argument \`${carried.name}\` cannot be carried.`;
105
-
106
- const list = endpoint.args.find((arg) => arg.arrDepth);
107
- if (list)
108
- return `a prompt argument is one string, so its list argument \`${list.name}\` could never carry more than one value.`;
109
-
110
- const opaque = endpoint.args.find((arg) => !isMcpDescribableArg(arg));
111
- if (opaque)
112
- return `its argument \`${opaque.name}\` is typed \`Any\`, and a prompt has no schema in which to describe one.`;
113
- return null;
114
- };