akanjs 3.0.0-alpha.70 → 3.0.0-alpha.72

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 (247) hide show
  1. package/client/cookie.ts +21 -34
  2. package/client/csrTypes.ts +0 -12
  3. package/client/frameConfig.ts +13 -0
  4. package/client/router.ts +7 -7
  5. package/common/Logger.ts +287 -67
  6. package/common/TrustedProxy.ts +134 -0
  7. package/common/clamp.ts +2 -0
  8. package/common/deepObjectify.ts +3 -2
  9. package/common/dynamicRecord.ts +12 -0
  10. package/common/index.ts +29 -2
  11. package/common/isValidDate.ts +1 -1
  12. package/common/logContext.ts +27 -0
  13. package/common/plainFieldsOf.ts +13 -0
  14. package/common/requestOrigin.ts +32 -0
  15. package/common/{httpClient.ts → restClient.ts} +16 -9
  16. package/constant/crystalize.ts +59 -39
  17. package/constant/dateSlot.ts +62 -0
  18. package/constant/getDefault.ts +16 -11
  19. package/constant/hydrationPlan.ts +72 -0
  20. package/constant/immerify.ts +4 -6
  21. package/constant/index.ts +2 -0
  22. package/constant/types.ts +22 -0
  23. package/constant/via.ts +14 -14
  24. package/dictionary/base.dictionary.ts +4 -0
  25. package/dictionary/trans.ts +24 -5
  26. package/document/dataLoader.ts +0 -4
  27. package/document/filterMeta.ts +2 -2
  28. package/document/loaderInfo.ts +4 -1
  29. package/fetch/client/fetchClient.ts +67 -72
  30. package/fetch/client/fetchHandle.ts +57 -0
  31. package/fetch/client/httpClient.ts +42 -14
  32. package/fetch/client/sliceInitHandle.ts +101 -0
  33. package/fetch/fetchType/appliedReturn.type.ts +64 -1
  34. package/fetch/fetchType/sliceFetch.type.ts +5 -5
  35. package/fetch/requestStorage.ts +19 -5
  36. package/index.ts +16 -2
  37. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  38. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  39. package/package.json +11 -1
  40. package/server/akanApp.ts +84 -93
  41. package/server/akanAppHeaders.ts +10 -4
  42. package/server/akanOption.ts +14 -1
  43. package/server/akanServer.ts +87 -10
  44. package/server/cachePolicy.ts +2 -11
  45. package/server/console.ts +102 -1
  46. package/server/consoleSession.ts +49 -5
  47. package/server/decorators.ts +12 -3
  48. package/server/devtools/constantSerializer.ts +8 -1
  49. package/server/di/diLifecycle.ts +21 -1
  50. package/server/di/resolveHierarchy.ts +3 -3
  51. package/server/logging/childOutputReader.ts +137 -0
  52. package/server/logging/hubFileSink.ts +39 -0
  53. package/server/logging/logControlSocket.ts +173 -0
  54. package/server/logging/logForwarder.ts +131 -0
  55. package/server/logging/logHub.ts +0 -0
  56. package/server/logging/logQuery.ts +130 -0
  57. package/server/logging/logStdoutWriter.ts +39 -0
  58. package/server/logging/logStreamRoute.ts +87 -0
  59. package/server/logging/logTailClient.ts +161 -0
  60. package/server/mcp/McpAuth.ts +4 -15
  61. package/server/mcp/McpDispatcher.ts +20 -10
  62. package/server/mcp/McpEventStream.ts +8 -60
  63. package/server/resolver/database.resolver.ts +13 -8
  64. package/server/resolver/signal.resolver.ts +83 -56
  65. package/server/routeTreeBuilder.ts +1 -15
  66. package/server/routing/apiRouter.ts +6 -5
  67. package/server/routing/appWsData.ts +9 -6
  68. package/server/routing/eventStream.ts +93 -0
  69. package/server/routing/soloAppRoutes.ts +5 -1
  70. package/server/rscWorker.tsx +27 -9
  71. package/server/rscWorkerHost.ts +59 -8
  72. package/server/staticPath.ts +39 -0
  73. package/server/webRouter.ts +44 -41
  74. package/service/adapt.ts +1 -5
  75. package/service/ipcTypes.ts +5 -0
  76. package/service/predefinedAdaptor/cache.adaptor.ts +8 -4
  77. package/service/predefinedAdaptor/database.adaptor.ts +37 -1766
  78. package/service/predefinedAdaptor/logging.adaptor.ts +1 -1
  79. package/service/predefinedAdaptor/sql/PendingStoreEnsures.ts +22 -0
  80. package/service/predefinedAdaptor/sql/QueryCompiler.ts +225 -0
  81. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +812 -0
  82. package/service/predefinedAdaptor/sql/UpdateCompiler.ts +67 -0
  83. package/service/predefinedAdaptor/sql/dialect/postgres.ts +151 -0
  84. package/service/predefinedAdaptor/sql/dialect/sqlite.ts +134 -0
  85. package/service/predefinedAdaptor/sql/driver/bunSqlite.ts +29 -0
  86. package/service/predefinedAdaptor/sql/driver/libsql.ts +37 -0
  87. package/service/predefinedAdaptor/sql/driver/postgres.ts +42 -0
  88. package/service/predefinedAdaptor/sql/types.ts +251 -0
  89. package/service/predefinedAdaptor/sql/values.ts +91 -0
  90. package/signal/CrossSiteGuard.ts +71 -0
  91. package/signal/SignalFailure.ts +51 -0
  92. package/signal/endpoint.ts +2 -2
  93. package/signal/exception.ts +43 -6
  94. package/signal/index.ts +2 -0
  95. package/signal/intercept.ts +5 -2
  96. package/signal/internal.ts +10 -2
  97. package/signal/serializer/fetch.serializer.ts +11 -1
  98. package/signal/signalContext.ts +73 -43
  99. package/signal/slice.ts +1 -1
  100. package/signal/trace.ts +268 -9
  101. package/store/SliceRequest.ts +23 -0
  102. package/store/action.ts +182 -120
  103. package/store/agent/AgentBridge.ts +3 -1
  104. package/store/agentic/StExposeBuilder.ts +7 -4
  105. package/store/agentic/StStateBuilder.ts +9 -4
  106. package/store/agentic/StToolBuilder.ts +21 -7
  107. package/store/store.ts +1 -1
  108. package/store/storeInstance.ts +3 -3
  109. package/store/storeRegistry.ts +1 -1
  110. package/test/testServer.ts +6 -6
  111. package/types/client/cookie.d.ts +13 -2
  112. package/types/client/csrTypes.d.ts +0 -1
  113. package/types/client/frameConfig.d.ts +1 -0
  114. package/types/common/Logger.d.ts +88 -6
  115. package/types/common/TrustedProxy.d.ts +24 -0
  116. package/types/common/clamp.d.ts +2 -0
  117. package/types/common/dynamicRecord.d.ts +14 -0
  118. package/types/common/index.d.ts +8 -2
  119. package/types/common/logContext.d.ts +17 -0
  120. package/types/common/plainFieldsOf.d.ts +6 -0
  121. package/types/common/requestOrigin.d.ts +14 -0
  122. package/types/common/restClient.d.ts +27 -0
  123. package/types/constant/crystalize.d.ts +3 -0
  124. package/types/constant/dateSlot.d.ts +14 -0
  125. package/types/constant/getDefault.d.ts +8 -0
  126. package/types/constant/hydrationPlan.d.ts +24 -0
  127. package/types/constant/index.d.ts +2 -0
  128. package/types/constant/types.d.ts +13 -0
  129. package/types/dictionary/base.dictionary.d.ts +1 -1
  130. package/types/dictionary/dictionary.d.ts +8 -8
  131. package/types/document/dataLoader.d.ts +0 -3
  132. package/types/document/filterMeta.d.ts +2 -2
  133. package/types/document/loaderInfo.d.ts +2 -2
  134. package/types/fetch/client/fetchHandle.d.ts +24 -0
  135. package/types/fetch/client/httpClient.d.ts +2 -0
  136. package/types/fetch/client/sliceInitHandle.d.ts +29 -0
  137. package/types/fetch/fetchType/appliedReturn.type.d.ts +38 -1
  138. package/types/fetch/fetchType/sliceFetch.type.d.ts +5 -5
  139. package/types/fetch/requestStorage.d.ts +14 -0
  140. package/types/index.d.ts +17 -2
  141. package/types/server/akanOption.d.ts +8 -1
  142. package/types/server/console.d.ts +10 -0
  143. package/types/server/consoleSession.d.ts +10 -0
  144. package/types/server/decorators.d.ts +12 -2
  145. package/types/server/di/diLifecycle.d.ts +9 -1
  146. package/types/server/logging/childOutputReader.d.ts +40 -0
  147. package/types/server/logging/hubFileSink.d.ts +18 -0
  148. package/types/server/logging/logControlSocket.d.ts +60 -0
  149. package/types/server/logging/logForwarder.d.ts +37 -0
  150. package/types/server/logging/logHub.d.ts +70 -0
  151. package/types/server/logging/logQuery.d.ts +31 -0
  152. package/types/server/logging/logStdoutWriter.d.ts +35 -0
  153. package/types/server/logging/logStreamRoute.d.ts +20 -0
  154. package/types/server/logging/logTailClient.d.ts +42 -0
  155. package/types/server/mcp/McpAuth.d.ts +2 -6
  156. package/types/server/mcp/McpEventStream.d.ts +4 -5
  157. package/types/server/routeTreeBuilder.d.ts +1 -2
  158. package/types/server/routing/appWsData.d.ts +7 -3
  159. package/types/server/routing/eventStream.d.ts +23 -0
  160. package/types/server/routing/soloAppRoutes.d.ts +3 -1
  161. package/types/server/rscWorkerHost.d.ts +4 -1
  162. package/types/server/staticPath.d.ts +13 -0
  163. package/types/server/webRouter.d.ts +3 -1
  164. package/types/service/adapt.d.ts +1 -1
  165. package/types/service/ipcTypes.d.ts +11 -0
  166. package/types/service/predefinedAdaptor/database.adaptor.d.ts +15 -407
  167. package/types/service/predefinedAdaptor/sql/PendingStoreEnsures.d.ts +6 -0
  168. package/types/service/predefinedAdaptor/sql/QueryCompiler.d.ts +28 -0
  169. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +127 -0
  170. package/types/service/predefinedAdaptor/sql/UpdateCompiler.d.ts +17 -0
  171. package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +27 -0
  172. package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +27 -0
  173. package/types/service/predefinedAdaptor/sql/driver/bunSqlite.d.ts +16 -0
  174. package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +17 -0
  175. package/types/service/predefinedAdaptor/sql/driver/postgres.d.ts +17 -0
  176. package/types/service/predefinedAdaptor/sql/types.d.ts +226 -0
  177. package/types/service/predefinedAdaptor/sql/values.d.ts +13 -0
  178. package/types/signal/CrossSiteGuard.d.ts +44 -0
  179. package/types/signal/SignalFailure.d.ts +28 -0
  180. package/types/signal/exception.d.ts +38 -54774
  181. package/types/signal/index.d.ts +2 -0
  182. package/types/signal/intercept.d.ts +3 -3
  183. package/types/signal/internal.d.ts +5 -1
  184. package/types/signal/serializer/fetch.serializer.d.ts +7 -1
  185. package/types/signal/signalContext.d.ts +16 -3
  186. package/types/signal/trace.d.ts +52 -2
  187. package/types/store/SliceRequest.d.ts +16 -0
  188. package/types/store/agentic/StToolBuilder.d.ts +10 -1
  189. package/types/ui/Agent/Menu.d.ts +1 -1
  190. package/types/ui/Field/Choice.d.ts +52 -0
  191. package/types/ui/Field/Collection.d.ts +51 -0
  192. package/types/ui/Field/DateField.d.ts +32 -0
  193. package/types/ui/Field/Label.d.ts +9 -0
  194. package/types/ui/Field/NumberField.d.ts +43 -0
  195. package/types/ui/Field/Relation.d.ts +80 -0
  196. package/types/ui/Field/Text.d.ts +101 -0
  197. package/types/ui/Field/index.d.ts +50 -0
  198. package/types/ui/Field.d.ts +1 -412
  199. package/types/ui/Load/Edit_Client.d.ts +3 -1
  200. package/types/ui/Load/Stream.d.ts +22 -0
  201. package/types/ui/Load/index.d.ts +2 -0
  202. package/types/ui/System/Root.d.ts +4 -0
  203. package/types/ui/index.d.ts +1 -1
  204. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  205. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  206. package/types/webkit/useCamera.d.ts +10 -1
  207. package/types/webkit/usePurchase.d.ts +4 -0
  208. package/ui/Agent/Menu.tsx +2 -2
  209. package/ui/Data/TableList.tsx +1 -1
  210. package/ui/DraggableList.tsx +1 -2
  211. package/ui/Field/Choice.tsx +172 -0
  212. package/ui/Field/Collection.tsx +293 -0
  213. package/ui/Field/DateField.tsx +138 -0
  214. package/ui/Field/Label.tsx +31 -0
  215. package/ui/Field/NumberField.tsx +169 -0
  216. package/ui/Field/Relation.tsx +387 -0
  217. package/ui/Field/Text.tsx +360 -0
  218. package/ui/Field/index.tsx +70 -0
  219. package/ui/Input.tsx +3 -3
  220. package/ui/Load/Edit.tsx +2 -2
  221. package/ui/Load/Edit_Client.tsx +11 -3
  222. package/ui/Load/Pagination.tsx +25 -25
  223. package/ui/Load/Stream.tsx +38 -0
  224. package/ui/Load/Units.tsx +50 -37
  225. package/ui/Load/View.tsx +25 -17
  226. package/ui/Load/index.ts +2 -0
  227. package/ui/Model/EditModal.tsx +8 -8
  228. package/ui/Signal/Message.tsx +7 -7
  229. package/ui/Signal/PubSub.tsx +4 -1
  230. package/ui/System/Client.tsx +1 -3
  231. package/ui/System/Gtag.tsx +1 -1
  232. package/ui/System/Root.tsx +4 -0
  233. package/ui/index.ts +1 -1
  234. package/ui/overlayPosition.ts +1 -2
  235. package/webkit/useCamera.tsx +22 -6
  236. package/webkit/useCodepush.tsx +2 -2
  237. package/webkit/useCsrValues.ts +1 -3
  238. package/webkit/useFetch.ts +4 -3
  239. package/webkit/useFrameRuntime.ts +14 -7
  240. package/webkit/usePurchase.tsx +4 -1
  241. package/fetch/serializer/fetch.serializer.ts +0 -142
  242. package/fetch/serializer/index.ts +0 -1
  243. package/types/common/httpClient.d.ts +0 -20
  244. package/types/fetch/serializer/fetch.serializer.d.ts +0 -14
  245. package/types/fetch/serializer/index.d.ts +0 -1
  246. package/types/fetch/serializer.d.ts +0 -1
  247. package/ui/Field.tsx +0 -1639
