akanjs 3.1.1 → 3.1.2-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.ko.md +22 -0
  2. package/README.md +22 -0
  3. package/client/cookie.ts +23 -7
  4. package/client/csrTypes.ts +1 -1
  5. package/client/makePageProto.tsx +10 -1
  6. package/client/router.ts +57 -16
  7. package/common/IpLiteral.ts +4 -2
  8. package/common/TrustedProxy.ts +45 -2
  9. package/common/akanNames.ts +169 -0
  10. package/common/codeAgentProfile.ts +51 -4
  11. package/common/index.ts +13 -0
  12. package/common/localeConfig.ts +30 -5
  13. package/common/localePath.ts +26 -0
  14. package/common/markdownSpans.ts +3 -1
  15. package/common/mcpExposure.ts +7 -7
  16. package/common/websocketContract.ts +50 -0
  17. package/constant/index.ts +1 -0
  18. package/constant/inputParser.ts +10 -2
  19. package/constant/requestValue.ts +47 -0
  20. package/dictionary/agent.dictionary.ts +2 -2
  21. package/dictionary/base.dictionary.ts +4 -0
  22. package/dictionary/dictInfo.ts +9 -9
  23. package/document/database.ts +1 -0
  24. package/document/documentQuery.ts +42 -6
  25. package/document/filterMeta.ts +56 -8
  26. package/document/queryEvaluator.ts +13 -8
  27. package/fetch/client/fetchClient.ts +28 -62
  28. package/fetch/client/httpClient.ts +1 -0
  29. package/fetch/client/sliceInitHandle.ts +18 -18
  30. package/fetch/index.ts +1 -0
  31. package/fetch/sessionCookie.ts +37 -0
  32. package/index.ts +8 -0
  33. package/package.json +13 -27
  34. package/server/akanApp.ts +108 -29
  35. package/server/akanServer.ts +18 -7
  36. package/server/assetHeaders.ts +20 -1
  37. package/server/cachePolicy.ts +18 -5
  38. package/server/devtools/depsSerializer.ts +2 -1
  39. package/server/devtools/devtoolsRouter.ts +3 -2
  40. package/server/devtools/signalSerializer.ts +5 -5
  41. package/server/hmr/clientReport.ts +10 -5
  42. package/server/imageOptimizer.ts +117 -22
  43. package/server/lifecycle/buildEnv.ts +11 -0
  44. package/server/lifecycle/loadBuildEnv.ts +3 -0
  45. package/server/logging/logControlSocket.ts +20 -14
  46. package/server/logging/logStreamRoute.ts +12 -8
  47. package/server/logging/rotatingLogWriter.ts +15 -1
  48. package/server/machine/machineToken.ts +4 -0
  49. package/server/mcp/McpAuth.ts +12 -4
  50. package/server/mcp/McpExecutionContext.ts +4 -0
  51. package/server/mcp/McpRouter.ts +25 -13
  52. package/server/mcp/PagePromptComposer.ts +4 -7
  53. package/server/oauth/OAuthClientIdMetadata.ts +29 -6
  54. package/server/oauth/OAuthRegistration.ts +9 -0
  55. package/server/ops/failureLimiter.ts +42 -0
  56. package/server/ops/opsCommand.ts +19 -7
  57. package/server/ops/opsRoute.ts +60 -9
  58. package/server/ops/opsToken.ts +5 -0
  59. package/server/ops/snapshotJobs.ts +3 -1
  60. package/server/ops/snapshotRestore.ts +23 -1
  61. package/server/ops/sqliteSnapshot.ts +98 -18
  62. package/server/ops/statusAccess.ts +37 -19
  63. package/server/proxy/hostBasePathWebProxy.ts +4 -7
  64. package/server/proxy/localeWebProxy.ts +36 -16
  65. package/server/resolver/database.resolver.ts +15 -11
  66. package/server/resolver/service.resolver.ts +17 -2
  67. package/server/resolver/signal.resolver.ts +50 -40
  68. package/server/routeElementComposer.tsx +42 -9
  69. package/server/routeShellGate.ts +42 -0
  70. package/server/routing/apiRouter.ts +64 -1
  71. package/server/routing/serverLimits.ts +151 -0
  72. package/server/routing/soloAppRoutes.ts +23 -12
  73. package/server/rscClient.tsx +5 -3
  74. package/server/rscScrollRestoration.ts +60 -9
  75. package/server/rscWorker.tsx +76 -24
  76. package/server/sitemap.ts +5 -7
  77. package/server/ssrFromRscRenderer.tsx +36 -4
  78. package/server/ssrTypes.ts +5 -2
  79. package/server/staticPath.ts +36 -1
  80. package/server/systemPageDocument.tsx +5 -3
  81. package/server/systemPages.tsx +14 -8
  82. package/server/tunnel/TunnelAgent.ts +19 -2
  83. package/server/tunnel/TunnelDataSocket.ts +4 -2
  84. package/server/tunnel/TunnelHttpStream.ts +7 -2
  85. package/server/tunnel/TunnelTcpStream.ts +6 -3
  86. package/server/tunnel/TunnelWebsocketStream.ts +9 -2
  87. package/server/tunnel/tunnelStream.ts +6 -0
  88. package/server/types.tsx +20 -2
  89. package/server/webRouter.ts +38 -18
  90. package/service/agent.service.ts +9 -7
  91. package/service/injectInfo.ts +2 -1
  92. package/service/liveSyncHub.ts +6 -0
  93. package/service/predefinedAdaptor/anthropicLlm.ts +13 -12
  94. package/service/predefinedAdaptor/database.adaptor.ts +4 -0
  95. package/service/predefinedAdaptor/llm.adaptor.ts +3 -0
  96. package/service/predefinedAdaptor/llmOverflow.ts +5 -1
  97. package/service/predefinedAdaptor/openaiDialect.ts +9 -8
  98. package/service/predefinedAdaptor/searchIndex.ts +21 -1
  99. package/service/predefinedAdaptor/sql/QueryCompiler.ts +28 -4
  100. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +45 -11
  101. package/service/predefinedAdaptor/sql/dialect/postgres.ts +6 -1
  102. package/service/predefinedAdaptor/sql/types.ts +3 -0
  103. package/service/predefinedAdaptor/storage.adaptor.ts +11 -10
  104. package/service/serve.ts +3 -3
  105. package/service/serviceModule.ts +28 -24
  106. package/service/types.ts +3 -0
  107. package/signal/SignalFailure.ts +5 -2
  108. package/signal/agent/AgentCatalogue.ts +3 -2
  109. package/signal/agentMeter.ts +81 -9
  110. package/signal/agentRelayPolicy.ts +53 -2
  111. package/signal/agentTurnStream.ts +12 -1
  112. package/signal/guard.ts +2 -2
  113. package/signal/mcp/McpDocument.ts +3 -3
  114. package/signal/mcp/McpUriTemplate.ts +5 -5
  115. package/signal/middleware.ts +15 -1
  116. package/signal/roomKey.ts +53 -0
  117. package/signal/signalContext.ts +100 -40
  118. package/signal/slice.ts +27 -22
  119. package/signal/trace.ts +17 -6
  120. package/store/action.ts +20 -49
  121. package/store/actionTag.ts +2 -0
  122. package/store/agent/ScreenReader.ts +36 -4
  123. package/store/agent/ScreenTarget.ts +1 -1
  124. package/store/agent/StoreCatalogue.ts +7 -1
  125. package/store/agent/StoreSurfaceSource.ts +5 -0
  126. package/store/agentic/AgentConfirm.ts +30 -0
  127. package/store/agentic/StStateBuilder.ts +2 -2
  128. package/store/agentic/StToolBuilder.ts +7 -2
  129. package/store/agentic/formFields.ts +1 -2
  130. package/store/agentic/index.ts +1 -0
  131. package/store/agentic/useFieldTool.ts +4 -3
  132. package/store/deprecatedNames.ts +6 -0
  133. package/store/draftStore.ts +2 -2
  134. package/store/formUploadOf.ts +6 -5
  135. package/store/index.ts +1 -2
  136. package/store/sliceKeys.ts +9 -32
  137. package/store/state.ts +1 -1
  138. package/store/stateBuilder.ts +1 -1
  139. package/store/store.ts +1 -1
  140. package/store/storeInstance.ts +12 -8
  141. package/store/storeRegistry.ts +14 -2
  142. package/types/client/cookie.d.ts +7 -2
  143. package/types/client/csrTypes.d.ts +1 -1
  144. package/types/client/router.d.ts +4 -1
  145. package/types/common/akanNames.d.ts +149 -0
  146. package/types/common/codeAgentProfile.d.ts +4 -1
  147. package/types/common/index.d.ts +3 -1
  148. package/types/common/localeConfig.d.ts +8 -0
  149. package/types/common/localePath.d.ts +13 -0
  150. package/types/common/websocketContract.d.ts +7 -0
  151. package/types/constant/index.d.ts +1 -0
  152. package/types/constant/requestValue.d.ts +5 -0
  153. package/types/dictionary/base.dictionary.d.ts +1 -1
  154. package/types/dictionary/dictionary.d.ts +3 -3
  155. package/types/document/database.d.ts +1 -0
  156. package/types/document/documentQuery.d.ts +3 -0
  157. package/types/document/filterMeta.d.ts +4 -0
  158. package/types/fetch/client/sliceInitHandle.d.ts +1 -2
  159. package/types/fetch/index.d.ts +1 -0
  160. package/types/fetch/sessionCookie.d.ts +12 -0
  161. package/types/index.d.ts +8 -0
  162. package/types/server/akanApp.d.ts +1 -0
  163. package/types/server/assetHeaders.d.ts +5 -0
  164. package/types/server/cachePolicy.d.ts +2 -0
  165. package/types/server/devtools/devtoolsRouter.d.ts +1 -1
  166. package/types/server/imageOptimizer.d.ts +4 -1
  167. package/types/server/lifecycle/buildEnv.d.ts +2 -0
  168. package/types/server/lifecycle/loadBuildEnv.d.ts +1 -0
  169. package/types/server/logging/logStreamRoute.d.ts +1 -1
  170. package/types/server/logging/rotatingLogWriter.d.ts +3 -0
  171. package/types/server/machine/machineToken.d.ts +2 -0
  172. package/types/server/mcp/McpAuth.d.ts +2 -1
  173. package/types/server/mcp/McpExecutionContext.d.ts +1 -0
  174. package/types/server/oauth/OAuthRegistration.d.ts +2 -0
  175. package/types/server/ops/failureLimiter.d.ts +11 -0
  176. package/types/server/ops/opsRoute.d.ts +4 -2
  177. package/types/server/ops/opsToken.d.ts +2 -0
  178. package/types/server/ops/snapshotRestore.d.ts +3 -1
  179. package/types/server/ops/sqliteSnapshot.d.ts +7 -1
  180. package/types/server/ops/statusAccess.d.ts +8 -4
  181. package/types/server/proxy/hostBasePathWebProxy.d.ts +1 -0
  182. package/types/server/resolver/signal.resolver.d.ts +0 -1
  183. package/types/server/routeElementComposer.d.ts +10 -4
  184. package/types/server/routeShellGate.d.ts +12 -0
  185. package/types/server/routing/apiRouter.d.ts +6 -1
  186. package/types/server/routing/serverLimits.d.ts +23 -0
  187. package/types/server/routing/soloAppRoutes.d.ts +1 -1
  188. package/types/server/rscScrollRestoration.d.ts +1 -0
  189. package/types/server/ssrTypes.d.ts +5 -2
  190. package/types/server/staticPath.d.ts +1 -0
  191. package/types/server/systemPageDocument.d.ts +1 -0
  192. package/types/server/tunnel/TunnelAgent.d.ts +6 -0
  193. package/types/server/tunnel/TunnelDataSocket.d.ts +2 -0
  194. package/types/server/tunnel/TunnelTcpStream.d.ts +1 -1
  195. package/types/server/tunnel/tunnelStream.d.ts +1 -0
  196. package/types/server/types.d.ts +11 -2
  197. package/types/service/agent.service.d.ts +1 -1
  198. package/types/service/liveSyncHub.d.ts +1 -0
  199. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
  200. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +1 -0
  201. package/types/service/predefinedAdaptor/openaiDialect.d.ts +1 -1
  202. package/types/service/predefinedAdaptor/searchIndex.d.ts +5 -1
  203. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +1 -0
  204. package/types/service/predefinedAdaptor/sql/types.d.ts +3 -0
  205. package/types/service/types.d.ts +3 -0
  206. package/types/signal/SignalFailure.d.ts +1 -1
  207. package/types/signal/agentMeter.d.ts +4 -1
  208. package/types/signal/agentRelayPolicy.d.ts +12 -0
  209. package/types/signal/guard.d.ts +1 -1
  210. package/types/signal/roomKey.d.ts +8 -0
  211. package/types/signal/signalContext.d.ts +20 -6
  212. package/types/signal/slice.d.ts +1 -0
  213. package/types/signal/trace.d.ts +4 -0
  214. package/types/store/actionTag.d.ts +2 -0
  215. package/types/store/agent/ScreenReader.d.ts +2 -1
  216. package/types/store/agent/StoreCatalogue.d.ts +5 -0
  217. package/types/store/agentic/AgentConfirm.d.ts +15 -0
  218. package/types/store/agentic/StToolBuilder.d.ts +3 -1
  219. package/types/store/agentic/index.d.ts +1 -0
  220. package/types/store/deprecatedNames.d.ts +5 -0
  221. package/types/store/index.d.ts +1 -2
  222. package/types/store/sliceKeys.d.ts +12 -2
  223. package/types/ui/Agent/agentSessionOf.d.ts +2 -0
  224. package/types/ui/Agent/useAccountBoundSession.d.ts +2 -0
  225. package/types/ui/Agent/useScreenActions.d.ts +1 -1
  226. package/types/ui/Icon.d.ts +15 -0
  227. package/types/ui/animated.d.ts +3 -3
  228. package/types/ui/sliceNamesOf.d.ts +4 -4
  229. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +10 -0
  230. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +6 -0
  231. package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +6 -1
  232. package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +7 -0
  233. package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +2 -0
  234. package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +2 -0
  235. package/types/vendor/use-agentic/AgentSession.d.ts +6 -1
  236. package/types/vendor/use-agentic/ToolRunner.d.ts +7 -2
  237. package/types/vendor/use-agentic/types.d.ts +2 -0
  238. package/types/vendor/use-agentic/useAgentTool.d.ts +2 -1
  239. package/types/webkit/useCsrValues.d.ts +3 -3
  240. package/ui/Agent/Chat.tsx +2 -0
  241. package/ui/Agent/Zone.tsx +2 -0
  242. package/ui/Agent/agentSessionOf.ts +54 -5
  243. package/ui/Agent/useAccountBoundSession.ts +26 -0
  244. package/ui/Agent/useScreenActions.ts +1 -1
  245. package/ui/BottomSheet.tsx +1 -1
  246. package/ui/Button.tsx +5 -5
  247. package/ui/Data/Item.tsx +2 -8
  248. package/ui/Data/ListContainer.tsx +4 -4
  249. package/ui/Data/QueryMaker.tsx +3 -2
  250. package/ui/Data/RefPicker.tsx +3 -2
  251. package/ui/Data/UnlistedEditModal.tsx +2 -1
  252. package/ui/Data/ViewActions.tsx +4 -4
  253. package/ui/Data/dataExport.ts +3 -0
  254. package/ui/Data/useViewModal.ts +10 -15
  255. package/ui/Dialog/LegacyModal.tsx +1 -1
  256. package/ui/Dialog/Provider.tsx +2 -2
  257. package/ui/DragAction.tsx +1 -1
  258. package/ui/Dropdown.tsx +2 -2
  259. package/ui/Field/lightRefCache.ts +2 -2
  260. package/ui/Field/useRelationOptions.ts +3 -10
  261. package/ui/Icon.tsx +87 -0
  262. package/ui/KeyboardAvoiding.tsx +1 -1
  263. package/ui/Layout/Sider.tsx +3 -3
  264. package/ui/Link/Anchor.tsx +4 -1
  265. package/ui/Load/Edit.tsx +2 -2
  266. package/ui/Load/Edit_Client.tsx +3 -3
  267. package/ui/Load/Pagination.tsx +3 -3
  268. package/ui/Load/View.tsx +15 -14
  269. package/ui/Loading/ProgressBar.tsx +1 -1
  270. package/ui/Model/DraftBar.tsx +7 -14
  271. package/ui/Model/EditModal.tsx +9 -40
  272. package/ui/Model/EditWrapper.tsx +3 -5
  273. package/ui/Model/NewWrapper_Client.tsx +3 -7
  274. package/ui/Model/Remove.tsx +2 -2
  275. package/ui/Model/RemoveWrapper.tsx +2 -4
  276. package/ui/Model/SureToRemove.tsx +2 -2
  277. package/ui/Model/ViewEditModal.tsx +9 -20
  278. package/ui/Model/ViewModal.tsx +4 -11
  279. package/ui/Model/ViewWrapper.tsx +4 -6
  280. package/ui/ScreenNavigator.tsx +2 -2
  281. package/ui/Signal/WebSocket.tsx +2 -2
  282. package/ui/System/CSR.tsx +3 -3
  283. package/ui/System/Reconnect.tsx +2 -2
  284. package/ui/System/SSR.tsx +2 -2
  285. package/ui/System/SelectLanguage.tsx +1 -1
  286. package/ui/System/ThemeToggle.tsx +1 -1
  287. package/ui/Tab/Provider.tsx +1 -0
  288. package/ui/Toast.tsx +7 -15
  289. package/ui/index.ts +0 -1
  290. package/ui/sliceNamesOf.ts +25 -15
  291. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +2 -1
  292. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +10 -2
  293. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +4 -0
  294. package/vendor/@akanjs/native/native/desktop/src/acl.rs +166 -0
  295. package/vendor/@akanjs/native/native/desktop/src/lib.rs +25 -6
  296. package/vendor/@akanjs/native/native/desktop/src/navigation.rs +22 -10
  297. package/vendor/@akanjs/native/native/desktop/src/routes.rs +2 -2
  298. package/vendor/@akanjs/native/native/desktop/src/vectors.rs +28 -0
  299. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +1 -1
  300. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +10 -2
  301. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +4 -0
  302. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +1 -0
  303. package/vendor/@akanjs/native/packages/cli/src/config.ts +10 -0
  304. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +2 -0
  305. package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +5 -3
  306. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +4 -0
  307. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +2 -0
  308. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +30 -0
  309. package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +1 -1
  310. package/vendor/@akanjs/native/packages/core/src/kernel.ts +18 -2
  311. package/vendor/@akanjs/native/packages/core/vectors/navigation.json +22 -1
  312. package/vendor/@akanjs/native/packages/core/vectors/routes.json +4 -0
  313. package/vendor/@akanjs/native/packages/desktop/src/main.ts +5 -1
  314. package/vendor/@akanjs/native/packages/desktop/src/paths.ts +24 -1
  315. package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +4 -5
  316. package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +6 -8
  317. package/vendor/@akanjs/native/plugins/filesystem/src/desktop.ts +4 -2
  318. package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +22 -3
  319. package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +6 -0
  320. package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +6 -0
  321. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +106 -18
  322. package/vendor/use-agentic/AgentSession.ts +7 -2
  323. package/vendor/use-agentic/ToolRunner.ts +44 -5
  324. package/vendor/use-agentic/types.ts +2 -0
  325. package/vendor/use-agentic/useAgentTool.ts +4 -2
  326. package/webkit/McpAppAgent.ts +2 -1
  327. package/webkit/bootCsr.tsx +1 -1
  328. package/webkit/useCsrValues.ts +5 -1
  329. package/webkit/usePageTool.tsx +1 -0
  330. package/store/databaseStateNames.ts +0 -24
  331. package/store/formSetterNames.ts +0 -18
  332. package/types/store/databaseStateNames.d.ts +0 -18
  333. package/types/store/formSetterNames.d.ts +0 -11
