akanjs 3.1.0-rc.4 → 3.1.0-rc.6

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 (235) hide show
  1. package/base/baseEnv.ts +5 -2
  2. package/client/device.ts +1 -1
  3. package/client/frameConfig.ts +2 -2
  4. package/client/router.ts +3 -3
  5. package/client/rscNavigation.ts +4 -1
  6. package/client/types.ts +3 -2
  7. package/client/webManifest.ts +5 -3
  8. package/common/TrustedProxy.ts +14 -0
  9. package/common/index.ts +1 -0
  10. package/common/interpolateTranslation.ts +17 -3
  11. package/common/isEmail.ts +1 -1
  12. package/common/localeConfig.ts +10 -4
  13. package/common/markdownSpans.ts +1 -1
  14. package/common/restClient.ts +3 -1
  15. package/common/routeConvention.ts +4 -4
  16. package/common/trimSlashes.ts +11 -0
  17. package/common/types.ts +1 -1
  18. package/constant/fieldInfo.ts +2 -2
  19. package/constant/fieldPlan.ts +80 -0
  20. package/constant/index.ts +3 -0
  21. package/constant/inputParser.ts +148 -0
  22. package/constant/invalidInputError.ts +13 -0
  23. package/constant/serialize.ts +36 -20
  24. package/constant/types.ts +1 -1
  25. package/constant/via.ts +2 -0
  26. package/dictionary/base.dictionary.ts +3 -0
  27. package/dictionary/trans.ts +8 -0
  28. package/document/database.ts +3 -6
  29. package/document/documentSchema.ts +1 -1
  30. package/fetch/client/fetchClient.ts +2 -1
  31. package/fetch/fetchType/appliedReturn.type.ts +3 -1
  32. package/fetch/types.ts +4 -2
  33. package/package.json +1 -1
  34. package/server/akanApp.ts +5 -2
  35. package/server/akanServer.ts +34 -14
  36. package/server/artifact/routeClientCache.ts +4 -5
  37. package/server/artifact/routeSeedIndexStore.ts +3 -20
  38. package/server/artifact/routeSeedMatcher.ts +70 -0
  39. package/server/console.ts +1 -1
  40. package/server/di/diLifecycle.ts +16 -7
  41. package/server/imageOptimizer.ts +1 -1
  42. package/server/machine/machineAccount.ts +28 -3
  43. package/server/machine/machineToken.ts +27 -3
  44. package/server/mcp/McpExecutionContext.ts +9 -3
  45. package/server/resolver/signal.resolver.ts +35 -55
  46. package/server/rscClient.tsx +22 -19
  47. package/server/rscNavigationState.ts +3 -5
  48. package/server/rscScrollRestoration.ts +116 -0
  49. package/server/sitemap.ts +4 -4
  50. package/server/tunnel/TunnelAgent.ts +9 -1
  51. package/server/webRouter.ts +17 -2
  52. package/service/injectInfo.ts +2 -2
  53. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +6 -2
  54. package/service/predefinedAdaptor/sql/dialect/postgres.ts +0 -1
  55. package/service/predefinedAdaptor/sql/dialect/sqlite.ts +0 -1
  56. package/signal/endpointCache.ts +7 -1
  57. package/signal/endpointErrorContract.ts +37 -0
  58. package/signal/endpointIdempotency.ts +5 -3
  59. package/signal/endpointInfo.ts +4 -1
  60. package/signal/endpointRateLimit.ts +6 -2
  61. package/signal/index.ts +1 -0
  62. package/signal/mcp/McpDocument.ts +16 -1
  63. package/signal/mcp/McpUriTemplate.ts +1 -1
  64. package/signal/middlewareChain.ts +105 -0
  65. package/signal/openapi/openapi.ts +46 -2
  66. package/signal/serializer/fetch.serializer.ts +1 -0
  67. package/signal/signalContext.ts +104 -81
  68. package/signal/slice.ts +2 -2
  69. package/signal/trace.ts +8 -2
  70. package/signal/types.ts +13 -0
  71. package/store/agent/ScreenReader.ts +2 -1
  72. package/store/stateBuilder.ts +1 -1
  73. package/store/storeInstance.ts +2 -2
  74. package/store/withSelector.ts +2 -2
  75. package/types/client/rscNavigation.d.ts +1 -0
  76. package/types/common/index.d.ts +1 -0
  77. package/types/common/trimSlashes.d.ts +2 -0
  78. package/types/common/types.d.ts +1 -1
  79. package/types/constant/fieldInfo.d.ts +2 -2
  80. package/types/constant/fieldPlan.d.ts +33 -0
  81. package/types/constant/index.d.ts +3 -0
  82. package/types/constant/inputParser.d.ts +11 -0
  83. package/types/constant/invalidInputError.d.ts +8 -0
  84. package/types/constant/types.d.ts +1 -1
  85. package/types/dictionary/base.dictionary.d.ts +1 -1
  86. package/types/dictionary/dictionary.d.ts +3 -3
  87. package/types/dictionary/trans.d.ts +3 -0
  88. package/types/document/database.d.ts +2 -3
  89. package/types/document/documentSchema.d.ts +1 -1
  90. package/types/fetch/fetchType/appliedReturn.type.d.ts +3 -1
  91. package/types/fetch/types.d.ts +2 -2
  92. package/types/server/akanServer.d.ts +1 -1
  93. package/types/server/artifact/routeSeedMatcher.d.ts +8 -0
  94. package/types/server/di/diLifecycle.d.ts +1 -0
  95. package/types/server/machine/machineAccount.d.ts +1 -0
  96. package/types/server/machine/machineToken.d.ts +9 -0
  97. package/types/server/rscNavigationState.d.ts +2 -3
  98. package/types/server/rscScrollRestoration.d.ts +14 -0
  99. package/types/server/sitemap.d.ts +1 -1
  100. package/types/service/injectInfo.d.ts +2 -2
  101. package/types/signal/endpointCache.d.ts +2 -0
  102. package/types/signal/endpointErrorContract.d.ts +10 -0
  103. package/types/signal/endpointIdempotency.d.ts +1 -0
  104. package/types/signal/endpointRateLimit.d.ts +1 -1
  105. package/types/signal/index.d.ts +1 -0
  106. package/types/signal/middlewareChain.d.ts +11 -0
  107. package/types/signal/signalContext.d.ts +2 -6
  108. package/types/signal/slice.d.ts +1 -1
  109. package/types/signal/trace.d.ts +1 -1
  110. package/types/signal/types.d.ts +12 -0
  111. package/types/store/stateBuilder.d.ts +1 -1
  112. package/types/store/storeInstance.d.ts +2 -2
  113. package/types/store/withSelector.d.ts +1 -1
  114. package/types/ui/Data/CardList.d.ts +1 -1
  115. package/types/ui/Data/Dashboard.d.ts +1 -1
  116. package/types/ui/Data/Insight.d.ts +1 -1
  117. package/types/ui/Data/Item.d.ts +2 -2
  118. package/types/ui/Data/ListContainer.d.ts +1 -1
  119. package/types/ui/Data/Pagination.d.ts +1 -1
  120. package/types/ui/Data/TableList.d.ts +1 -1
  121. package/types/ui/Field/Relation.d.ts +13 -10
  122. package/types/ui/Field/index.d.ts +4 -4
  123. package/types/ui/Field/useRelationOptions.d.ts +3 -1
  124. package/types/ui/Field/useRelationWindow.d.ts +14 -0
  125. package/types/ui/Load/View.d.ts +1 -1
  126. package/types/ui/Pagination.d.ts +1 -1
  127. package/types/ui/Signal/Arg.d.ts +1 -1
  128. package/types/ui/System/CSR.d.ts +1 -1
  129. package/types/ui/System/Client.d.ts +1 -1
  130. package/types/ui/System/SSR.d.ts +1 -1
  131. package/types/vendor/@akanjs/native/packages/core/src/plugin.d.ts +3 -3
  132. package/types/vendor/@akanjs/native/packages/desktop/src/page-veto.d.ts +1 -1
  133. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +1 -1
  134. package/types/vendor/@akanjs/native/plugins/auth-session/src/desktop.d.ts +2 -2
  135. package/types/vendor/@akanjs/native/plugins/auth-session/src/index.d.ts +1 -1
  136. package/types/vendor/@akanjs/native/plugins/autostart/src/desktop.d.ts +2 -2
  137. package/types/vendor/@akanjs/native/plugins/autostart/src/index.d.ts +1 -1
  138. package/types/vendor/@akanjs/native/plugins/badge/src/desktop.d.ts +1 -1
  139. package/types/vendor/@akanjs/native/plugins/badge/src/index.d.ts +1 -1
  140. package/types/vendor/@akanjs/native/plugins/badge/src/web.d.ts +1 -1
  141. package/types/vendor/@akanjs/native/plugins/biometric/src/index.d.ts +1 -1
  142. package/types/vendor/@akanjs/native/plugins/camera/src/desktop.d.ts +1 -1
  143. package/types/vendor/@akanjs/native/plugins/camera/src/index.d.ts +1 -1
  144. package/types/vendor/@akanjs/native/plugins/camera/src/web.d.ts +1 -1
  145. package/types/vendor/@akanjs/native/plugins/clipboard/src/desktop.d.ts +2 -2
  146. package/types/vendor/@akanjs/native/plugins/clipboard/src/index.d.ts +1 -1
  147. package/types/vendor/@akanjs/native/plugins/clipboard/src/web.d.ts +1 -1
  148. package/types/vendor/@akanjs/native/plugins/contacts/src/index.d.ts +1 -1
  149. package/types/vendor/@akanjs/native/plugins/device/src/desktop.d.ts +2 -2
  150. package/types/vendor/@akanjs/native/plugins/device/src/index.d.ts +1 -1
  151. package/types/vendor/@akanjs/native/plugins/device/src/web.d.ts +1 -1
  152. package/types/vendor/@akanjs/native/plugins/dialog/src/desktop.d.ts +1 -1
  153. package/types/vendor/@akanjs/native/plugins/dialog/src/index.d.ts +1 -1
  154. package/types/vendor/@akanjs/native/plugins/dialog/src/web.d.ts +1 -1
  155. package/types/vendor/@akanjs/native/plugins/dock/src/desktop.d.ts +1 -1
  156. package/types/vendor/@akanjs/native/plugins/dock/src/index.d.ts +1 -1
  157. package/types/vendor/@akanjs/native/plugins/file-picker/src/desktop.d.ts +2 -2
  158. package/types/vendor/@akanjs/native/plugins/file-picker/src/web.d.ts +1 -1
  159. package/types/vendor/@akanjs/native/plugins/filesystem/src/desktop.d.ts +2 -2
  160. package/types/vendor/@akanjs/native/plugins/filesystem/src/web.d.ts +1 -1
  161. package/types/vendor/@akanjs/native/plugins/haptics/src/index.d.ts +1 -1
  162. package/types/vendor/@akanjs/native/plugins/haptics/src/web.d.ts +1 -1
  163. package/types/vendor/@akanjs/native/plugins/http/src/desktop.d.ts +2 -2
  164. package/types/vendor/@akanjs/native/plugins/http/src/web.d.ts +1 -1
  165. package/types/vendor/@akanjs/native/plugins/keep-awake/src/desktop.d.ts +2 -2
  166. package/types/vendor/@akanjs/native/plugins/keep-awake/src/index.d.ts +1 -1
  167. package/types/vendor/@akanjs/native/plugins/keep-awake/src/web.d.ts +1 -1
  168. package/types/vendor/@akanjs/native/plugins/opener/src/desktop.d.ts +2 -2
  169. package/types/vendor/@akanjs/native/plugins/opener/src/index.d.ts +1 -1
  170. package/types/vendor/@akanjs/native/plugins/opener/src/web.d.ts +1 -1
  171. package/types/vendor/@akanjs/native/plugins/preferences/src/desktop.d.ts +1 -1
  172. package/types/vendor/@akanjs/native/plugins/preferences/src/index.d.ts +1 -1
  173. package/types/vendor/@akanjs/native/plugins/preferences/src/web.d.ts +1 -1
  174. package/types/vendor/@akanjs/native/plugins/secure-storage/src/desktop.d.ts +2 -2
  175. package/types/vendor/@akanjs/native/plugins/secure-storage/src/index.d.ts +1 -1
  176. package/types/vendor/@akanjs/native/plugins/share/src/index.d.ts +1 -1
  177. package/types/vendor/@akanjs/native/plugins/share/src/web.d.ts +1 -1
  178. package/types/vendor/@akanjs/native/plugins/single-instance/src/desktop.d.ts +2 -2
  179. package/types/vendor/@akanjs/native/plugins/splash-screen/src/index.d.ts +1 -1
  180. package/types/vendor/@akanjs/native/plugins/splash-screen/src/web.d.ts +1 -1
  181. package/types/vendor/@akanjs/native/plugins/sqlite/src/desktop.d.ts +2 -2
  182. package/types/vendor/@akanjs/native/plugins/toast/src/index.d.ts +1 -1
  183. package/types/vendor/@akanjs/native/plugins/toast/src/web.d.ts +1 -1
  184. package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +2 -2
  185. package/types/vendor/@akanjs/native/plugins/window-state/src/desktop.d.ts +2 -2
  186. package/types/vendor/@akanjs/native/plugins/window-state/src/index.d.ts +1 -1
  187. package/types/vendor/use-agentic/Reference.d.ts +4 -1
  188. package/ui/Data/CardList.tsx +1 -1
  189. package/ui/Data/Dashboard.tsx +1 -1
  190. package/ui/Data/Insight.tsx +1 -1
  191. package/ui/Data/Item.tsx +2 -2
  192. package/ui/Data/ListContainer.tsx +2 -2
  193. package/ui/Data/Pagination.tsx +1 -1
  194. package/ui/Data/TableList.tsx +1 -1
  195. package/ui/Field/Relation.tsx +33 -14
  196. package/ui/Field/useRelationOptions.ts +33 -5
  197. package/ui/Field/useRelationWindow.ts +50 -0
  198. package/ui/Load/View.tsx +2 -2
  199. package/ui/Model/ViewEditModal.tsx +1 -1
  200. package/ui/Model/ViewModal.tsx +2 -2
  201. package/ui/Pagination.tsx +1 -1
  202. package/ui/Signal/Arg.tsx +1 -3
  203. package/ui/System/CSR.tsx +1 -1
  204. package/ui/System/Client.tsx +1 -1
  205. package/ui/System/Reconnect.tsx +1 -1
  206. package/ui/System/SSR.tsx +1 -1
  207. package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +4 -1
  208. package/vendor/@akanjs/native/packages/cli/src/lib/apk.ts +2 -2
  209. package/vendor/@akanjs/native/packages/cli/src/lib/codegen.ts +1 -1
  210. package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +4 -3
  211. package/vendor/@akanjs/native/packages/cli/src/lib/googleservices.ts +7 -1
  212. package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +8 -5
  213. package/vendor/@akanjs/native/packages/cli/src/lib/html.ts +18 -3
  214. package/vendor/@akanjs/native/packages/cli/src/lib/manifestmerge.ts +1 -1
  215. package/vendor/@akanjs/native/packages/cli/src/lib/plist.ts +2 -4
  216. package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +1 -1
  217. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +10 -4
  218. package/vendor/@akanjs/native/packages/cli/src/lib/xml.ts +4 -3
  219. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +3 -3
  220. package/vendor/@akanjs/native/packages/core/src/kernel.ts +3 -3
  221. package/vendor/@akanjs/native/packages/core/src/live.ts +1 -0
  222. package/vendor/@akanjs/native/packages/core/src/plugin.ts +5 -3
  223. package/vendor/@akanjs/native/packages/desktop/src/host.ts +13 -5
  224. package/vendor/@akanjs/native/packages/desktop/src/lifecycle.ts +1 -0
  225. package/vendor/@akanjs/native/packages/desktop/src/page-veto.ts +1 -1
  226. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +4 -1
  227. package/vendor/@akanjs/native/plugins/camera/src/web.ts +3 -3
  228. package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +4 -1
  229. package/vendor/@akanjs/native/plugins/local-notifications/src/web.ts +1 -1
  230. package/vendor/@akanjs/native/plugins/secure-storage/src/desktop.ts +1 -1
  231. package/vendor/@akanjs/native/plugins/share/src/options.ts +1 -1
  232. package/vendor/@akanjs/native/plugins/single-instance/src/desktop.ts +3 -2
  233. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +4 -1
  234. package/vendor/@akanjs/native/plugins/window/src/index.ts +2 -2
  235. package/vendor/use-agentic/Reference.ts +5 -2