@@ -0,0 +1,134 @@
1
+ import { normalizeIpAddress } from "./clientAddress";
2
+
3
+ /**
4
+ * Whether the hop that connected is allowed to name the caller.
5
+ *
6
+ * `x-real-ip` and `x-forwarded-for` are just request headers: anyone can send them. They are the truth only when
7
+ * the peer that sent them is a proxy we put there — otherwise a client forges its own address and walks past
8
+ * every `.with(Ip)` guard, rate limit and audit line at once.
9
+ *
10
+ * The default trusts a private, loopback or link-local peer and nothing else, because that is exactly the shape
11
+ * of a proxy on the same network: an ingress, a service mesh, the dev machine. A gateway reachable from the
12
+ * internet sees a public peer, so a header from there is dropped rather than believed. `AKAN_TRUSTED_PROXIES`
13
+ * takes a comma-separated CIDR list for the case the proxy is not private — and `*` for "the deployment
14
+ * guarantees nothing untrusted can reach this port", which is the old behaviour, written down.
15
+ */
16
+ export class TrustedProxy {
17
+ static #cidrs: { bytes: Uint8Array; bits: number }[] | null = null;
18
+ static #trustAll: boolean | null = null;
19
+
20
+ static configure(list: string | null | undefined) {
21
+ TrustedProxy.#trustAll = list?.trim() === "*";
22
+ TrustedProxy.#cidrs = (list ?? "")
23
+ .split(",")
24
+ .map((entry) => entry.trim())
25
+ .filter((entry) => entry && entry !== "*")
26
+ .flatMap((entry) => {
27
+ const parsed = TrustedProxy.#parseCidr(entry);
28
+ return parsed ? [parsed] : [];
29
+ });
30
+ }
31
+
32
+ static reset() {
33
+ TrustedProxy.#cidrs = null;
34
+ TrustedProxy.#trustAll = null;
35
+ }
36
+
37
+ static #load() {
38
+ if (TrustedProxy.#cidrs === null) TrustedProxy.configure(process.env.AKAN_TRUSTED_PROXIES);
39
+ }
40
+
41
+ static isTrusted(address: string | null | undefined): boolean {
42
+ if (!address) return false;
43
+ TrustedProxy.#load();
44
+ if (TrustedProxy.#trustAll) return true;
45
+ const bytes = TrustedProxy.#toBytes(normalizeIpAddress(address));
46
+ if (!bytes) return false;
47
+ if (TrustedProxy.#isLocalRange(bytes)) return true;
48
+ return (TrustedProxy.#cidrs ?? []).some((cidr) => TrustedProxy.#inRange(bytes, cidr));
49
+ }
50
+
51
+ /**
52
+ * The caller's address as recorded by a proxy, but only when the peer is one. Falls back to the peer itself,
53
+ * which is the right answer for a process nothing is proxying, and `null` when there is no peer either.
54
+ */
55
+ static clientAddress(headers: Headers, peerAddress: string | null | undefined): string | null {
56
+ if (TrustedProxy.isTrusted(peerAddress)) {
57
+ const realIp = headers.get("x-real-ip")?.trim();
58
+ if (realIp) return normalizeIpAddress(realIp);
59
+ const forwarded = headers.get("x-forwarded-for")?.split(",")[0]?.trim();
60
+ if (forwarded) return normalizeIpAddress(forwarded);
61
+ }
62
+ return peerAddress ? normalizeIpAddress(peerAddress) : null;
63
+ }
64
+
65
+ /** `10/8`, `172.16/12`, `192.168/16`, `127/8`, `169.254/16`, and their IPv6 counterparts `::1`, `fc00::/7`, `fe80::/10`. */
66
+ static #isLocalRange(bytes: Uint8Array): boolean {
67
+ if (bytes.length === 4) {
68
+ const [a = 0, b = 0] = bytes;
69
+ if (a === 10 || a === 127) return true;
70
+ if (a === 192 && b === 168) return true;
71
+ if (a === 172 && b >= 16 && b <= 31) return true;
72
+ return a === 169 && b === 254;
73
+ }
74
+ const [first = 0, second = 0] = bytes;
75
+ if ((first & 0xfe) === 0xfc) return true;
76
+ if (first === 0xfe && (second & 0xc0) === 0x80) return true;
77
+ return bytes.every((byte, idx) => (idx === 15 ? byte === 1 : byte === 0));
78
+ }
79
+
80
+ static #parseCidr(entry: string): { bytes: Uint8Array; bits: number } | null {
81
+ const [address, prefix] = entry.split("/");
82
+ const bytes = TrustedProxy.#toBytes(address ?? "");
83
+ if (!bytes) return null;
84
+ const bits = prefix === undefined ? bytes.length * 8 : Number(prefix);
85
+ if (!Number.isInteger(bits) || bits < 0 || bits > bytes.length * 8) return null;
86
+ return { bytes, bits };
87
+ }
88
+
89
+ static #inRange(bytes: Uint8Array, cidr: { bytes: Uint8Array; bits: number }): boolean {
90
+ if (bytes.length !== cidr.bytes.length) return false;
91
+ const wholeBytes = cidr.bits >> 3;
92
+ for (let idx = 0; idx < wholeBytes; idx += 1) if (bytes[idx] !== cidr.bytes[idx]) return false;
93
+ const restBits = cidr.bits & 7;
94
+ if (!restBits) return true;
95
+ const mask = 0xff << (8 - restBits);
96
+ return ((bytes[wholeBytes] ?? 0) & mask) === ((cidr.bytes[wholeBytes] ?? 0) & mask);
97
+ }
98
+
99
+ static #toBytes(address: string): Uint8Array | null {
100
+ if (address.includes(".") && !address.includes(":")) {
101
+ const parts = address.split(".");
102
+ if (parts.length !== 4) return null;
103
+ const bytes = new Uint8Array(4);
104
+ for (const [idx, part] of parts.entries()) {
105
+ const value = Number(part);
106
+ if (!/^\d{1,3}$/.test(part) || value > 255) return null;
107
+ bytes[idx] = value;
108
+ }
109
+ return bytes;
110
+ }
111
+ return address.includes(":") ? TrustedProxy.#ipv6ToBytes(address) : null;
112
+ }
113
+
114
+ static #ipv6ToBytes(address: string): Uint8Array | null {
115
+
116
+ const bare = address.split("%")[0] ?? "";
117
+ const halves = bare.split("::");
118
+ if (halves.length > 2) return null;
119
+ const groupsOf = (text: string) => (text ? text.split(":").filter((group) => group !== "") : []);
120
+ const head = groupsOf(halves[0] ?? "");
121
+ const tail = halves.length === 2 ? groupsOf(halves[1] ?? "") : [];
122
+ const filled = halves.length === 2 ? 8 - head.length - tail.length : 0;
123
+ if (filled < 0 || head.length + tail.length + filled !== 8) return null;
124
+ const groups = [...head, ...new Array(filled).fill("0"), ...tail];
125
+ const bytes = new Uint8Array(16);
126
+ for (const [idx, group] of groups.entries()) {
127
+ if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
128
+ const value = Number.parseInt(group, 16);
129
+ bytes[idx * 2] = value >> 8;
130
+ bytes[idx * 2 + 1] = value & 0xff;
131
+ }
132
+ return bytes;
133
+ }
134
+ }
@@ -0,0 +1,2 @@
1
+ /** Constrains a value to `[min, max]`. */
2
+ export const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
@@ -31,9 +31,10 @@ export const deepObjectify = <T = unknown>(obj: T | null | undefined, option: De
31
31
  } else if (obj && typeof obj === "object") {
32
32
  const val: Record<string, unknown> = {};
33
33
  const objRecord = obj as Record<string, unknown>;
34
- Object.keys(obj).forEach((key) => {
34
+
35
+ for (const key in objRecord) {
35
36
  if (typeof objRecord[key] !== "function") val[key] = objectifyChild(objRecord[key], option);
36
- });
37
+ }
37
38
  return val as T;
38
39
  } else {
39
40
  return obj as unknown as T;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A generated surface read by a name computed at runtime.
3
+ *
4
+ * The framework's glue layers dispatch by name: a store action reaches `fetch[key]`, a resolver reaches
5
+ * `service[method]`, a `Load.*` component reads `init[names.pageOfModel]`. None of those keys exist in a type,
6
+ * because the type is generated from the model — so every one of them used to be spelled `as any`, which the
7
+ * guide bans and which also silences the *value* type on the way out.
8
+ *
9
+ * This says the same thing about the index and nothing about the value: `as DynamicRecord` to index, then the
10
+ * read carries its own `as T`. One alias so a reader can tell name-based dispatch from an untyped object.
11
+ */
12
+ export type DynamicRecord<Value = unknown> = { [key: string]: Value };
package/common/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { applyMixins } from "./applyMixins";
2
2
  export { capitalize } from "./capitalize";
3
+ export { clamp } from "./clamp";
3
4
  export {
4
5
  clientAddressFromHeaders,
5
6
  clientPortFromHeaders,
@@ -7,6 +8,7 @@ export {
7
8
  normalizeIpAddress,
8
9
  } from "./clientAddress";
9
10
  export { deepObjectify } from "./deepObjectify";
11
+ export type { DynamicRecord } from "./dynamicRecord";
10
12
  export {
11
13
  type FileUploadCapability,
12
14
  fileUploadContract,
@@ -16,7 +18,6 @@ export { formatNumber } from "./formatNumber";
16
18
  export { formatPhone } from "./formatPhone";
17
19
  export { getAllPropertyDescriptors } from "./getAllPropertyDescriptors";
18
20
  export { type AkanHmrPhase, getAkanHmrPhase, isAkanHmrApplying } from "./hmrPhase";
19
- export { HttpClient } from "./httpClient";
20
21
  export { isDayjs } from "./isDayjs";
21
22
  export { isEmail } from "./isEmail";
22
23
  export { isPhoneNumber } from "./isPhoneNumber";
@@ -24,7 +25,22 @@ export { isQueryEqual } from "./isQueryEqual";
24
25
  export { isThenable } from "./isThenable";
25
26
  export { isValidDate } from "./isValidDate";
26
27
  export { decodeJwtPayload } from "./jwtDecode";
27
- export { Logger, type LoggerSink, type LoggerSinkEntry, type LogLevel } from "./Logger";
28
+ export {
29
+ type LogAttrs,
30
+ type LogAttrValue,
31
+ type LogFormat,
32
+ Logger,
33
+ type LoggerEmitInput,
34
+ type LoggerSink,
35
+ type LoggerSinkEntry,
36
+ type LoggerSinkOptions,
37
+ type LogLevel,
38
+ type LogLevelInput,
39
+ type LogRecord,
40
+ logFormats,
41
+ logLevels,
42
+ logSeverity,
43
+ } from "./Logger";
28
44
  export {
29
45
  type AkanI18nConfig,
30
46
  type AkanI18nConfigInput,
@@ -32,6 +48,13 @@ export {
32
48
  parseAkanI18nEnv,
33
49
  resolveAkanI18nConfig,
34
50
  } from "./localeConfig";
51
+ export {
52
+ type LogContextReader,
53
+ type LogContextSnapshot,
54
+ type LogFlightRecorder,
55
+ readLogContext,
56
+ registerLogContextReader,
57
+ } from "./logContext";
35
58
  export { lowerlize } from "./lowerlize";
36
59
  export {
37
60
  isMcpDescribableArg,
@@ -47,8 +70,11 @@ export { objectify } from "./objectify";
47
70
  export { pathGet } from "./pathGet";
48
71
  export { pathGetLoose } from "./pathGetLoose";
49
72
  export { pathSet } from "./pathSet";
73
+ export { plainFieldsOf } from "./plainFieldsOf";
50
74
  export { randomPick } from "./randomPick";
51
75
  export { randomPicks } from "./randomPicks";
76
+ export { isJsonContentType, originFromRequest } from "./requestOrigin";
77
+ export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient";
52
78
  export {
53
79
  assertUniqueRoutePatterns,
54
80
  compareRouteSpecificity,
@@ -76,6 +102,7 @@ export {
76
102
  export { sleep } from "./sleep";
77
103
  export { splitVersion } from "./splitVersion";
78
104
  export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
105
+ export { TrustedProxy } from "./TrustedProxy";
79
106
  export type * from "./types";
80
107
  export {
81
108
  type WebsocketAuthAckData,
@@ -10,5 +10,5 @@ export const isValidDate = (d: string | Date | Dayjs) => {
10
10
  if (typeof d === "string") {
11
11
  return dayjs(d, format).isValid();
12
12
  } else if (isDayjs(d)) return d.isValid();
13
- else return d instanceof Date && !isNaN(d.getTime());
13
+ else return d instanceof Date && !Number.isNaN(d.getTime());
14
14
  };
@@ -0,0 +1,27 @@
1
+ import type { LogRecord } from "./Logger";
2
+
3
+ /** A per-request ring the Logger hands every record to; the trace decides at its end whether any of it is shown. */
4
+ export interface LogFlightRecorder {
5
+ minSev: number;
6
+ capture(record: LogRecord, written: boolean): void;
7
+ }
8
+
9
+ export interface LogContextSnapshot {
10
+ traceId: string | null;
11
+ endpoint: string | null;
12
+ origin: string | null;
13
+ flight?: LogFlightRecorder | null;
14
+ /** A per-request floor below the process level (`x-akan-debug`); `null` when the request asked for nothing. */
15
+ debugSev?: number | null;
16
+ }
17
+
18
+ export type LogContextReader = () => LogContextSnapshot | undefined;
19
+
20
+ const slot: { __akanLogContextReader?: LogContextReader | null } =
21
+ typeof process === "undefined" ? {} : (process as unknown as { __akanLogContextReader?: LogContextReader | null });
22
+
23
+ export const registerLogContextReader = (reader: LogContextReader | null) => {
24
+ slot.__akanLogContextReader = reader;
25
+ };
26
+
27
+ export const readLogContext = () => slot.__akanLogContextReader?.();
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Own and inherited enumerable data of an object as a plain record. An Akan model keeps its Date fields as
3
+ * enumerable prototype accessors, which `Object.keys` and a spread never reach; methods are non-enumerable and stay
4
+ * out either way.
5
+ */
6
+ export const plainFieldsOf = (source: object): Record<string, unknown> => {
7
+ const out: Record<string, unknown> = {};
8
+ for (const key in source) {
9
+ const value = (source as Record<string, unknown>)[key];
10
+ if (typeof value !== "function") out[key] = value;
11
+ }
12
+ return out;
13
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The origin a caller actually saw, and whether a body claims to be JSON. Both read only headers and a URL, so
3
+ * they answer the same way for the cache layer, the CSRF gate, and anything else that has to compare origins.
4
+ */
5
+ export const originFromRequest = (headers: Headers, url: URL): string => {
6
+
7
+ const forwardedProto = headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
8
+ const forwardedHost = headers.get("x-forwarded-host")?.split(",")[0]?.trim();
9
+ const host = forwardedHost ?? headers.get("host")?.split(",")[0]?.trim();
10
+ const proto = forwardedProto ?? url.protocol.slice(0, -1);
11
+ if (host && proto) {
12
+ try {
13
+ return new URL(`${proto}://${host}`).origin;
14
+ } catch {
15
+ }
16
+ }
17
+ return url.origin;
18
+ };
19
+
20
+ /**
21
+ * Whether a request body may be read as JSON.
22
+ *
23
+ * `Request.json()` parses whatever bytes it holds regardless of this header, and the three content types a
24
+ * cross-site form can send — `text/plain`, `application/x-www-form-urlencoded`, `multipart/form-data` — are
25
+ * exactly the ones a browser sends with **no preflight**. Requiring the JSON type is therefore what puts a
26
+ * preflight in front of every mutation that carries a body.
27
+ */
28
+ export const isJsonContentType = (contentType: string | null): boolean => {
29
+ if (!contentType) return false;
30
+ const essence = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
31
+ return essence === "application/json" || essence.endsWith("+json");
32
+ };
@@ -1,39 +1,46 @@
1
- export interface HttpClientOptions {
1
+ /**
2
+ * A plain REST client for a service that is not an Akan API: base url, headers, timeout, JSON in and out.
3
+ *
4
+ * Not `akanjs/fetch`'s `HttpClient`, which builds an Akan endpoint's path from its serialized args and restores
5
+ * an `Err` from the response. Two classes of the same name in one package meant an app importing both barrels
6
+ * got a collision, so this one is named for what it is.
7
+ */
8
+ export interface RestClientOptions {
2
9
  baseUrl?: string;
3
10
  headers?: HeadersInit;
4
11
  timeout?: number;
5
12
  }
6
13
 
7
- export interface HttpRequestOptions extends Omit<RequestInit, "body" | "headers" | "method"> {
14
+ export interface RestRequestOptions extends Omit<RequestInit, "body" | "headers" | "method"> {
8
15
  headers?: HeadersInit;
9
16
  timeout?: number;
10
17
  }
11
18
 
12
- export class HttpClient {
19
+ export class RestClient {
13
20
  readonly baseUrl: string;
14
21
  readonly headers: Headers;
15
22
  readonly timeout: number | undefined;
16
23
 
17
- constructor(options: string | HttpClientOptions = {}) {
24
+ constructor(options: string | RestClientOptions = {}) {
18
25
  const normalized = typeof options === "string" ? { baseUrl: options } : options;
19
26
  this.baseUrl = normalized.baseUrl?.replace(/\/+$/, "") ?? "";
20
27
  this.headers = new Headers(normalized.headers);
21
28
  this.timeout = normalized.timeout;
22
29
  }
23
30
 
24
- async get<Returns>(url: string, options?: HttpRequestOptions): Promise<Returns> {
31
+ async get<Returns>(url: string, options?: RestRequestOptions): Promise<Returns> {
25
32
  return await this.#request<Returns>("GET", url, undefined, options);
26
33
  }
27
34
 
28
- async post<Returns>(url: string, data?: unknown, options?: HttpRequestOptions): Promise<Returns> {
35
+ async post<Returns>(url: string, data?: unknown, options?: RestRequestOptions): Promise<Returns> {
29
36
  return await this.#request<Returns>("POST", url, data, options);
30
37
  }
31
38
 
32
- async put<Returns>(url: string, data?: unknown, options?: HttpRequestOptions): Promise<Returns> {
39
+ async put<Returns>(url: string, data?: unknown, options?: RestRequestOptions): Promise<Returns> {
33
40
  return await this.#request<Returns>("PUT", url, data, options);
34
41
  }
35
42
 
36
- async delete<Returns>(url: string, options?: HttpRequestOptions): Promise<Returns> {
43
+ async delete<Returns>(url: string, options?: RestRequestOptions): Promise<Returns> {
37
44
  return await this.#request<Returns>("DELETE", url, undefined, options);
38
45
  }
39
46
 
@@ -41,7 +48,7 @@ export class HttpClient {
41
48
  method: string,
42
49
  url: string,
43
50
  data?: unknown,
44
- options: HttpRequestOptions = {},
51
+ options: RestRequestOptions = {},
45
52
  ): Promise<Returns> {
46
53
  const { body, headers } = this.#makeBody(data);
47
54
  const timeout = options.timeout ?? this.timeout;
@@ -8,8 +8,10 @@ import {
8
8
  } from "akanjs/base";
9
9
 
10
10
  import type { FieldProps } from ".";
11
+ import { ConstantRegistry } from "./constantRegistry";
11
12
 
12
13
  export type CrystalizeFunc<Model> = (self: GetStateObject<Model>, isChild?: boolean) => Model;
14
+ export type Converter = (value: unknown) => unknown;
13
15
 
14
16
  /**
15
17
  * The relation instances one hydration pass has already built, by model class and then document id.
@@ -34,58 +36,76 @@ export const withSharedInstances = <T>(hydrate: () => T): T => {
34
36
  }
35
37
  };
36
38
 
37
- type ModelCls = Cls<{ set: (obj: object) => object }>;
39
+ type ModelCls = new (obj?: object) => object;
38
40
 
39
41
  const relationIdOf = (value: object): string | null => {
40
42
  const id = (value as { id?: unknown }).id;
41
43
  return typeof id === "string" && id ? id : null;
42
44
  };
43
45
 
44
- const crystalizeModel = (field: FieldProps, value: object): object => {
45
- const modelRef = field.modelRef as ModelCls;
46
+ const modelConverterOf = (props: FieldProps): Converter => {
47
+ const modelRef = props.modelRef as unknown as ModelCls;
46
48
 
47
- if (value instanceof modelRef) return value;
49
+ if (props.isScalar) return (value) => (value instanceof modelRef ? value : new modelRef(value as object));
50
+ return (value) => {
51
+ if (value instanceof modelRef) return value;
48
52
 
49
- const id = field.isScalar ? null : relationIdOf(value);
50
- if (!sharedInstances || !id) return new modelRef().set(value);
51
- const byId = sharedInstances.get(modelRef) ?? new Map<string, object>();
52
- if (!sharedInstances.has(modelRef)) sharedInstances.set(modelRef, byId);
53
- const shared = byId.get(id) ?? new modelRef().set(value);
54
- byId.set(id, shared);
55
- return shared;
53
+ const id = relationIdOf(value as object);
54
+ if (!sharedInstances || !id) return new modelRef(value as object);
55
+ const byId = sharedInstances.get(modelRef as unknown as Cls) ?? new Map<string, object>();
56
+ if (!sharedInstances.has(modelRef as unknown as Cls)) sharedInstances.set(modelRef as unknown as Cls, byId);
57
+ const shared = byId.get(id) ?? new modelRef(value as object);
58
+ byId.set(id, shared);
59
+ return shared;
60
+ };
56
61
  };
57
62
 
58
- export const crystalize = (field: FieldProps, value: unknown): unknown => {
59
- if (value === undefined || value === null) return value as undefined | null;
60
- if (field.isArray && Array.isArray(value))
61
- return value.map((v: unknown) =>
62
- crystalize({ ...field, isArray: field.arrDepth > 1, arrDepth: field.arrDepth - 1 }, v),
63
- );
64
- const crystalizeValue = PrimitiveRegistry.has(field.modelRef)
65
- ? (value: unknown) => (field.modelRef as unknown as typeof PrimitiveScalar)._parse(value as never)
66
- : (value: unknown) => value as object;
67
- if (field.isMap) {
68
- const mapValueField = {
69
- ...field,
70
- modelRef: field.of ?? field.modelRef,
71
- isMap: false,
72
- isClass: !!field.of && !PrimitiveRegistry.has(field.of),
73
- isScalar: !!field.of && PrimitiveRegistry.has(field.of),
74
- isArray: false,
75
- arrDepth: 0,
76
- };
63
+ const mapConverterOf = (props: FieldProps): Converter => {
64
+ const of = props.of as Cls | undefined;
65
+ const valueConverter: Converter = of
66
+ ? singleConverterOf({
67
+ ...props,
68
+ modelRef: of as FieldProps["modelRef"],
69
+ isMap: false,
70
+ isClass: !PrimitiveRegistry.has(of),
71
+ isScalar: PrimitiveRegistry.has(of) || ConstantRegistry.isScalar(of),
72
+ isArray: false,
73
+ arrDepth: 0,
74
+ })
75
+ : (value) => value;
76
+ return (value) => {
77
77
 
78
78
  const entries = value instanceof Map ? [...value.entries()] : Object.entries(value as Record<string, unknown>);
79
79
  return new Map(
80
- entries.map(([key, val]: [string, unknown]) => [
81
- key,
82
- field.of
83
- ? applyFnToArrayObjects(val, (v: never) => crystalize(mapValueField, v))
84
- : applyFnToArrayObjects(val, crystalizeValue),
85
- ]),
80
+ entries.map(([key, val]: [string, unknown]) => [key, applyFnToArrayObjects(val, valueConverter as never)]),
86
81
  );
82
+ };
83
+ };
84
+
85
+ const singleConverterOf = (props: FieldProps): Converter => {
86
+ if (props.isMap) return mapConverterOf(props);
87
+ if (props.isClass) return modelConverterOf(props);
88
+
89
+ if ((props.modelRef as unknown) === Date) return (value) => dayjs(value as Date);
90
+ if (PrimitiveRegistry.has(props.modelRef as Cls))
91
+ return (value) => (props.modelRef as unknown as typeof PrimitiveScalar)._parse(value as never);
92
+ return (value) => value;
93
+ };
94
+
95
+ const converters = new WeakMap<FieldProps, Converter>();
96
+
97
+ /** The converter for one field, compiled once per `FieldProps` — which `getProps()` freezes and shares per field. */
98
+ export const converterOf = (props: FieldProps): Converter => {
99
+ const cached = converters.get(props);
100
+ if (cached) return cached;
101
+ const single = singleConverterOf(props);
102
+ let convert: Converter = (value) => (value === null || value === undefined ? value : single(value));
103
+ for (let depth = props.arrDepth; depth > 0; depth--) {
104
+ const inner: Converter = convert;
105
+ convert = (value: unknown): unknown => (Array.isArray(value) ? value.map(inner) : inner(value));
87
106
  }
88
- if (field.isClass) return crystalizeModel(field, value as object);
89
- if (field.modelRef === Date) return dayjs(value as Date);
90
- return crystalizeValue(value);
107
+ converters.set(props, convert);
108
+ return convert;
91
109
  };
110
+
111
+ export const crystalize = (field: FieldProps, value: unknown): unknown => converterOf(field)(value);
@@ -0,0 +1,62 @@
1
+ import { type Dayjs, dayjs } from "akanjs/base";
2
+
3
+ /**
4
+ * A Date field stores a native `Date` under a per-key symbol and builds the `Dayjs` its type promises on first
5
+ * read, memoized by that `Date`. A dayjs instance is ~300 bytes and eight getter calls at construction; a `Date`
6
+ * is 40 bytes. A listing carries every row's dates and reads a handful of them, so this is most of what a
7
+ * hydrated list used to cost — see `crystalize`.
8
+ *
9
+ * The slot is a function of the key, not the class (`Symbol.for`): `applyMixins` copies a light model's accessors
10
+ * onto the full model's prototype, so an accessor and the `set()` that fills its slot must agree on the slot
11
+ * whichever class defined either one.
12
+ */
13
+ const slotByKey = new Map<string, symbol>();
14
+ export const dateSlotOf = (key: string): symbol => {
15
+ const cached = slotByKey.get(key);
16
+ if (cached) return cached;
17
+ const slot = Symbol.for(`akan.date.${key}`);
18
+ slotByKey.set(key, slot);
19
+ return slot;
20
+ };
21
+
22
+ const dayjsByDate = new WeakMap<Date, Dayjs>();
23
+ const lazyDayjsOf = (date: Date): Dayjs => {
24
+ const cached = dayjsByDate.get(date);
25
+ if (cached) return cached;
26
+ const value = dayjs(date);
27
+ dayjsByDate.set(date, value);
28
+ return value;
29
+ };
30
+
31
+ /** A caller's own `Date` is copied: the slot must not alias an object the caller can still mutate. */
32
+ export const toDateValue = (value: unknown): Date | null | undefined => {
33
+ if (value === null || value === undefined) return value;
34
+ if (value instanceof Date) return new Date(value.getTime());
35
+ if (dayjs.isDayjs(value)) return value.toDate();
36
+ return new Date(value as string | number);
37
+ };
38
+
39
+ type DateSlotHolder = Record<symbol, Date | null | undefined>;
40
+
41
+ /**
42
+ * Enumerable so `for...in` — `plainFieldsOf`, `immerify`, `deepObjectify` — still sees the field. immer calls a
43
+ * prototype setter with the draft as `this`, so a write through the accessor stays copy-on-write, and the getter
44
+ * writes nothing, so a read never marks a draft modified.
45
+ */
46
+ export const dateAccessorOf = (key: string): PropertyDescriptor => {
47
+ const slot = dateSlotOf(key);
48
+ return {
49
+ enumerable: true,
50
+ configurable: true,
51
+ get(this: DateSlotHolder) {
52
+ const date = this[slot];
53
+ return date === null || date === undefined ? date : lazyDayjsOf(date);
54
+ },
55
+ set(this: DateSlotHolder, value: unknown) {
56
+ this[slot] = toDateValue(value);
57
+ },
58
+ };
59
+ };
60
+
61
+ export const isDateSlotField = (props: { modelRef: unknown; isArray: boolean; isMap: boolean }): boolean =>
62
+ props.modelRef === Date && !props.isArray && !props.isMap;
@@ -2,11 +2,11 @@ import { DEFAULT_VALUE, FIELD_META, type PrimitiveScalar } from "akanjs/base";
2
2
  import type { FieldObject } from ".";
3
3
  import type { DefaultOf } from "./types";
4
4
 
5
- interface DefaultPlan {
5
+ export interface DefaultPlan {
6
6
  /** Fields whose default is a value that can be shared: a primitive, `null`, or the field's own literal. */
7
7
  shared: Record<string, unknown>;
8
8
  /** Fields that have to be produced per call — a thunk, a fresh array, or a nested scalar record. */
9
- perCall: [key: string, make: () => unknown][];
9
+ perCall: Map<string, () => unknown>;
10
10
  }
11
11
 
12
12
  const planCache = new WeakMap<FieldObject, DefaultPlan>();
@@ -17,28 +17,33 @@ const planCache = new WeakMap<FieldObject, DefaultPlan>();
17
17
  * sharing one. Only values that were already shared before this cache existed live in `shared`.
18
18
  */
19
19
  export const getDefault = <T>(fieldObj: FieldObject): DefaultOf<T> => {
20
- let plan = planCache.get(fieldObj);
21
- if (!plan) {
22
- plan = buildPlan(fieldObj);
23
- planCache.set(fieldObj, plan);
24
- }
20
+ const plan = defaultPlanOf(fieldObj);
25
21
  const result: Record<string, unknown> = { ...plan.shared };
26
22
  for (const [key, make] of plan.perCall) result[key] = make();
27
23
  return result as DefaultOf<T>;
28
24
  };
29
25
 
26
+ /** The per-field default rules, split as above; `HydrationPlan` reads them per field so a present value skips its thunk. */
27
+ export const defaultPlanOf = (fieldObj: FieldObject): DefaultPlan => {
28
+ const cached = planCache.get(fieldObj);
29
+ if (cached) return cached;
30
+ const plan = buildPlan(fieldObj);
31
+ planCache.set(fieldObj, plan);
32
+ return plan;
33
+ };
34
+
30
35
  const buildPlan = (fieldObj: FieldObject): DefaultPlan => {
31
36
  const shared: Record<string, unknown> = {};
32
- const perCall: [string, () => unknown][] = [];
37
+ const perCall = new Map<string, () => unknown>();
33
38
  for (const [key, field] of Object.entries(fieldObj)) {
34
39
  if (field.fieldType === "hidden" || field.fieldType === "secret") shared[key] = null;
35
40
  else if (field.default !== undefined && field.default !== null) {
36
- if (typeof field.default === "function") perCall.push([key, field.default as () => unknown]);
41
+ if (typeof field.default === "function") perCall.set(key, field.default as () => unknown);
37
42
  else shared[key] = field.default as object;
38
- } else if (field.isArray) perCall.push([key, () => []]);
43
+ } else if (field.isArray) perCall.set(key, () => []);
39
44
  else if (field.nullable) shared[key] = null;
40
45
  else if (field.isClass) {
41
- if (field.isScalar) perCall.push([key, () => getDefault(field.modelRef[FIELD_META])]);
46
+ if (field.isScalar) perCall.set(key, () => getDefault(field.modelRef[FIELD_META]));
42
47
  else shared[key] = null;
43
48
  } else shared[key] = (field.modelRef as unknown as typeof PrimitiveScalar)[DEFAULT_VALUE];
44
49
  }