@@ -1,58 +1,76 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
2
  import { bearerTokenOf, Logger } from "akanjs/common";
3
+ import { isLocalDevRuntime } from "../../signal/trace";
4
+ import { FailureLimiter } from "./failureLimiter";
3
5
 
4
6
  interface StatusAccessOptions {
5
7
  tokens: (string | null | undefined)[];
6
8
  open?: boolean;
9
+ failuresPerMinute?: number;
7
10
  }
8
11
 
9
12
  export class StatusAccess {
13
+ static readonly minTokenLength = 32;
10
14
  static readonly #noStore = { "cache-control": "no-store" };
11
15
  static readonly #warned = new Set<string>();
12
16
  readonly #digests: Buffer[];
13
17
  readonly #open: boolean;
18
+ readonly #failures: FailureLimiter;
14
19
 
15
- constructor({ tokens, open = false }: StatusAccessOptions) {
16
- this.#digests = tokens.filter((token): token is string => !!token).map((token) => StatusAccess.#digest(token));
20
+ constructor({ tokens, open = false, failuresPerMinute = 10 }: StatusAccessOptions) {
21
+ this.#digests = tokens.filter((token): token is string => !!token).map((token) => StatusAccess.digest(token));
17
22
  this.#open = open;
23
+ this.#failures = new FailureLimiter({ limit: failuresPerMinute });
18
24
  }
19
25
 
20
26
  static fromEnv(): StatusAccess {
21
27
  return new StatusAccess({
22
- tokens: [StatusAccess.#tokenOf("AKAN_STATUS_TOKEN"), StatusAccess.#tokenOf("AKAN_LOG_STREAM_TOKEN")],
23
- open: process.env.AKAN_PUBLIC_ENV === "local",
28
+ tokens: [StatusAccess.tokenOf("AKAN_STATUS_TOKEN"), StatusAccess.tokenOf("AKAN_LOG_STREAM_TOKEN")],
29
+ open: isLocalDevRuntime(),
24
30
  });
25
31
  }
26
32
 
27
- static #tokenOf(name: string): string | null {
33
+ static tokenOf(name: string): string | null {
28
34
  const token = process.env[name]?.trim();
29
35
  if (!token) return null;
30
- if (!/\s/.test(token)) return token;
31
- if (!StatusAccess.#warned.has(name))
32
- new Logger("StatusAccess").warn(`${name} must be one word, with no whitespace; it does not unlock status detail`);
33
- StatusAccess.#warned.add(name);
34
- return null;
36
+ if (/\s/.test(token)) {
37
+ StatusAccess.#warnOnce(`${name} must be one word, with no whitespace; it unlocks nothing`);
38
+ return null;
39
+ }
40
+ if (token.length < StatusAccess.minTokenLength)
41
+ StatusAccess.#warnOnce(
42
+ `${name} is shorter than ${StatusAccess.minTokenLength} characters, short enough to guess; use a random one`,
43
+ );
44
+ return token;
35
45
  }
36
46
 
37
- static #digest(token: string): Buffer {
47
+ static #warnOnce(message: string) {
48
+ if (StatusAccess.#warned.has(message)) return;
49
+ StatusAccess.#warned.add(message);
50
+ new Logger("StatusAccess").warn(message);
51
+ }
52
+
53
+ static digest(token: string): Buffer {
38
54
  return createHash("sha256").update(token).digest();
39
55
  }
40
56
 
41
- allows(req: Request): boolean {
57
+ allows(req: Request, client?: string | null): boolean {
42
58
  if (this.#open) return true;
43
59
  const token = bearerTokenOf(req.headers.get("authorization"));
44
- if (!token) return false;
45
- const presented = StatusAccess.#digest(token);
46
- return this.#digests.reduce((matched, digest) => timingSafeEqual(presented, digest) || matched, false);
60
+ if (!token || this.#failures.blocked(client)) return false;
61
+ const presented = StatusAccess.digest(token);
62
+ const matched = this.#digests.reduce((matched, digest) => timingSafeEqual(presented, digest) || matched, false);
63
+ if (!matched) this.#failures.fail(client);
64
+ return matched;
47
65
  }
48
66
 
49
- health(req: Request, detail: () => { status: string }): Response {
67
+ health(req: Request, detail: () => { status: string }, client?: string | null): Response {
50
68
  const body = detail();
51
- return Response.json(this.allows(req) ? body : { status: body.status }, { headers: StatusAccess.#noStore });
69
+ return Response.json(this.allows(req, client) ? body : { status: body.status }, { headers: StatusAccess.#noStore });
52
70
  }
53
71
 
54
- metrics(req: Request, detail: () => unknown): Response {
55
- if (!this.allows(req))
72
+ metrics(req: Request, detail: () => unknown, client?: string | null): Response {
73
+ if (!this.allows(req, client))
56
74
  return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": "Bearer" } });
57
75
  return Response.json(detail(), { headers: StatusAccess.#noStore });
58
76
  }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getEnv } from "akanjs/base";
4
- import { Logger, resolveSubRouteHosts } from "akanjs/common";
4
+ import { LocalePath, Logger, parseAkanI18nEnv, resolveSubRouteHosts } from "akanjs/common";
5
5
  import type { BaseBuildArtifact } from "../types";
6
6
  import { AkanResponse, type WebProxy } from "./types";
7
7
 
@@ -29,11 +29,8 @@ export class HostBasePathWebProxy implements WebProxy {
29
29
 
30
30
  if (firstPath === basePath) {
31
31
  if (isInternalProxyRequest(requestUrl)) return AkanResponse.next({ request: { headers } });
32
- const publicPath = `/${[locale, ...segments.slice(3)].filter(Boolean).join("/")}`;
33
- return Response.redirect(
34
- new URL(`${publicPath || `/${locale}`}${targetUrl.search}`, getPublicRequestUrl(request)),
35
- 308,
36
- );
32
+ const publicPath = LocalePath.join(locale, `/${segments.slice(3).filter(Boolean).join("/")}`, parseAkanI18nEnv());
33
+ return Response.redirect(new URL(`${publicPath}${targetUrl.search}`, getPublicRequestUrl(request)), 308);
37
34
  }
38
35
 
39
36
  const internalPath = `/${[locale, basePath, ...segments.slice(2)].filter(Boolean).join("/")}`;
@@ -114,7 +111,7 @@ function getProxyTargetUrl(requestUrl: URL): URL {
114
111
  return target ? new URL(target, requestUrl.origin) : new URL(requestUrl);
115
112
  }
116
113
 
117
- function toProxyRequestUrl(requestUrl: URL, targetUrl: URL): URL {
114
+ export function toProxyRequestUrl(requestUrl: URL, targetUrl: URL): URL {
118
115
  if (!isInternalProxyRequest(requestUrl)) return targetUrl;
119
116
  const rewritten = new URL(requestUrl);
120
117
  rewritten.searchParams.set("url", `${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`);
@@ -1,6 +1,6 @@
1
1
  import { getApiPrefix } from "akanjs/base";
2
- import { parseAkanI18nEnv } from "akanjs/common";
3
- import { getPublicRequestUrl, isInternalProxyRequest } from "./hostBasePathWebProxy";
2
+ import { LocalePath, parseAkanI18nEnv } from "akanjs/common";
3
+ import { getPublicRequestUrl, isInternalProxyRequest, toProxyRequestUrl } from "./hostBasePathWebProxy";
4
4
  import { AkanResponse, type WebProxy } from "./types";
5
5
 
6
6
  function getLocale(request: Bun.BunRequest): string {
@@ -46,39 +46,59 @@ export class LocaleWebProxy implements WebProxy {
46
46
 
47
47
  const targetUrl = getProxyTargetUrl(requestUrl);
48
48
  const pathname = targetUrl.pathname;
49
- const pathnameIsMissingLocale = i18n.locales.every(
50
- (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
51
- );
49
+ const isInternal = isInternalProxyRequest(requestUrl);
50
+ const isPage = !isInternal && !isWellKnownRequest(pathname) && !isApiRequest(pathname);
51
+ const { locale, path, isPrefixed } = LocalePath.split(pathname, i18n);
52
52
 
53
- if (
54
- !isInternalProxyRequest(requestUrl) &&
55
- !isWellKnownRequest(pathname) &&
56
- !isApiRequest(pathname) &&
57
- pathnameIsMissingLocale
58
- ) {
53
+ if (isPage && !locale) {
59
54
  return Response.redirect(
60
55
  new URL(`/${getLocale(request)}/${pathname.slice(1)}${targetUrl.search}`, getPublicRequestUrl(request)),
61
56
  307,
62
57
  );
63
58
  }
64
59
 
65
- const splits = pathname.split("/");
66
- const [, locale = i18n.defaultLocale] = splits;
67
- const headers = new Headers(request.headers);
60
+ if (isPage && locale && LocalePath.omits(locale, i18n)) {
61
+ const publicUrl = getPublicRequestUrl(request);
62
+ if (isCsrRequest(targetUrl) && !isPrefixed)
63
+ return Response.redirect(onOrigin(publicUrl, `/${locale}${pathname}`, targetUrl.search), 307);
64
+
65
+ if (!isCsrRequest(targetUrl) && isPrefixed)
66
+ return new Response(null, {
67
+ status: 308,
68
+ headers: { location: onOrigin(publicUrl, path, targetUrl.search).href, "cache-control": "max-age=86400" },
69
+ });
70
+ }
68
71
 
69
- headers.set("x-locale", i18n.locales.includes(locale) ? locale : i18n.defaultLocale);
70
- headers.set("x-path", `/${splits.slice(2).join("/")}`);
72
+ const headers = new Headers(request.headers);
73
+ headers.set("x-locale", locale ?? i18n.defaultLocale);
74
+ headers.set("x-path", path);
71
75
 
76
+ if (locale && !isPrefixed && (isPage || isInternal)) {
77
+ const localizedUrl = new URL(targetUrl);
78
+ localizedUrl.pathname = `/${locale}${pathname}`;
79
+ return AkanResponse.rewrite(toProxyRequestUrl(requestUrl, localizedUrl), { request: { headers } });
80
+ }
72
81
  return AkanResponse.next({ request: { headers } });
73
82
  }
74
83
  }
75
84
 
85
+ function onOrigin(publicUrl: URL, pathname: string, search: string): URL {
86
+ const location = new URL(publicUrl);
87
+ location.pathname = pathname;
88
+ location.search = search;
89
+ return location;
90
+ }
91
+
76
92
  function getProxyTargetUrl(requestUrl: URL): URL {
77
93
  if (!isInternalProxyRequest(requestUrl)) return requestUrl;
78
94
  const target = requestUrl.searchParams.get("url");
79
95
  return target ? new URL(target, requestUrl.origin) : requestUrl;
80
96
  }
81
97
 
98
+ function isCsrRequest(url: URL): boolean {
99
+ return url.searchParams.get("csr") === "true";
100
+ }
101
+
82
102
  function isWellKnownRequest(pathname: string): boolean {
83
103
  return pathname === "/.well-known" || pathname.startsWith("/.well-known/");
84
104
  }
@@ -1,5 +1,5 @@
1
1
  import type { PromiseOrObject } from "akanjs/base";
2
- import { applyMixins, capitalize } from "akanjs/common";
2
+ import { applyMixins, capitalize, modelNames } from "akanjs/common";
3
3
  import { type ConstantModel, type QueryOf, resolvePageLimit, resolvePageSkip } from "akanjs/constant";
4
4
  import {
5
5
  assertFilterFitsCrud,
@@ -65,6 +65,7 @@ export class DatabaseResolver {
65
65
  database: DatabaseModel,
66
66
  ): { adaptor: AdaptorCls<DatabaseInstance>; schema: DocumentSchema } {
67
67
  const [modelName, className]: [string, string] = [database.refName, capitalize(database.refName)];
68
+ const names = modelNames(modelName);
68
69
 
69
70
  const resolveSort = (sortKey?: string | null): { [key: string]: 1 | -1 } | null => {
70
71
  if (!sortKey) return null;
@@ -133,10 +134,10 @@ export class DatabaseResolver {
133
134
  const listenPreHook = listen("pre");
134
135
  const listenPostHook = listen("post");
135
136
 
136
- class DatabaseModelInstance extends adapt(`${modelName}Model`, ({ plug }) => ({
137
+ class DatabaseModelInstance extends adapt(names.modelModel, ({ plug }) => ({
137
138
  __database: plug(DatabaseAdaptorRole, (database) => database),
138
139
  __cache: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
139
- [`${modelName}Cache` as never]: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
140
+ [names.modelCache as never]: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
140
141
  })) {
141
142
  declare readonly __database: DatabaseAdaptor;
142
143
  __store!: DocumentStore;
@@ -154,11 +155,11 @@ export class DatabaseResolver {
154
155
  const byId = new Map(docs.map((doc) => [String(doc.id), doc]));
155
156
  return ids.map((id) => byId.get(String(id)) ?? null);
156
157
  },
157
- { name: `${modelName}Loader`, cache: false },
158
+ { name: names.modelLoader, cache: false },
158
159
  );
159
160
  Object.assign(this, {
160
161
  [className]: this.__model,
161
- [`${modelName}Loader`]: this.__loader,
162
+ [names.modelLoader]: this.__loader,
162
163
  });
163
164
  Object.entries(getLoaderInfos(database.model)).forEach(([key, loaderInfo]) => {
164
165
  Object.assign(this, {
@@ -373,19 +374,19 @@ export class DatabaseResolver {
373
374
  if (!doc) throw new NoDocumentError(`No Document (${database.refName}): ${id}`);
374
375
  return doc;
375
376
  }
376
- async [`get${className}`](id: string) {
377
+ async [names.getModel](id: string) {
377
378
  return this.__get(id);
378
379
  }
379
380
  async __load(id?: string) {
380
381
  return (id ? await this.__loader.load(id) : null) as unknown;
381
382
  }
382
- async [`load${className}`](id?: string) {
383
+ async [names.loadModel](id?: string) {
383
384
  return this.__load(id);
384
385
  }
385
386
  async __loadMany(ids: string[]) {
386
387
  return await this.__loader.loadMany(ids);
387
388
  }
388
- async [`load${className}Many`](ids: string[]) {
389
+ async [names.loadModelMany](ids: string[]) {
389
390
  return this.__loadMany(ids);
390
391
  }
391
392
  async clone(data: DataInputOf<any, any> & { id: string }) {
@@ -394,13 +395,16 @@ export class DatabaseResolver {
394
395
  async __create(data: DataInputOf<any, any>) {
395
396
  return await timedQuery(() => this.__store.create(data));
396
397
  }
397
- async [`create${className}`](data: DataInputOf<any, any>) {
398
+ async [names.createModel](data: DataInputOf<any, any>) {
398
399
  return this.__create(data);
399
400
  }
401
+ async __createMany(data: DataInputOf<any, any>[]) {
402
+ return await timedQuery(() => this.__store.createMany(data));
403
+ }
400
404
  async __update(id: string, data: DataInputOf<any, any>) {
401
405
  return await timedQuery(() => this.__store.update(id, data));
402
406
  }
403
- async [`update${className}`](id: string, data: DataInputOf<any, any>) {
407
+ async [names.updateModel](id: string, data: DataInputOf<any, any>) {
404
408
  return this.__update(id, data);
405
409
  }
406
410
  async __remove(id: string) {
@@ -421,7 +425,7 @@ export class DatabaseResolver {
421
425
  async __updateOne(query: QueryOf<any>, update: DocumentUpdateInput, options?: DocumentTouchOptions) {
422
426
  return await timedQuery(() => this.__store.updateOneByQuery(query, update, ...touchArgs(options)));
423
427
  }
424
- async [`remove${className}`](id: string) {
428
+ async [names.removeModel](id: string) {
425
429
  return this.__remove(id);
426
430
  }
427
431
  }
@@ -1,4 +1,4 @@
1
- import { capitalize } from "akanjs/common";
1
+ import { capitalize, modelNames } from "akanjs/common";
2
2
  import type { QueryOf } from "akanjs/constant";
3
3
  import type { DatabaseModel, DataInputOf, Doc, DocumentTouchOptions, DocumentUpdateInput } from "akanjs/document";
4
4
  import { type DatabaseService, type ServiceCls, ServiceModel } from "akanjs/service";
@@ -17,6 +17,15 @@ export class ServiceResolver {
17
17
  const doc = await this.__databaseModel.__create(input);
18
18
  return await this.__libsPostCreate(doc);
19
19
  },
20
+ async __createMany(this: DatabaseService, data: DataInputOf[]) {
21
+ if (!data.length) return [];
22
+ const inputs: DataInputOf[] = [];
23
+ for (const row of data) inputs.push(await this.__libsPreCreate(row));
24
+ const created: Doc[] = [];
25
+ for (const doc of await this.__databaseModel.__createMany(inputs))
26
+ created.push(await this.__libsPostCreate(doc));
27
+ return created;
28
+ },
20
29
  async __update(this: DatabaseService, id: string, data: DataInputOf) {
21
30
  const input = await this.__libsPreUpdate(id, data);
22
31
  const doc = await this.__databaseModel.__update(id, input);
@@ -56,7 +65,13 @@ export class ServiceResolver {
56
65
  }
57
66
  static resolveDatabaseService(database: DatabaseModel, srvRef: ServiceCls, cascade: CascadeRunner): ServiceCls {
58
67
  const className = capitalize(database.refName);
59
- Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(database.refName, className, cascade));
68
+
69
+ const createMany = modelNames(database.refName).createModelMany;
70
+ const methods = Object.entries(ServiceResolver.#getDefaultDbServiceMethods(database.refName, className, cascade));
71
+ Object.assign(
72
+ srvRef.prototype,
73
+ Object.fromEntries(methods.filter(([key]) => key !== createMany || !(key in srvRef.prototype))),
74
+ );
60
75
  DatabaseResolver.applyFilterMethods(srvRef.prototype, database, className);
61
76
  return srvRef;
62
77
  }
@@ -13,7 +13,7 @@ import {
13
13
  type PromiseOrObject,
14
14
  SLICE_META,
15
15
  } from "akanjs/base";
16
- import { capitalize, Logger } from "akanjs/common";
16
+ import { capitalize, Logger, modelNames, moduleNames, sliceNames } from "akanjs/common";
17
17
  import {
18
18
  type ConstantField,
19
19
  deserialize,
@@ -35,7 +35,6 @@ import {
35
35
  type WebsocketAdaptor,
36
36
  WebsocketAdaptorRole,
37
37
  } from "akanjs/service";
38
- import { websocketRoomContract } from "../../common/websocketContract";
39
38
  import { type Endpoint, type EndpointCls, sliceEndpoint } from "../../signal/endpoint";
40
39
  import { EndpointErrorContract } from "../../signal/endpointErrorContract";
41
40
  import type { EndpointInfo } from "../../signal/endpointInfo";
@@ -43,6 +42,7 @@ import type { Internal, InternalCls } from "../../signal/internal";
43
42
  import { Machine } from "../../signal/internalArg";
44
43
  import type { InternalInfo } from "../../signal/internalInfo";
45
44
  import type { MiddlewareCls } from "../../signal/middleware";
45
+ import { RoomKey } from "../../signal/roomKey";
46
46
  import type { ServerSignal, ServerSignalCls } from "../../signal/serverSignal";
47
47
  import { SignalContext, type WebSocketExecutionContext } from "../../signal/signalContext";
48
48
  import type { SliceCls } from "../../signal/slice";
@@ -70,9 +70,6 @@ type HttpMethodRoutes = Record<string, (req: Bun.BunRequest) => Response | Promi
70
70
  export class SignalResolver {
71
71
  static logger = new Logger("SignalResolver");
72
72
 
73
- static makeRoomId(key: string, args: unknown[]) {
74
- return websocketRoomContract.idOf(key, args);
75
- }
76
73
  static #localPublish: LocalPublish = () => {
77
74
  SignalResolver.logger.verbose(`Local publish is not initialized yet`);
78
75
  };
@@ -143,7 +140,7 @@ export class SignalResolver {
143
140
  registry,
144
141
  live,
145
142
  });
146
- const roomId = SignalResolver.makeRoomId(key, roomArgs);
143
+ const roomId = RoomKey.idOfPublished(key, endpointInfo.args, roomArgs);
147
144
  if (isBinaryFrame) {
148
145
  const bytes = Binary._parse(resolvedData as Uint8Array) as Uint8Array;
149
146
  websocket.publish(roomId, bytes, { coalesce: SignalResolver.coalescesRoom(roomId) });
@@ -197,7 +194,7 @@ export class SignalResolver {
197
194
  const refName = cnst.refName;
198
195
  const liveKeys = Object.entries(sliceMeta)
199
196
  .filter(([, sliceInfo]) => sliceInfo.liveOption && sliceInfo.execFn)
200
- .map(([key]) => `${refName}Live${capitalize(key)}`);
197
+ .map(([key]) => sliceNames(refName, key).endpoint.live);
201
198
  if (!liveKeys.length) return [];
202
199
  const service = live.service.get(refName) as
203
200
  | {
@@ -387,8 +384,8 @@ export class SignalResolver {
387
384
  const cnst = sliceCls.srv.cnst;
388
385
  if (!cnst) throw new Error("Constant is not set for slice");
389
386
  const refName = cnst.refName;
390
- const serviceName = `${refName}Service`;
391
- const capitalizedRefName = capitalize(refName);
387
+ const serviceName = moduleNames(refName).service;
388
+ const names = modelNames(refName);
392
389
 
393
390
  const assertSliceQuery = (query: unknown, key: string) => {
394
391
  if (Array.isArray(query))
@@ -404,10 +401,10 @@ export class SignalResolver {
404
401
  const endpointObj: { [key: string]: EndpointInfo } = {};
405
402
  Object.entries(sliceMeta).forEach(([key, sliceInfo]) => {
406
403
  if (!sliceInfo.execFn) return;
407
- const capitalizedKey = capitalize(key);
404
+ const { slice: sliceName, endpoint: namesOfSlice } = sliceNames(refName, key);
408
405
  const argLength = sliceInfo.args.length;
409
406
 
410
- const listKey = `${refName}List${capitalizedKey}`;
407
+ const listKey = namesOfSlice.list;
411
408
  endpointObj[listKey] = (builder as any)
412
409
  .query([sliceInfo.light], sliceInfo.signalOption)
413
410
  ._addArgs(sliceInfo.args)
@@ -418,11 +415,7 @@ export class SignalResolver {
418
415
  .exec(async function (this: any, ...requestArgs: any) {
419
416
  const args = requestArgs.slice(0, argLength);
420
417
  const skip = resolvePageSkip(requestArgs[argLength]);
421
- const limit = SignalResolver.#clientPageLimit(
422
- listKey,
423
- `${capitalizedRefName}${capitalizedKey}`,
424
- requestArgs[argLength + 1],
425
- );
418
+ const limit = SignalResolver.#clientPageLimit(listKey, capitalize(sliceName), requestArgs[argLength + 1]);
426
419
  const sort = requestArgs[argLength + 2] ?? "latest";
427
420
  const internalArgs = requestArgs.slice(argLength + 3);
428
421
  const query = assertSliceQuery(
@@ -445,7 +438,7 @@ export class SignalResolver {
445
438
  );
446
439
  SignalResolver.#assertLiveSort(refName, key, sliceInfo);
447
440
  SignalResolver.#assertLivePauseOn(refName, key, sliceInfo);
448
- const liveKey = `${refName}Live${capitalizedKey}`;
441
+ const liveKey = namesOfSlice.live;
449
442
 
450
443
  const liveBuilder = (builder as any).pubsub(Any, {
451
444
  ...sliceInfo.signalOption,
@@ -474,7 +467,7 @@ export class SignalResolver {
474
467
  });
475
468
  }
476
469
 
477
- const insightKey = `${refName}Insight${capitalizedKey}`;
470
+ const insightKey = namesOfSlice.insight;
478
471
  endpointObj[insightKey] = (builder as any)
479
472
  .query(sliceInfo.insight, sliceInfo.signalOption)
480
473
  ._addArgs(sliceInfo.args)
@@ -491,23 +484,23 @@ export class SignalResolver {
491
484
  });
492
485
 
493
486
  if (sliceCls.crud.get) {
494
- endpointObj[refName] = (builder as any)
487
+ endpointObj[names.model] = (builder as any)
495
488
  .query(cnst.full, { guards: sliceCls.getGuards })
496
- .param(`${refName}Id`, ID)
489
+ .param(names.modelId, ID)
497
490
  .exec(async function (this: any, id: string) {
498
- return await this[serviceName][`get${capitalizedRefName}`](id);
491
+ return await this[serviceName][names.getModel](id);
499
492
  });
500
493
 
501
- endpointObj[`light${capitalizedRefName}`] = (builder as any)
494
+ endpointObj[names.lightModel] = (builder as any)
502
495
  .query(cnst.light, { guards: sliceCls.getGuards })
503
- .param(`${refName}Id`, ID)
496
+ .param(names.modelId, ID)
504
497
  .exec(async function (this: any, id: string) {
505
- return await this[serviceName][`get${capitalizedRefName}`](id);
498
+ return await this[serviceName][names.getModel](id);
506
499
  });
507
500
  }
508
501
 
509
502
  if (sliceCls.crud.create)
510
- endpointObj[`create${capitalizedRefName}`] = (builder as any)
503
+ endpointObj[names.createModel] = (builder as any)
511
504
  .mutation(cnst.full, { guards: sliceCls.createGuards })
512
505
  .body("data", cnst.input)
513
506
  .exec(async function (this: any, data: any) {
@@ -515,18 +508,18 @@ export class SignalResolver {
515
508
  });
516
509
 
517
510
  if (sliceCls.crud.update)
518
- endpointObj[`update${capitalizedRefName}`] = (builder as any)
511
+ endpointObj[names.updateModel] = (builder as any)
519
512
  .mutation(cnst.full, { guards: sliceCls.updateGuards })
520
- .param(`${refName}Id`, ID)
513
+ .param(names.modelId, ID)
521
514
  .body("data", cnst.input)
522
515
  .exec(async function (this: any, id: string, data: any) {
523
516
  return await this[serviceName].__update(id, data);
524
517
  });
525
518
 
526
519
  if (sliceCls.crud.remove)
527
- endpointObj[`remove${capitalizedRefName}`] = (builder as any)
520
+ endpointObj[names.removeModel] = (builder as any)
528
521
  .mutation(cnst.full, { guards: sliceCls.removeGuards })
529
- .param(`${refName}Id`, ID)
522
+ .param(names.modelId, ID)
530
523
  .exec(async function (this: any, id: string) {
531
524
  return await this[serviceName].__remove(id);
532
525
  });
@@ -658,7 +651,7 @@ export class SignalResolver {
658
651
  const routeOptions: NonNullable<SignalRoutes["routeOptions"]> = {};
659
652
  const wsRoutes: WebsocketRoutes = {};
660
653
  const defaultPrefix = endpointCls.srv.cnst?.refName;
661
- SignalResolver.#warnUnguarded(endpointMeta);
654
+ SignalResolver.#checkUnguarded(endpointMeta);
662
655
  SignalResolver.#warnMachineKeyless(endpointMeta);
663
656
  SignalResolver.#warnUndefinedErrors(endpointMeta);
664
657
  Object.entries(endpointMeta).forEach(([key, endpointInfo]) => {
@@ -711,11 +704,12 @@ export class SignalResolver {
711
704
 
712
705
  const query = subscribe ? await context.exec() : null;
713
706
 
714
- const requestRoomId = context.getRoomId(key);
707
+ const requestRoomId = context.getRequestRoomId(key);
708
+ const baseRoomId = context.getRoomId(key);
715
709
  if (subscribe) {
716
710
  const isClosed = SignalResolver.#closedWs.has(ws);
717
711
 
718
- if (isClosed || SignalResolver.#pendingSubscribes.isLeft(ws, requestRoomId, arrival)) {
712
+ if (isClosed || SignalResolver.#pendingSubscribes.isLeft(ws, baseRoomId, arrival)) {
719
713
  await SignalResolver.#runLifecycleHandlers(
720
714
  [context as SignalContext<WebSocketExecutionContext>],
721
715
  isClosed ? ["unsubscribe", "disconnect"] : ["unsubscribe"],
@@ -729,7 +723,7 @@ export class SignalResolver {
729
723
  };
730
724
  return unsubscribed;
731
725
  }
732
- const roomId = liveOption ? context.getLiveRoomId(key) : requestRoomId;
726
+ const roomId = liveOption ? context.getLiveRoomId(key, query) : baseRoomId;
733
727
  const roomCtxMap = SignalResolver.#liveWsPubsubRoomCtx.get(ws) ?? new Map();
734
728
  if (liveOption && !roomCtxMap.has(roomId))
735
729
  live.syncHub.join({
@@ -763,10 +757,10 @@ export class SignalResolver {
763
757
  };
764
758
  return ack;
765
759
  }
766
- SignalResolver.#pendingSubscribes.leave(ws, requestRoomId, arrival);
760
+ SignalResolver.#pendingSubscribes.leave(ws, baseRoomId, arrival);
767
761
 
768
762
  const heldRooms = [...(SignalResolver.#liveWsPubsubRoomCtx.get(ws) ?? [])].filter(
769
- ([, roomCtx]) => roomCtx.getRoomId(roomCtx.key) === requestRoomId,
763
+ ([, roomCtx]) => roomCtx.getRoomId(roomCtx.key) === baseRoomId,
770
764
  );
771
765
  for (const [roomId, roomCtx] of heldRooms) {
772
766
  SignalResolver.#leaveWsRoom(ws, roomId, roomCtx, websocket, liveOption ? live : undefined);
@@ -807,15 +801,31 @@ export class SignalResolver {
807
801
  return { routes, wsRoutes, routeOptions };
808
802
  }
809
803
 
810
- static #warnUnguarded(endpointMeta: { [key: string]: EndpointInfo }) {
811
- const open = Object.entries(endpointMeta)
804
+ static #checkUnguarded(endpointMeta: { [key: string]: EndpointInfo }) {
805
+ const unguarded = Object.entries(endpointMeta)
812
806
  .filter(([, endpointInfo]) => !endpointInfo.signalOption.guards?.length)
813
807
  .map(([key]) => key);
814
- if (!open.length) return;
815
- SignalResolver.logger.warn(
816
- `${open.join(", ")} declare${open.length === 1 ? "s" : ""} no guards, so anyone can call ${open.length === 1 ? "it" : "them"} over HTTP and WebSocket. Name the guards, or \`guards: [Public]\` to keep one open on purpose.`,
808
+ if (!unguarded.length) return;
809
+ const names = `${unguarded.join(", ")} declare${unguarded.length === 1 ? "s" : ""} no guards`;
810
+ const fix = "Name the guards, or `guards: [Public]` to open one on purpose.";
811
+ if (SignalContext.unguardedOpen()) {
812
+ SignalResolver.logger.warn(`${names}, and AKAN_UNGUARDED_ENDPOINTS=open lets anyone call them. ${fix}`);
813
+ return;
814
+ }
815
+ if (SignalResolver.#isLocalOperation()) {
816
+ SignalResolver.logger.warn(`${names}, so every call to them is refused. ${fix}`);
817
+ return;
818
+ }
819
+ throw new Error(
820
+ `${names}, so every call to them would be refused. ${fix} AKAN_UNGUARDED_ENDPOINTS=open answers them as before.`,
817
821
  );
818
822
  }
823
+
824
+ static #isLocalOperation() {
825
+ if (process.env.NODE_ENV === "test") return true;
826
+ const environment = process.env.AKAN_PUBLIC_ENV ?? "debug";
827
+ return (process.env.AKAN_PUBLIC_OPERATION_MODE ?? (environment === "local" ? "local" : "cloud")) === "local";
828
+ }
819
829
 
820
830
  static #warnUndefinedErrors(endpointMeta: { [key: string]: EndpointInfo }) {
821
831
  const declared = Object.entries(endpointMeta).flatMap(([key, endpointInfo]) =>