package/base/baseEnv.ts CHANGED
@@ -81,8 +81,11 @@ const globalWithPrefix = globalThis as typeof globalThis & { __AKAN_PREFIX__?: R
81
81
  export const normalizeRoutePrefix = (value: string | undefined | null): string | undefined => {
82
82
  const trimmed = value?.trim();
83
83
  if (!trimmed) return undefined;
84
- const normalized = `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
85
- return normalized === "/" ? undefined : normalized;
84
+ let start = 0;
85
+ let end = trimmed.length;
86
+ while (start < end && trimmed[start] === "/") start += 1;
87
+ while (end > start && trimmed[end - 1] === "/") end -= 1;
88
+ return start === end ? undefined : `/${trimmed.slice(start, end)}`;
86
89
  };
87
90
 
88
91
  /**
package/client/device.ts CHANGED
@@ -11,7 +11,7 @@ type DebugPayload = Record<string, unknown>;
11
11
 
12
12
  const globalWithProcess = globalThis as typeof globalThis & { process?: ProcessEnvLike };
13
13
 
14
- const debugSessionId = Math.random().toString(36).slice(2, 8);
14
+ const debugSessionId = crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
15
15
  let debugSeq = 0;
16
16
 
17
17
  const FRAME_TRACE_LIMIT = 2000;
@@ -1,4 +1,4 @@
1
- import { isRecord } from "akanjs/common";
1
+ import { isRecord, trimSlashes } from "akanjs/common";
2
2
  import type { PageConfig, PageSafeAreaConfig, PageState, ResponsiveInset, SsrRenderMode } from "./csrTypes";
3
3
  import { transitionTypes } from "./transitionTypes";
4
4
 
@@ -246,7 +246,7 @@ const getPlatformFrameProfile = (platform: DevicePlatform): PageConfig => {
246
246
  };
247
247
 
248
248
  const getRouteRoleFrameDefaults = (path: string, platform: DevicePlatform, basePath?: string): PageConfig => {
249
- const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "");
249
+ const normalizedBasePath = trimSlashes(basePath ?? "");
250
250
  const depth = path
251
251
  .split("/")
252
252
  .filter(Boolean)
package/client/router.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getEnv } from "akanjs/base";
2
- import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths } from "akanjs/common";
2
+ import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths, trimSlashes } from "akanjs/common";
3
3
  import { csrLocation } from "./csrLocation";
4
4
  import { isMcpView } from "./mcpView";
5
5
 
@@ -99,7 +99,7 @@ const normalizeRoutePath = (path: string | undefined) => {
99
99
  if (!normalized) return undefined;
100
100
  const [pathWithoutHash, hash = ""] = normalized.split("#");
101
101
  const [pathname, search = ""] = pathWithoutHash.split("?");
102
- const routePath = `/${pathname.replace(/^\/+|\/+$/g, "")}`;
102
+ const routePath = `/${trimSlashes(pathname)}`;
103
103
  return `${routePath === "/" ? "/" : routePath}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
104
104
  };
105
105
 
@@ -453,7 +453,7 @@ class Router {
453
453
  const h = requestHeaders();
454
454
  const req = getRequest();
455
455
  const reqPathname = req ? new URL(req.url).pathname : "";
456
- const langFromPath = reqPathname.split("/").filter(Boolean)[0];
456
+ const langFromPath = reqPathname.split("/").find(Boolean);
457
457
  const lang = (h.get("x-locale") ?? langFromPath ?? this.#lang) as string;
458
458
  const basePath = getServerBasePath(reqPathname, lang, h.get("x-base-path") ?? undefined, this.#prefix);
459
459
  const { pathname, href: fullHref } = getPathInfo(href, lang, shouldExposeBasePath() ? basePath : "");
@@ -3,7 +3,10 @@ declare global {
3
3
  var __AKAN_RSC_IS_FROM_CACHE__: (() => boolean) | undefined;
4
4
  var __AKAN_RSC_REFRESH__: ((options?: { buildId?: number }) => Promise<void>) | undefined;
5
5
  var __AKAN_RSC_NAVIGATE__:
6
- | ((href: string, options?: { replace?: boolean; scrollToTop?: boolean; shallow?: boolean }) => Promise<void>)
6
+ | ((
7
+ href: string,
8
+ options?: { replace?: boolean; scrollToTop?: boolean; scrollY?: number; shallow?: boolean },
9
+ ) => Promise<void>)
7
10
  | undefined;
8
11
  var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
9
12
  var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
package/client/types.ts CHANGED
@@ -102,8 +102,9 @@ const slugFontPart = (value: string) =>
102
102
  value
103
103
  .trim()
104
104
  .toLowerCase()
105
- .replace(/[^a-z0-9]+/g, "-")
106
- .replace(/^-+|-+$/g, "") || "font";
105
+ .split(/[^a-z0-9]+/)
106
+ .filter(Boolean)
107
+ .join("-") || "font";
107
108
 
108
109
  export interface ModelsProps<M extends { id: string }> {
109
110
  className?: string;
@@ -1,8 +1,10 @@
1
+ import { trimSlashes } from "akanjs/common";
2
+
1
3
  /** Where an SSR document links its root layout's `.manifest()`; the server answers it from that layout. */
2
4
  export const webManifestHref = (basePath?: string | null) =>
3
5
  basePath ? `/${basePath}/manifest.webmanifest` : "/manifest.webmanifest";
4
6
 
5
- const trimSlashes = (value: string) => value.trim().replace(/^\/+|\/+$/g, "");
7
+ const trimBasePath = (value: string) => trimSlashes(value.trim());
6
8
 
7
9
  /** The basePath a `/manifest.webmanifest` request asks for: by a visible prefix, else by the request's basePath. */
8
10
  export const getWebManifestBasePath = (
@@ -12,9 +14,9 @@ export const getWebManifestBasePath = (
12
14
  ): string | null | undefined => {
13
15
  const segments = pathname.trim().split("/").filter(Boolean);
14
16
  if (segments.at(-1) !== "manifest.webmanifest" || segments.length > 2) return undefined;
15
- const known = basePaths.map(trimSlashes);
17
+ const known = basePaths.map(trimBasePath);
16
18
  if (segments.length === 2) return known.includes(segments[0] ?? "") ? (segments[0] ?? null) : undefined;
17
- const fromHeader = trimSlashes(headerBasePath ?? "");
19
+ const fromHeader = trimBasePath(headerBasePath ?? "");
18
20
  return known.includes(fromHeader) ? fromHeader : null;
19
21
  };
20
22
 
@@ -3,8 +3,12 @@ import { clientAddressFromHeaders, normalizeIpAddress } from "./clientAddress";
3
3
  export class TrustedProxy {
4
4
  static #cidrs: { bytes: Uint8Array; bits: number }[] | null = null;
5
5
  static #trustAll: boolean | null = null;
6
+
7
+ static readonly #verdicts = new Map<string, boolean>();
8
+ static readonly #verdictLimit = 1024;
6
9
 
7
10
  static configure(list: string | null | undefined) {
11
+ TrustedProxy.#verdicts.clear();
8
12
  TrustedProxy.#trustAll = list?.trim() === "*";
9
13
  TrustedProxy.#cidrs = (list ?? "")
10
14
  .split(",")
@@ -19,6 +23,7 @@ export class TrustedProxy {
19
23
  static reset() {
20
24
  TrustedProxy.#cidrs = null;
21
25
  TrustedProxy.#trustAll = null;
26
+ TrustedProxy.#verdicts.clear();
22
27
  }
23
28
 
24
29
  static #load() {
@@ -29,6 +34,15 @@ export class TrustedProxy {
29
34
  if (!address) return false;
30
35
  TrustedProxy.#load();
31
36
  if (TrustedProxy.#trustAll) return true;
37
+ const known = TrustedProxy.#verdicts.get(address);
38
+ if (known !== undefined) return known;
39
+ const verdict = TrustedProxy.#judge(address);
40
+ if (TrustedProxy.#verdicts.size >= TrustedProxy.#verdictLimit) TrustedProxy.#verdicts.clear();
41
+ TrustedProxy.#verdicts.set(address, verdict);
42
+ return verdict;
43
+ }
44
+
45
+ static #judge(address: string): boolean {
32
46
  const bytes = TrustedProxy.#toBytes(normalizeIpAddress(address));
33
47
  if (!bytes) return false;
34
48
  if (TrustedProxy.#isLocalRange(bytes)) return true;
package/common/index.ts CHANGED
@@ -199,6 +199,7 @@ export { sleep } from "./sleep";
199
199
  export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
200
200
  export { TrustedProxy } from "./TrustedProxy";
201
201
  export { toError } from "./toError";
202
+ export { trimSlashes, trimTrailingSlashes } from "./trimSlashes";
202
203
  export {
203
204
  type TunnelAgentIdentity,
204
205
  type TunnelAttachedFrame,
@@ -1,7 +1,21 @@
1
1
  /** A `{name}` placeholder whose value is absent is left as written, never replaced by "undefined". */
2
2
  export const interpolateTranslation = (message: string, data: Record<string, unknown> | undefined) => {
3
3
  if (!data) return message;
4
- return message.replace(/{([^}]+)}/g, (placeholder, name: string) =>
5
- data[name] === undefined ? placeholder : String(data[name]),
6
- );
4
+
5
+ let result = "";
6
+ let copied = 0;
7
+ let open = message.indexOf("{");
8
+ while (open !== -1) {
9
+ const close = message.indexOf("}", open + 1);
10
+ if (close === -1) break;
11
+ if (close === open + 1) {
12
+ open = message.indexOf("{", close);
13
+ continue;
14
+ }
15
+ const value = data[message.slice(open + 1, close)];
16
+ result += message.slice(copied, open) + (value === undefined ? message.slice(open, close + 1) : String(value));
17
+ copied = close + 1;
18
+ open = message.indexOf("{", copied);
19
+ }
20
+ return result + message.slice(copied);
7
21
  };
package/common/isEmail.ts CHANGED
@@ -1 +1 @@
1
- export const isEmail = (email?: string | null) => !!email && /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,8})+$/.test(email);
1
+ export const isEmail = (email?: string | null) => !!email && /^\w+(?:[.-]\w+)*@\w+(?:[.-]\w+)*\.\w{2,8}$/.test(email);
@@ -28,14 +28,20 @@ export function resolveAkanI18nConfig(input: AkanI18nConfigInput = {}): AkanI18n
28
28
  return { defaultLocale, locales };
29
29
  }
30
30
 
31
+ let lastParsed: { defaultLocale?: string; locales?: string; config: AkanI18nConfig } | null = null;
32
+
31
33
  export function parseAkanI18nEnv(
32
34
  env: Record<string, string | undefined> = {
33
35
  AKAN_PUBLIC_DEFAULT_LOCALE: typeof process !== "undefined" ? process.env.AKAN_PUBLIC_DEFAULT_LOCALE : undefined,
34
36
  AKAN_PUBLIC_LOCALES: typeof process !== "undefined" ? process.env.AKAN_PUBLIC_LOCALES : undefined,
35
37
  },
36
38
  ): AkanI18nConfig {
37
- return resolveAkanI18nConfig({
38
- defaultLocale: env.AKAN_PUBLIC_DEFAULT_LOCALE,
39
- locales: env.AKAN_PUBLIC_LOCALES?.split(","),
40
- });
39
+ const defaultLocale = env.AKAN_PUBLIC_DEFAULT_LOCALE;
40
+ const locales = env.AKAN_PUBLIC_LOCALES;
41
+ if (lastParsed && lastParsed.defaultLocale === defaultLocale && lastParsed.locales === locales)
42
+ return lastParsed.config;
43
+ const resolved = resolveAkanI18nConfig({ defaultLocale, locales: locales?.split(",") });
44
+ const config = Object.freeze({ ...resolved, locales: Object.freeze(resolved.locales) as unknown as string[] });
45
+ lastParsed = { defaultLocale, locales, config };
46
+ return config;
41
47
  }
@@ -9,7 +9,7 @@ export type MarkdownSpan =
9
9
  | { kind: "plain"; text: string };
10
10
 
11
11
  const inline =
12
- /(!?)\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)\)|`([^`]+)`|\*\*([\s\S]+?)\*\*|\*([^*\n]+?)\*|~~([\s\S]+?)~~/g;
12
+ /(!?)\[([^[\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)\)|`([^`]+)`|\*\*([\s\S]+?)\*\*|\*([^*\n]+?)\*|~~([\s\S]+?)~~/g;
13
13
 
14
14
  export class MarkdownSpans {
15
15
  static of(text: string): MarkdownSpan[] {
@@ -1,3 +1,5 @@
1
+ import { trimTrailingSlashes } from "./trimSlashes";
2
+
1
3
  export interface RestClientOptions {
2
4
  baseUrl?: string;
3
5
  headers?: HeadersInit;
@@ -16,7 +18,7 @@ export class RestClient {
16
18
 
17
19
  constructor(options: string | RestClientOptions = {}) {
18
20
  const normalized = typeof options === "string" ? { baseUrl: options } : options;
19
- this.baseUrl = normalized.baseUrl?.replace(/\/+$/, "") ?? "";
21
+ this.baseUrl = trimTrailingSlashes(normalized.baseUrl ?? "");
20
22
  this.headers = new Headers(normalized.headers);
21
23
  this.timeout = normalized.timeout;
22
24
  }
@@ -1,3 +1,5 @@
1
+ import { trimSlashes, trimTrailingSlashes } from "./trimSlashes";
2
+
1
3
  const ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
2
4
  const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
3
5
  const RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
@@ -97,9 +99,7 @@ export function validateSubRoutePageKey(
97
99
  basePaths: Iterable<string>,
98
100
  options: ValidateSubRoutePageKeyOptions = {},
99
101
  ): void {
100
- const allowedBasePaths = [
101
- ...new Set([...basePaths].map((basePath) => basePath.trim().replace(/^\/+|\/+$/g, "")).filter(Boolean)),
102
- ];
102
+ const allowedBasePaths = [...new Set([...basePaths].map((basePath) => trimSlashes(basePath.trim())).filter(Boolean))];
103
103
  if (allowedBasePaths.length === 0) return;
104
104
 
105
105
  const match = ROUTE_SOURCE_RE.exec(key);
@@ -165,7 +165,7 @@ export function tryParseRouteModuleKey(key: string): ParsedRouteModuleKey | null
165
165
 
166
166
  export function normalizeRoutePattern(segments: string[]): string {
167
167
  const parts = segments.map(routeSegmentToPatternPart).filter((part) => part.length > 0);
168
- return `/${parts.join("/")}`.replace(/\/+$/, "") || "/";
168
+ return trimTrailingSlashes(`/${parts.join("/")}`) || "/";
169
169
  }
170
170
 
171
171
  export function routeSegmentToPatternPart(segment: string): string {
@@ -0,0 +1,11 @@
1
+ export const trimTrailingSlashes = (value: string) => {
2
+ let end = value.length;
3
+ while (end > 0 && value[end - 1] === "/") end -= 1;
4
+ return value.slice(0, end);
5
+ };
6
+
7
+ export const trimSlashes = (value: string) => {
8
+ let start = 0;
9
+ while (start < value.length && value[start] === "/") start += 1;
10
+ return trimTrailingSlashes(value.slice(start));
11
+ };
package/common/types.ts CHANGED
@@ -6,7 +6,7 @@ export interface RetryPolicy extends BackoffPolicy {
6
6
  attempts?: number;
7
7
  }
8
8
 
9
- export interface FetchPolicy<Returns = unknown> {
9
+ export interface FetchPolicy<_Returns = unknown> {
10
10
  cache?: boolean | number;
11
11
  crystalize?: boolean;
12
12
  origin?: string;
@@ -276,7 +276,7 @@ export type FieldInfoObjectToFieldObject<Obj extends FieldInfoObject> = {
276
276
 
277
277
  export class ConstantField<
278
278
  FieldType extends ConstantFieldKind = ConstantFieldKind,
279
- Value extends ConstantFieldTypeInput | null = any,
279
+ _Value extends ConstantFieldTypeInput | null = any,
280
280
  FieldValue = any,
281
281
  MapValue = any,
282
282
  Nullable extends boolean = boolean,
@@ -472,7 +472,7 @@ type FieldOption<
472
472
  >
473
473
  >[];
474
474
 
475
- export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ...infer Rest]
475
+ export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ...unknown[]]
476
476
  ? PlainTypeToFieldType<First>[]
477
477
  : PlainType extends number
478
478
  ? typeof Int | typeof Float
@@ -0,0 +1,80 @@
1
+ import { FIELD_META, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
2
+
3
+ import type { ConstantField, FieldObject } from "./fieldInfo";
4
+
5
+ /**
6
+ * What a response does with a field: leaves it out (`hidden` / `secret`), computes it (`resolve`), copies it (a
7
+ * primitive or a Map), walks into it (an embedded scalar), or loads what it points at (a relation).
8
+ */
9
+ export type FieldPlanKind = "masked" | "computed" | "value" | "scalar" | "relation";
10
+
11
+ export interface FieldPlanEntry {
12
+ key: string;
13
+ field: ConstantField;
14
+ kind: FieldPlanKind;
15
+ /** Points at another stored model, so an input carries its id; independent of `kind`, as `serialize` reads it. */
16
+ isRelationRef: boolean;
17
+ /** The scalar class of a primitive field, which `serialize` calls straight; `null` for everything else. */
18
+ primitive: typeof PrimitiveScalar | null;
19
+ serializeOption: { nullable: boolean; key: string };
20
+ }
21
+ interface PlanOwner {
22
+ [FIELD_META]: FieldObject;
23
+ }
24
+
25
+ export class FieldPlan {
26
+ static #byClass = new WeakMap<object, FieldPlan>();
27
+ static #generation = 0;
28
+ static #computingAsync = new Set<FieldPlan>();
29
+ static of(cls: PlanOwner): FieldPlan {
30
+ const cached = FieldPlan.#byClass.get(cls);
31
+ if (cached) return cached;
32
+ const plan = new FieldPlan(cls[FIELD_META]);
33
+ FieldPlan.#byClass.set(cls, plan);
34
+ return plan;
35
+ }
36
+ static reset(cls: object) {
37
+ FieldPlan.#byClass.delete(cls);
38
+ FieldPlan.#generation += 1;
39
+ }
40
+
41
+ readonly entries: readonly FieldPlanEntry[];
42
+ #resolvesAsync: { value: boolean; generation: number } | null = null;
43
+ constructor(fieldObj: FieldObject) {
44
+ this.entries = Object.entries(fieldObj).map(([key, field]) => ({
45
+ key,
46
+ field,
47
+ kind: FieldPlan.#kindOf(field),
48
+ isRelationRef: field.isClass && !field.isScalar,
49
+ primitive: PrimitiveRegistry.has(field.modelRef) ? (field.modelRef as unknown as typeof PrimitiveScalar) : null,
50
+ serializeOption: { nullable: field.nullable, key },
51
+ }));
52
+ }
53
+
54
+ static #kindOf(field: ConstantField): FieldPlanKind {
55
+ if (field.fieldType === "hidden" || field.fieldType === "secret") return "masked";
56
+ if (field.fieldType === "resolve") return "computed";
57
+ if (!field.isClass) return "value";
58
+ return field.isScalar ? "scalar" : "relation";
59
+ }
60
+
61
+ /** Whether resolving a value of this class awaits anything: a relation, a resolve field, or one in a scalar below. */
62
+ get resolvesAsync(): boolean {
63
+ if (this.#resolvesAsync?.generation === FieldPlan.#generation) return this.#resolvesAsync.value;
64
+
65
+ if (FieldPlan.#computingAsync.has(this)) return true;
66
+ FieldPlan.#computingAsync.add(this);
67
+ try {
68
+ const value = this.entries.some(
69
+ ({ kind, field }) =>
70
+ kind === "computed" ||
71
+ kind === "relation" ||
72
+ (kind === "scalar" && FieldPlan.of(field.modelRef as unknown as PlanOwner).resolvesAsync),
73
+ );
74
+ this.#resolvesAsync = { value, generation: FieldPlan.#generation };
75
+ return value;
76
+ } finally {
77
+ FieldPlan.#computingAsync.delete(this);
78
+ }
79
+ }
80
+ }
package/constant/index.ts CHANGED
@@ -3,10 +3,13 @@ export * from "./constantRegistry";
3
3
  export * from "./crystalize";
4
4
  export * from "./dateSlot";
5
5
  export * from "./fieldInfo";
6
+ export * from "./fieldPlan";
6
7
  export * from "./fieldQueryMeta";
7
8
  export * from "./getDefault";
8
9
  export * from "./hydrationPlan";
9
10
  export * from "./immerify";
11
+ export * from "./inputParser";
12
+ export * from "./invalidInputError";
10
13
  export * from "./labelOf";
11
14
  export * from "./mask";
12
15
  export * from "./purify";
@@ -0,0 +1,148 @@
1
+ import {
2
+ Any,
3
+ type Cls,
4
+ dayjs,
5
+ type EnumInstance,
6
+ FIELD_META,
7
+ Float,
8
+ getNonArrayModel,
9
+ ID,
10
+ Int,
11
+ PrimitiveRegistry,
12
+ type PrimitiveScalar,
13
+ } from "akanjs/base";
14
+
15
+ import { ConstantRegistry } from "./constantRegistry";
16
+ import type { ConstantField, FieldObject } from "./fieldInfo";
17
+ import { FieldPlan, type FieldPlanEntry } from "./fieldPlan";
18
+ import { defaultPlanOf } from "./getDefault";
19
+ import { InvalidInputError } from "./invalidInputError";
20
+ import { deserialize } from "./serialize";
21
+
22
+ type ModelCls = Cls & { [FIELD_META]: FieldObject };
23
+ type Parse = (value: unknown, path: string) => unknown;
24
+
25
+ interface ParseOption {
26
+ key: string;
27
+ nullable?: boolean;
28
+ enum?: EnumInstance;
29
+ }
30
+
31
+ const isString = (value: unknown) => typeof value === "string";
32
+ const isNumber = (value: unknown) => typeof value === "number";
33
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
34
+ typeof value === "object" && value !== null && !Array.isArray(value);
35
+
36
+ export class InputParser {
37
+
38
+ static readonly #wireTypes = new Map<unknown, (value: unknown) => boolean>([
39
+ [String, isString],
40
+ [ID, isString],
41
+ [Int, isNumber],
42
+ [Float, isNumber],
43
+ [Boolean, (value) => typeof value === "boolean"],
44
+ [Date, (value) => isString(value) || isNumber(value) || value instanceof Date || dayjs.isDayjs(value)],
45
+ ]);
46
+
47
+ static parse(argRef: Cls, arrDepth: number, value: unknown, option: ParseOption) {
48
+ if (PrimitiveRegistry.has(argRef)) return deserialize(argRef, arrDepth, value, option);
49
+ const { key, nullable = false } = option;
50
+ if (value === null || value === undefined) {
51
+ if (nullable) return null;
52
+ throw new InvalidInputError(key, null);
53
+ }
54
+ return InputParser.#walk(value, arrDepth, key, (item, path) => InputParser.#value(argRef, item, path));
55
+ }
56
+
57
+ static #walk(value: unknown, arrDepth: number, path: string, parse: Parse): unknown {
58
+ if (!arrDepth) return parse(value, path);
59
+ if (!Array.isArray(value)) throw new InvalidInputError(path, "array");
60
+ return value.map((item, idx) => InputParser.#walk(item, arrDepth - 1, `${path}.${idx}`, parse));
61
+ }
62
+
63
+ static #value(ref: Cls, value: unknown, path: string, enumRef?: EnumInstance): unknown {
64
+ if (value === null || value === undefined) {
65
+ if (ref === Any) return value ?? null;
66
+ throw new InvalidInputError(path, null);
67
+ }
68
+ if (PrimitiveRegistry.has(ref)) return InputParser.#primitive(ref as typeof PrimitiveScalar, value, path, enumRef);
69
+ if (ConstantRegistry.isScalar(ref)) return InputParser.#scalar(ref as ModelCls, value, path);
70
+ return InputParser.#model(ref as ModelCls, value, path);
71
+ }
72
+
73
+ static #primitive(primitive: typeof PrimitiveScalar, value: unknown, path: string, enumRef?: EnumInstance) {
74
+ const hasWireType = InputParser.#wireTypes.get(primitive);
75
+ if (hasWireType && !hasWireType(value)) throw new InvalidInputError(path, primitive.refName);
76
+ let parsed: unknown;
77
+ try {
78
+ parsed = primitive._parse(value as never);
79
+ } catch {
80
+ throw new InvalidInputError(path, primitive.refName);
81
+ }
82
+ if (enumRef && !enumRef.has(parsed as never))
83
+ throw new InvalidInputError(path, `one of ${enumRef.values.join(", ")}`);
84
+ return parsed;
85
+ }
86
+
87
+ static #scalar(ref: ModelCls, value: unknown, path: string) {
88
+ if (!isRecord(value)) throw new InvalidInputError(path, "object");
89
+ const defaults = defaultPlanOf(ref[FIELD_META]);
90
+ const parsed: Record<string, unknown> = {};
91
+ for (const entry of FieldPlan.of(ref).entries) {
92
+ if (entry.kind === "computed") continue;
93
+ const fieldPath = `${path}.${entry.key}`;
94
+ const raw = value[entry.key];
95
+ if (raw !== undefined) parsed[entry.key] = InputParser.#field(entry, raw, fieldPath);
96
+ else if (InputParser.#isRequired(entry.field)) throw new InvalidInputError(fieldPath, null);
97
+ else {
98
+ const make = defaults.perCall.get(entry.key);
99
+ parsed[entry.key] = make ? make() : defaults.shared[entry.key];
100
+ }
101
+ }
102
+ return parsed;
103
+ }
104
+
105
+ static #model(ref: ModelCls, value: unknown, path: string) {
106
+ if (!isRecord(value)) throw new InvalidInputError(path, "object");
107
+ const parsed: Record<string, unknown> = {};
108
+ for (const entry of FieldPlan.of(ref).entries) {
109
+ if (entry.kind === "computed") continue;
110
+ const fieldPath = `${path}.${entry.key}`;
111
+ const raw = value[entry.key];
112
+ if (raw !== undefined) parsed[entry.key] = InputParser.#field(entry, raw, fieldPath);
113
+ else if (InputParser.#isRequired(entry.field)) throw new InvalidInputError(fieldPath, null);
114
+ }
115
+ return parsed;
116
+ }
117
+
118
+ static #field({ field, isRelationRef }: FieldPlanEntry, value: unknown, path: string): unknown {
119
+ if (value === null) {
120
+ if (field.nullable || (field.modelRef as Cls) === Any) return null;
121
+ throw new InvalidInputError(path, null);
122
+ }
123
+ return InputParser.#walk(value, field.arrDepth, path, (item, itemPath) => {
124
+ if (field.isMap) return InputParser.#map(field.of as Cls | Cls[], item, itemPath);
125
+ if (isRelationRef) return InputParser.#value(ID, item, itemPath);
126
+ return InputParser.#value(field.modelRef, item, itemPath, field.enum);
127
+ });
128
+ }
129
+
130
+ static #map(of: Cls | Cls[], value: unknown, path: string) {
131
+ if (!isRecord(value)) throw new InvalidInputError(path, "object");
132
+ const [valueRef, valueArrDepth] = getNonArrayModel(of);
133
+ return Object.fromEntries(
134
+ Object.entries(value).map(([key, item]) => [
135
+ key,
136
+ InputParser.#walk(item, valueArrDepth, `${path}.${key}`, (entry, entryPath) =>
137
+ InputParser.#value(valueRef as Cls, entry, entryPath),
138
+ ),
139
+ ]),
140
+ );
141
+ }
142
+
143
+ static #isRequired(field: ConstantField) {
144
+ if (field.nullable || (field.modelRef as Cls) === Any) return false;
145
+ if (field.default !== null && field.default !== undefined) return false;
146
+ return !(field.isClass && field.isScalar);
147
+ }
148
+ }
@@ -0,0 +1,13 @@
1
+ /** A body value the request schema refuses: `path` names it from the argument down (`data.items.2.price`). */
2
+ export class InvalidInputError extends Error {
3
+ readonly path: string;
4
+ readonly reason: "missing" | "invalid";
5
+ /** What the field takes — a type name, `object`, `array`, or the enum's values; `null` when it is missing. */
6
+ readonly expected: string | null;
7
+ constructor(path: string, expected: string | null) {
8
+ super(expected ? `Invalid input at ${path}: expected ${expected}` : `Missing input at ${path}`);
9
+ this.path = path;
10
+ this.reason = expected ? "invalid" : "missing";
11
+ this.expected = expected;
12
+ }
13
+ }