akanjs 3.1.1-rc.9 → 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.
- package/CHANGELOG.md +114 -0
- package/README.ko.md +22 -0
- package/README.md +22 -0
- package/base/baseEnv.ts +2 -1
- package/client/cookie.ts +33 -13
- package/client/csrLocation.ts +8 -0
- package/client/csrTypes.ts +3 -1
- package/client/index.ts +6 -1
- package/client/makePageProto.tsx +10 -1
- package/client/navigationGuards.ts +6 -6
- package/client/router.ts +70 -24
- package/common/IpBytes.ts +45 -0
- package/common/IpLiteral.ts +7 -46
- package/common/TrustedProxy.ts +58 -10
- package/common/akanNames.ts +169 -0
- package/common/clientAddress.ts +2 -1
- package/common/codeAgentProfile.ts +51 -4
- package/common/cookieJar.ts +18 -2
- package/common/index.ts +13 -1
- package/common/localeConfig.ts +30 -5
- package/common/localePath.ts +26 -0
- package/common/markdownSpans.ts +3 -1
- package/common/mcpExposure.ts +7 -7
- package/common/websocketContract.ts +50 -0
- package/constant/constantRegistry.ts +3 -3
- package/constant/index.ts +1 -0
- package/constant/inputParser.ts +10 -2
- package/constant/refNameClaims.ts +4 -4
- package/constant/requestValue.ts +47 -0
- package/constant/serialize.ts +7 -1
- package/dictionary/agent.dictionary.ts +2 -2
- package/dictionary/base.dictionary.ts +4 -0
- package/dictionary/dictInfo.ts +9 -9
- package/document/database.ts +1 -0
- package/document/documentQuery.ts +42 -6
- package/document/filterMeta.ts +56 -8
- package/document/into.ts +8 -0
- package/document/queryEvaluator.ts +13 -8
- package/fetch/agentTurn.ts +5 -3
- package/fetch/client/fetchClient.ts +28 -62
- package/fetch/client/httpClient.ts +1 -0
- package/fetch/client/sliceInitHandle.ts +18 -18
- package/fetch/index.ts +1 -0
- package/fetch/renderScope.ts +2 -2
- package/fetch/sessionCookie.ts +37 -0
- package/index.ts +14 -0
- package/package.json +13 -27
- package/server/akanApp.ts +108 -29
- package/server/akanOption.ts +28 -0
- package/server/akanServer.ts +23 -9
- package/server/assetHeaders.ts +20 -1
- package/server/cachePolicy.ts +18 -5
- package/server/contentEncoding.ts +12 -7
- package/server/devtools/depsSerializer.ts +2 -1
- package/server/devtools/devtoolsRouter.ts +3 -2
- package/server/devtools/signalSerializer.ts +5 -5
- package/server/di/diLifecycle.ts +15 -2
- package/server/di/utils.ts +5 -3
- package/server/hmr/clientReport.ts +75 -0
- package/server/hmr/clientScript.ts +39 -1
- package/server/hmr/wsHub.ts +7 -2
- package/server/imageOptimizer.ts +117 -22
- package/server/lifecycle/buildEnv.ts +11 -0
- package/server/lifecycle/loadBuildEnv.ts +3 -0
- package/server/logging/logControlSocket.ts +32 -14
- package/server/logging/logStreamRoute.ts +12 -8
- package/server/logging/rotatingLogWriter.ts +15 -1
- package/server/machine/machineToken.ts +4 -0
- package/server/mcp/McpAuth.ts +12 -4
- package/server/mcp/McpExecutionContext.ts +4 -0
- package/server/mcp/McpRouter.ts +25 -13
- package/server/mcp/PagePromptComposer.ts +4 -7
- package/server/oauth/OAuthClientIdMetadata.ts +29 -6
- package/server/oauth/OAuthRegistration.ts +9 -0
- package/server/ops/failureLimiter.ts +42 -0
- package/server/ops/opsCommand.ts +19 -7
- package/server/ops/opsRoute.ts +60 -9
- package/server/ops/opsToken.ts +5 -0
- package/server/ops/snapshotJobs.ts +3 -1
- package/server/ops/snapshotRestore.ts +23 -1
- package/server/ops/sqliteSnapshot.ts +98 -18
- package/server/ops/statusAccess.ts +37 -19
- package/server/proxy/hostBasePathWebProxy.ts +4 -7
- package/server/proxy/localeWebProxy.ts +36 -16
- package/server/resolver/database.resolver.ts +15 -11
- package/server/resolver/service.resolver.ts +17 -2
- package/server/resolver/signal.resolver.ts +59 -42
- package/server/routeElementComposer.tsx +42 -9
- package/server/routeShellGate.ts +42 -0
- package/server/routing/apiRouter.ts +64 -1
- package/server/routing/serverLimits.ts +151 -0
- package/server/routing/soloAppRoutes.ts +23 -12
- package/server/rscClient.tsx +5 -3
- package/server/rscScrollRestoration.ts +60 -9
- package/server/rscWorker.tsx +76 -24
- package/server/sitemap.ts +5 -7
- package/server/ssrFromRscRenderer.tsx +36 -4
- package/server/ssrTypes.ts +5 -2
- package/server/staticPath.ts +36 -1
- package/server/systemPageDocument.tsx +7 -4
- package/server/systemPages.tsx +14 -8
- package/server/tunnel/TunnelAgent.ts +19 -2
- package/server/tunnel/TunnelDataSocket.ts +4 -2
- package/server/tunnel/TunnelHttpStream.ts +7 -2
- package/server/tunnel/TunnelTcpStream.ts +6 -3
- package/server/tunnel/TunnelWebsocketStream.ts +9 -2
- package/server/tunnel/tunnelStream.ts +6 -0
- package/server/types.tsx +20 -2
- package/server/webRouter.ts +42 -22
- package/service/agent.service.ts +9 -7
- package/service/injectInfo.ts +2 -1
- package/service/liveSyncHub.ts +6 -0
- package/service/predefinedAdaptor/anthropicLlm.ts +13 -12
- package/service/predefinedAdaptor/database.adaptor.ts +25 -16
- package/service/predefinedAdaptor/llm.adaptor.ts +3 -0
- package/service/predefinedAdaptor/llmOverflow.ts +5 -1
- package/service/predefinedAdaptor/openaiDialect.ts +9 -8
- package/service/predefinedAdaptor/searchIndex.ts +21 -1
- package/service/predefinedAdaptor/sql/AfterCommitHooks.ts +1 -2
- package/service/predefinedAdaptor/sql/QueryCompiler.ts +28 -4
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +118 -22
- package/service/predefinedAdaptor/sql/TransactionScope.ts +22 -0
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +11 -1
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +8 -0
- package/service/predefinedAdaptor/sql/types.ts +12 -1
- package/service/predefinedAdaptor/storage.adaptor.ts +11 -10
- package/service/serve.ts +5 -5
- package/service/serviceModule.ts +28 -24
- package/service/types.ts +3 -0
- package/signal/SignalFailure.ts +5 -2
- package/signal/agent/AgentCatalogue.ts +3 -2
- package/signal/agentMeter.ts +81 -9
- package/signal/agentRelayPolicy.ts +53 -2
- package/signal/agentTurnStream.ts +12 -1
- package/signal/guard.ts +2 -2
- package/signal/mcp/McpDocument.ts +3 -3
- package/signal/mcp/McpUriTemplate.ts +5 -5
- package/signal/middleware.ts +15 -1
- package/signal/roomKey.ts +53 -0
- package/signal/signalContext.ts +100 -40
- package/signal/slice.ts +27 -22
- package/signal/trace.ts +17 -6
- package/store/action.ts +22 -50
- package/store/actionTag.ts +2 -0
- package/store/agent/ScreenReader.ts +36 -4
- package/store/agent/ScreenTarget.ts +1 -1
- package/store/agent/StoreCatalogue.ts +7 -1
- package/store/agent/StoreSurfaceSource.ts +5 -0
- package/store/agentic/AgentConfirm.ts +30 -0
- package/store/agentic/StStateBuilder.ts +2 -2
- package/store/agentic/StToolBuilder.ts +7 -2
- package/store/agentic/formFields.ts +1 -2
- package/store/agentic/index.ts +1 -0
- package/store/agentic/useFieldTool.ts +4 -3
- package/store/baseSt.ts +5 -1
- package/store/deprecatedNames.ts +6 -0
- package/store/draftStore.ts +2 -2
- package/store/formUploadOf.ts +6 -5
- package/store/index.ts +1 -2
- package/store/sliceKeys.ts +9 -32
- package/store/state.ts +1 -1
- package/store/stateBuilder.ts +1 -1
- package/store/store.ts +1 -1
- package/store/storeInstance.ts +12 -8
- package/store/storeRegistry.ts +14 -2
- package/types/base/baseEnv.d.ts +1 -1
- package/types/client/cookie.d.ts +7 -2
- package/types/client/csrLocation.d.ts +2 -0
- package/types/client/csrTypes.d.ts +3 -1
- package/types/client/index.d.ts +1 -1
- package/types/client/navigationGuards.d.ts +4 -4
- package/types/client/router.d.ts +6 -2
- package/types/common/IpBytes.d.ts +5 -0
- package/types/common/IpLiteral.d.ts +0 -2
- package/types/common/akanNames.d.ts +149 -0
- package/types/common/codeAgentProfile.d.ts +4 -1
- package/types/common/index.d.ts +3 -2
- package/types/common/localeConfig.d.ts +8 -0
- package/types/common/localePath.d.ts +13 -0
- package/types/common/websocketContract.d.ts +7 -0
- package/types/constant/constantRegistry.d.ts +1 -1
- package/types/constant/index.d.ts +1 -0
- package/types/constant/refNameClaims.d.ts +1 -1
- package/types/constant/requestValue.d.ts +5 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +3 -3
- package/types/document/database.d.ts +1 -0
- package/types/document/documentQuery.d.ts +3 -0
- package/types/document/filterMeta.d.ts +4 -0
- package/types/document/into.d.ts +11 -0
- package/types/fetch/client/sliceInitHandle.d.ts +1 -2
- package/types/fetch/index.d.ts +1 -0
- package/types/fetch/renderScope.d.ts +2 -2
- package/types/fetch/sessionCookie.d.ts +12 -0
- package/types/index.d.ts +14 -0
- package/types/server/akanApp.d.ts +1 -0
- package/types/server/akanOption.d.ts +6 -0
- package/types/server/assetHeaders.d.ts +5 -0
- package/types/server/cachePolicy.d.ts +2 -0
- package/types/server/devtools/devtoolsRouter.d.ts +1 -1
- package/types/server/di/utils.d.ts +2 -0
- package/types/server/hmr/clientReport.d.ts +16 -0
- package/types/server/imageOptimizer.d.ts +4 -1
- package/types/server/lifecycle/buildEnv.d.ts +2 -0
- package/types/server/lifecycle/loadBuildEnv.d.ts +1 -0
- package/types/server/logging/logStreamRoute.d.ts +1 -1
- package/types/server/logging/rotatingLogWriter.d.ts +3 -0
- package/types/server/machine/machineToken.d.ts +2 -0
- package/types/server/mcp/McpAuth.d.ts +2 -1
- package/types/server/mcp/McpExecutionContext.d.ts +1 -0
- package/types/server/oauth/OAuthRegistration.d.ts +2 -0
- package/types/server/ops/failureLimiter.d.ts +11 -0
- package/types/server/ops/opsRoute.d.ts +4 -2
- package/types/server/ops/opsToken.d.ts +2 -0
- package/types/server/ops/snapshotRestore.d.ts +3 -1
- package/types/server/ops/sqliteSnapshot.d.ts +7 -1
- package/types/server/ops/statusAccess.d.ts +8 -4
- package/types/server/proxy/hostBasePathWebProxy.d.ts +1 -0
- package/types/server/resolver/signal.resolver.d.ts +0 -1
- package/types/server/routeElementComposer.d.ts +10 -4
- package/types/server/routeShellGate.d.ts +12 -0
- package/types/server/routing/apiRouter.d.ts +6 -1
- package/types/server/routing/serverLimits.d.ts +23 -0
- package/types/server/routing/soloAppRoutes.d.ts +1 -1
- package/types/server/rscScrollRestoration.d.ts +1 -0
- package/types/server/ssrTypes.d.ts +5 -2
- package/types/server/staticPath.d.ts +1 -0
- package/types/server/systemPageDocument.d.ts +1 -0
- package/types/server/tunnel/TunnelAgent.d.ts +6 -0
- package/types/server/tunnel/TunnelDataSocket.d.ts +2 -0
- package/types/server/tunnel/TunnelTcpStream.d.ts +1 -1
- package/types/server/tunnel/tunnelStream.d.ts +1 -0
- package/types/server/types.d.ts +11 -2
- package/types/server/webRouter.d.ts +1 -1
- package/types/service/agent.service.d.ts +1 -1
- package/types/service/liveSyncHub.d.ts +1 -0
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +1 -0
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +1 -1
- package/types/service/predefinedAdaptor/searchIndex.d.ts +5 -1
- package/types/service/predefinedAdaptor/sql/AfterCommitHooks.d.ts +1 -2
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +2 -1
- package/types/service/predefinedAdaptor/sql/TransactionScope.d.ts +6 -0
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +12 -1
- package/types/service/serve.d.ts +1 -1
- package/types/service/types.d.ts +3 -0
- package/types/signal/SignalFailure.d.ts +1 -1
- package/types/signal/agentMeter.d.ts +4 -1
- package/types/signal/agentRelayPolicy.d.ts +12 -0
- package/types/signal/guard.d.ts +1 -1
- package/types/signal/roomKey.d.ts +8 -0
- package/types/signal/signalContext.d.ts +20 -6
- package/types/signal/slice.d.ts +1 -0
- package/types/signal/trace.d.ts +4 -0
- package/types/store/actionTag.d.ts +2 -0
- package/types/store/agent/ScreenReader.d.ts +2 -1
- package/types/store/agent/StoreCatalogue.d.ts +5 -0
- package/types/store/agentic/AgentConfirm.d.ts +15 -0
- package/types/store/agentic/StToolBuilder.d.ts +3 -1
- package/types/store/agentic/index.d.ts +1 -0
- package/types/store/baseSt.d.ts +12 -12
- package/types/store/deprecatedNames.d.ts +5 -0
- package/types/store/index.d.ts +1 -2
- package/types/store/sliceKeys.d.ts +12 -2
- package/types/ui/Agent/agentSessionOf.d.ts +2 -0
- package/types/ui/Agent/useAccountBoundSession.d.ts +2 -0
- package/types/ui/Agent/useScreenActions.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +2 -1
- package/types/ui/Data/ListContainer.d.ts +11 -1
- package/types/ui/Data/UnlistedEditModal.d.ts +9 -0
- package/types/ui/Data/ViewActions.d.ts +12 -0
- package/types/ui/Data/useViewModal.d.ts +6 -0
- package/types/ui/Icon.d.ts +15 -0
- package/types/ui/Model/ViewModal.d.ts +3 -1
- package/types/ui/System/Common.d.ts +2 -2
- package/types/ui/animated.d.ts +3 -3
- package/types/ui/sliceNamesOf.d.ts +4 -4
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +15 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +6 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/host.d.ts +3 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +7 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +8 -4
- package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +2 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +10 -1
- package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +18 -8
- package/types/vendor/use-agentic/AgentSession.d.ts +6 -1
- package/types/vendor/use-agentic/ToolRunner.d.ts +7 -2
- package/types/vendor/use-agentic/types.d.ts +2 -0
- package/types/vendor/use-agentic/useAgentTool.d.ts +2 -1
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/types/webkit/useNavigationGuard.d.ts +7 -0
- package/types/webkit/usePageLocation.d.ts +3 -4
- package/ui/Agent/Chat.tsx +2 -0
- package/ui/Agent/Zone.tsx +2 -0
- package/ui/Agent/agentSessionOf.ts +54 -5
- package/ui/Agent/useAccountBoundSession.ts +26 -0
- package/ui/Agent/useScreenActions.ts +1 -1
- package/ui/BottomSheet.tsx +1 -1
- package/ui/Button.tsx +5 -5
- package/ui/Data/Item.tsx +43 -30
- package/ui/Data/ListContainer.tsx +44 -6
- package/ui/Data/QueryMaker.tsx +3 -2
- package/ui/Data/RefPicker.tsx +3 -2
- package/ui/Data/UnlistedEditModal.tsx +32 -0
- package/ui/Data/ViewActions.tsx +57 -0
- package/ui/Data/dataExport.ts +3 -0
- package/ui/Data/useViewModal.ts +76 -0
- package/ui/Dialog/LegacyModal.tsx +1 -1
- package/ui/Dialog/Provider.tsx +8 -2
- package/ui/DragAction.tsx +1 -1
- package/ui/Dropdown.tsx +2 -2
- package/ui/Field/lightRefCache.ts +2 -2
- package/ui/Field/useRelationOptions.ts +3 -10
- package/ui/Icon.tsx +87 -0
- package/ui/KeyboardAvoiding.tsx +1 -1
- package/ui/Layout/Sider.tsx +3 -3
- package/ui/Link/Anchor.tsx +4 -1
- package/ui/Load/Edit.tsx +2 -2
- package/ui/Load/Edit_Client.tsx +3 -3
- package/ui/Load/Pagination.tsx +3 -3
- package/ui/Load/View.tsx +15 -14
- package/ui/Loading/ProgressBar.tsx +1 -1
- package/ui/Model/DraftBar.tsx +7 -14
- package/ui/Model/EditModal.tsx +9 -40
- package/ui/Model/EditWrapper.tsx +3 -5
- package/ui/Model/NewWrapper_Client.tsx +3 -7
- package/ui/Model/Remove.tsx +2 -2
- package/ui/Model/RemoveWrapper.tsx +2 -4
- package/ui/Model/SureToRemove.tsx +2 -2
- package/ui/Model/ViewEditModal.tsx +9 -20
- package/ui/Model/ViewModal.tsx +14 -20
- package/ui/Model/ViewWrapper.tsx +4 -6
- package/ui/ScreenNavigator.tsx +2 -2
- package/ui/Signal/WebSocket.tsx +2 -2
- package/ui/System/CSR.tsx +4 -4
- package/ui/System/Client.tsx +4 -2
- package/ui/System/Common.tsx +2 -2
- package/ui/System/Messages.tsx +4 -2
- package/ui/System/Reconnect.tsx +5 -6
- package/ui/System/SSR.tsx +7 -5
- package/ui/System/SelectLanguage.tsx +1 -1
- package/ui/System/ThemeToggle.tsx +1 -1
- package/ui/Tab/Provider.tsx +1 -0
- package/ui/Toast.tsx +7 -15
- package/ui/index.ts +0 -1
- package/ui/sliceNamesOf.ts +25 -15
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +2 -1
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +10 -2
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +4 -0
- package/vendor/@akanjs/native/native/desktop/Cargo.toml +1 -1
- package/vendor/@akanjs/native/native/desktop/src/acl.rs +166 -0
- package/vendor/@akanjs/native/native/desktop/src/activity.rs +57 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +48 -10
- package/vendor/@akanjs/native/native/desktop/src/navigation.rs +22 -10
- package/vendor/@akanjs/native/native/desktop/src/routes.rs +2 -2
- package/vendor/@akanjs/native/native/desktop/src/vectors.rs +28 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +1 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +10 -2
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +4 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +1 -0
- package/vendor/@akanjs/native/packages/cli/src/config.ts +15 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +3 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +5 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +4 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +3 -4
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +49 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +6 -5
- package/vendor/@akanjs/native/packages/core/src/kernel.ts +18 -2
- package/vendor/@akanjs/native/packages/core/vectors/navigation.json +22 -1
- package/vendor/@akanjs/native/packages/core/vectors/routes.json +4 -0
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +6 -1
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +5 -1
- package/vendor/@akanjs/native/packages/desktop/src/paths.ts +24 -1
- package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +19 -8
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +2 -0
- package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +4 -5
- package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +6 -8
- package/vendor/@akanjs/native/plugins/filesystem/src/desktop.ts +4 -2
- package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +22 -3
- package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +6 -0
- package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +6 -0
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +444 -221
- package/vendor/@akanjs/native/plugins/updates/src/index.ts +18 -8
- package/vendor/use-agentic/AgentSession.ts +7 -2
- package/vendor/use-agentic/ToolRunner.ts +44 -5
- package/vendor/use-agentic/types.ts +2 -0
- package/vendor/use-agentic/useAgentTool.ts +4 -2
- package/webkit/McpAppAgent.ts +2 -1
- package/webkit/bootCsr.tsx +1 -1
- package/webkit/index.ts +1 -0
- package/webkit/nativeNavigation.ts +2 -10
- package/webkit/useCsrValues.ts +5 -1
- package/webkit/useNavigationGuard.ts +21 -0
- package/webkit/usePageLocation.ts +11 -5
- package/webkit/usePageTool.tsx +1 -0
- package/store/databaseStateNames.ts +0 -24
- package/store/formSetterNames.ts +0 -18
- package/types/store/databaseStateNames.d.ts +0 -18
- package/types/store/formSetterNames.d.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,119 @@
|
|
|
1
1
|
# akanjs
|
|
2
2
|
|
|
3
|
+
## 3.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 39d5902: Akan.js 3.1.1. Read **Upgrade notes** before deploying an app that runs 3.1.0: sessions, proxies, the status routes and a few boot checks change, and `akan lint` gains gates a workspace adopts with a baseline.
|
|
8
|
+
|
|
9
|
+
**Security**
|
|
10
|
+
|
|
11
|
+
- **Build files.** In 3.1.0, `/_akan/styles/` and `/_akan/fonts/` resolved an encoded `..` against the whole build folder, so a request such as `/_akan/fonts/..%2fserver%2fpages-<hash>.js` could read any file in it, the server bundle and `base-artifact.json` included, through the gateway too. They now serve only from the build's `styles/` and `fonts/` folders. Upgrade any internet-facing 3.1.0 deployment.
|
|
12
|
+
- **Session cookie.** Outside local the session cookie is `__Host-jwt:<appName>`, a name a browser accepts only from the app's own host over HTTPS at `Path=/`, so a sibling subdomain or a parent domain can no longer plant one. A request that carries the session cookie more than once reads as signed out — on the server, in SSR fetches, on websockets and in the browser — and an `Authorization: Bearer` header still wins. The server expires the duplicated copies of an unprefixed session cookie it can name (a local server's `jwt:<appName>`, or a name a library adds with `SessionCookies.watch(name)` from `akanjs/common`) and logs the name, the count and the host, never a value. A cookie name is trimmed of space and tab only, as RFC 6265 does, so a name padded with other whitespace never reads as a `__Host-` cookie. Outside local, `libs/shared` moves its SSO state, sign-up token and refresh cookies to `__Host-` names too, and reads a refresh cookie sent twice as absent.
|
|
13
|
+
- **SSO origin.** `libs/shared` no longer builds the provider's `redirect_uri` from a cookie the sign-in page set. The server takes the origin from the request, trusting `x-forwarded-host` and `x-forwarded-proto` only from a trusted hop, and outside local refuses a host that is not the app's own with `shared.error.invalidSsoOrigin`. The native SSO start uses the same origin and check.
|
|
14
|
+
- **Status routes.** Outside local, `/_akan/app/health` answers an anonymous caller only `{ status }` (`running`, or `stopping` while it drains) with 200, and `/_akan/app/metrics` answers 401. `Authorization: Bearer <AKAN_STATUS_TOKEN>` unlocks both in full; `AKAN_LOG_STREAM_TOKEN` works too, and a token containing whitespace unlocks nothing. Both answers are `cache-control: no-store`; `/_akan/app/info` and `/_akan/bench/ping` stay public. Detail stays open wherever `AKAN_PUBLIC_ENV` is `local`, and `akan build` bakes in the workspace's own value, so never expose an image built with `local`.
|
|
15
|
+
- **Client address.** `AKAN_TRUSTED_PROXIES` names the proxies whose forwarded headers count. Unset, it is loopback and the private and link-local ranges, as before, and the caller is `X-Real-IP`, else the leftmost `X-Forwarded-For` entry. Set to a list of addresses or CIDRs, `private`, or `none` (loopback alone), it replaces those ranges: `X-Forwarded-For` is walked from the right past every trusted hop, and `X-Real-IP` counts only when `X-Forwarded-For` is absent or its nearest entry is no address. `*` alone trusts every peer; beside other entries it is ignored with a warning, as 3.1.0 ignored it. A forwarded value counts only when it is an IP address, and `Ip`, `context.getClientIp()` and the websocket address report it in one spelling, with no port or IPv6 zone and an IPv4-mapped IPv6 address as IPv4: `IpLiteral.parse(text)` from `akanjs/common` gives an address your code stores or compares the same spelling. The gateway and a solo server take `X-Forwarded-Host`, `-Proto` and `-Port` only from a trusted proxy. We recommend listing your load balancer's and ingress controller's own addresses: `private` lets any peer at a private address, such as a pod or a VPN user, name any caller, so pair it with a NetworkPolicy that admits only the ingress.
|
|
16
|
+
- **In-page agent requests.** The browser writes a `runAgentTurn` request's instructions, tools and transcript, so a caller past `setAgentAccess` could send a hand-built turn and use the app's LLM key as a plain LLM. `option.setAgentRequestPolicy((request, { account, ip }) => request)` sees each turn after the access guard and before the quota hook and the model, with tool names as the page published them, and returns the request to send — a server-fixed lead to the instructions, a tool allowlist, caps, a per-turn `model` — or throws an `Err` to refuse it (`agent.error.requestRefused` is new) before any stream opens. Compaction turns pass through it too, with no tools and the summarizer's instructions. Attachments, references and tool results are folded into the prompt after the policy runs, so a size cap counts them itself. Without a policy nothing changes.
|
|
17
|
+
- **CLI sign-in.** `akan login` prints a code such as `BCDF-GHJK` beside the sign-in URL: type it on the sign-in page to approve, and never type a code that did not come from your own terminal. The URL and its QR code carry only a login id, `RemoteAuthCode.loginIdOf(deviceCode)` from `akanjs/common`; the device code the CLI polls with is never in the link, so neither the code nor the session can be taken from it. A cloud serving `/remoteAuth` itself asks for the code: it accepts a poll only by a device code in the UUID form the CLI mints, registers a short-lived pending login under `loginIdOf` it, rate-limits that anonymous poll, approves only when `RemoteAuthCode.matches(deviceCode, typed)` holds, expires the login after three wrong codes, and hands the session out once. A cloud that does not still signs the CLI in, without the code.
|
|
18
|
+
|
|
19
|
+
**Upgrade notes**
|
|
20
|
+
|
|
21
|
+
- **Sessions.** Every visitor of a server-rendered page signs in once more; a CSR bundle keeps the token it stored until its `libs/shared` refresh fails. Native apps keep `jwt:<appName>` in secure storage and stay signed in. The `jwt:<appName>` and bare `jwt` cookies and the bare `jwt` secret-storage key are no longer read, and sign-in, sign-out and `initAuth` delete them; code that reads the cookie by a hard-coded name calls `authTokenKey()`. The app shell no longer signs a visitor in from a `?jwt=` query parameter, and `initAuth` no longer writes back a cookie it only read. `cookies()` from `akanjs/fetch` and `getCookie` / `cookies()` from `akanjs/client` read a name sent more than once as unset, percent-decode values, and no longer unwrap `j:` JSON values; the new `cookieJar()` in both returns every occurrence, with `read(name)`, `all(name)` and `readJson(name)`. `setCookie` escapes whitespace, control characters, `;`, `,`, `"`, `\` and a lone `%`, so `getCookie` reads the value back; a `%XX` its caller encoded is written unchanged. `removeCookie` writes `Secure` when it deletes a `__Host-` or `__Secure-` cookie. Deprecated: `parseCookieHeader`, `readAuthToken` (use `readSessionToken(CookieJar.parse(header))`), `cookieHeaderHasAuthToken` and `AppWsData.cookies` (use `jar`).
|
|
22
|
+
- **SSO hosts.** Outside local, SSO accepts only the app's own hosts: the `routes` domains in `akan.config.ts`, `AKAN_SUB_ROUTE_HOSTS`, `<app>-<environment>.<serveDomain>`, the `hostname` option or `HOST_NAME` (`BackendEnv.hostname` now also takes a list, such as the apex and `www`, whose first name is the one links, the issuer and mail use), `AKAN_ALLOWED_HOSTS` (which, once set, also refuses every host it leaves out) and `AppHosts.declare(hosts)` from `akanjs/common`. Declare every host users sign in on before upgrading. A build with `web: false` records no `routes` domains, so list its hosts in `AKAN_ALLOWED_HOSTS`. A sign-in or sign-up left unfinished across the upgrade starts again, and a web session cannot be refreshed past it. `getSsoOrigin`, `NativeSso.publicOrigin` and `shared.error.invalidSsoCallbackMissingOrigin` are removed, and `readRefreshTokenCookie` takes the request.
|
|
23
|
+
- **Proxies.** A proxy at a public address that terminates TLS or rewrites `Host`, such as a CDN, must be in `AKAN_TRUSTED_PROXIES` even where you never set it: otherwise the app reads `http` and the inner host, so MCP OAuth metadata, the SSO `redirect_uri` and absolute URLs come out wrong and browser mutations are refused as cross-site. If you already set the list, a private-range ingress it does not name is no longer trusted (add it), and the caller now comes from the right-hand walk of `X-Forwarded-For`, not `X-Real-IP`, so your proxy must set or append `X-Forwarded-For`.
|
|
24
|
+
- **Status routes.** A metrics scraper or monitor that reads the health or metrics body sends `Authorization: Bearer <AKAN_STATUS_TOKEN>`. Probes that read only the status code — the chart's probes, the desktop shell, `akan` commands — need nothing.
|
|
25
|
+
- **Writes during a render.** `fetch.<mutation>()` or a socket `message` sent while a page renders now throws `base.error.mutationInRender` as the call is made, before anything is sent, so a `.catch()` on its promise does not swallow it. On the server that covers a route's `.render()`, `.guard()`, `.head()` and `.loading()`, a layout's `.notFound()` and `.error()`, any component rendered inside them, and the page behind a `.prompt()`; in a CSR bundle, a route stage's synchronous part. Move the write to a user action, or pass `{ duringRender: true }` as the call's option (`fetch.prepareSession(id, { duringRender: true })`) only for a write harmless to repeat and safe for anyone following a link to trigger; an OAuth callback page that completes a connection needs it and must check that its `state` was issued to the signed-in user. `.app()` loads still refuse every mutation. `akan quality` reports these calls as `akan.ssr.render-mutation`, reading a route file's stages and the functions it calls in that file.
|
|
26
|
+
- **Boot checks.** An app now fails to boot where 3.1.0 served one of two declarations and dropped the other; rename one side of each pair the error names.
|
|
27
|
+
- One refName with two shapes: scalar fields, enum values, or a model declared under a lib's refName without building on the lib's model (extending through `lib/__lib/lib.constant.ts` stays legal). A field's `secret`, `hidden` or `visual` kind is part of its shape. Likely hits: an app enum or scalar sharing a `libs/shared` name with other values, and an app model that leaves out one of the lib model's layers. Libs disabled with `disableLibs` are not checked, and a lib's own `agentTurn` scalar with other fields is refused, naming `akanjs` as the other owner. When a lib and the app both declare a refName, the app's declaration now wins in every registry.
|
|
28
|
+
- One `fetch.*` name claimed twice (`[DI:fetch]`): a mounted generated endpoint, a slice's `xListInY` / `initXInY`, or a custom query, mutation, pubsub or message; and a pubsub and a message sharing a name (`[DI:socket]`). A native or desktop build already shipped still calls the old name.
|
|
29
|
+
- Two internal jobs of one kind under one name (`[DI:internal]`): two `initialize`, two `destroy`, two `process`, or two of `cron`, `interval` and `timeout`. Renaming a `process` job moves it to a new queue, so drain the old one first; renaming a cron or interval starts a new lease.
|
|
30
|
+
- **Service methods.** `transaction` and `afterCommit` are now methods of every service: rename a service method of either name. A custom `DatabaseAdaptor` implements `afterCommit(fn)`.
|
|
31
|
+
- **WebSocket on first use.** The socket opens at a page's first subscribe or message instead of at load. An app that relied on it being open at load — reading `fetch.ws.connected`, listening for `"connect"`, or expecting a push before it subscribes — adds `.wsConnect(true)` to its root layout.
|
|
32
|
+
- **Dictionaries.** `System.Provider` no longer takes a `dictionary` prop: remove it from a hand-written root layout. `Translator.withDefaultLocale` is removed; the build fills a locale's missing keys from the default locale.
|
|
33
|
+
- **MCP.** An app slice that writes no `mcp` map now inherits every verb its lib's slice took off the shelf, root `mcp: false` included. An app extending a `libs/shared` model whose slice says `mcp: { cru: false }` (such as `admin` or `notification`) stops publishing those generated writes on `/mcp`; write `mcp: { cru: true }` on the app's slice to publish them again. HTTP is unchanged.
|
|
34
|
+
- **Slices on the client.** An app's slice that extends a lib's replaces the lib's guards, `mcp`, person-only and `crud` maps whole on the client too, as the server mounts them. A verb the app sets to `None` no longer gets a client handler, so `fetch.createX` for it is undefined instead of answering 403.
|
|
35
|
+
- **Library sort keys.** An app filter built from a library's filter carries the library's sort keys in its type, so an app dictionary that does not spread the library's dictionaries into `modelDictionary(...)` translates them in `.sort()`.
|
|
36
|
+
- **Pubsub `null`.** Publishing `null` or `undefined` to a pubsub that is not `{ nullable: true }` throws an error naming the pubsub.
|
|
37
|
+
- **Compression.** HTML, RSC and other text responses compressed as they stream carry no `Content-Length`, and a strong `ETag` on one becomes weak. `AKAN_HTTP_COMPRESS=0` still turns compression off.
|
|
38
|
+
- **Desktop updates.** Install matching `akanjs` and `@akanjs/cli` (native build API 0.13.0). A self-hosted feed must serve `<os>-<arch>/<channel>/app.json`. Renaming a native target cuts its installed desktop apps off updates. Before you revoke a key or sign with a spare, publish a release signed with the first key, so apps built with 3.1.0 move to a 3.1.1 binary. `native.updates.publicKeys` refuses a non-canonical or small-order Ed25519 key, which would verify signatures nobody made: replace it with one `akan update-keygen` prints. A desktop app now updates itself by default; set `native.updates.automatic: false` to keep checking, downloading and applying in the app's own `updates` calls.
|
|
39
|
+
- **`@akanjs/devkit`.** `CloudApi.getRemoteAuthToken` and `CloudApi.getRemoteSelf` throw on a failed request and give up after 20 seconds; `null` now means only "not approved yet" and "no sign-in from this session". `uploadRelease`, an unused helper importable only as `@akanjs/devkit/uploadRelease`, is removed. The native build types come from `akanjs/native/api` and `akanjs/native/config`, not the unpublished `@akanjs/native/api`.
|
|
40
|
+
- **Workspaces.** An existing workspace adds `@akanjs/cli` at its `akanjs` version (`bun add -d @akanjs/cli@<version>`), so `bunx akan` runs Akan's CLI and not the unrelated npm package `akan`. A `local/` folder an earlier version left inside an app or library is no longer written; delete it.
|
|
41
|
+
- **Lint gates.** `akan lint` now fails in an app or library that already breaks the size and write rules under **New**, until `akan quality baseline` records what is there: run it once and commit the `akan.baseline.json` files it writes. A CI job that runs `akan lint` needs the default branch's history (`actions/checkout` with `fetch-depth: 0`); in a shallow clone the gate fails rather than pass unchecked. A literal list limit above 500 (`no-oversized-list-limit`) is a lint error the baseline does not record, so lower it. `akan typecheck` now also checks an app's `script/` files, which nothing imports.
|
|
42
|
+
- **Agent guide.** Run `akan agent install` and commit what it writes: `AGENTS.md`, the scoped `AGENTS.md` files, `.claude/rules/akan/`, `.claude/skills/akan-*`, `.agents/skills/akan-*` and `.cursor/rules/akan-*.mdc`. Your `CLAUDE.md` and `.cursor/rules/akan.mdc` pointers are kept. Nothing reads `docs/AI-DEVELOPMENT.md` or `docs/GENERATED.md` anymore, so a workspace created before this release can delete them; `akan doctor` reports them.
|
|
43
|
+
|
|
44
|
+
**New**
|
|
45
|
+
|
|
46
|
+
- **Navigation guards.** `router.addNavigationGuard(guard)` keeps a page with unsaved work. `guard({ kind, from, to })` is asked before every in-app move — `push`, `replace`, `back`, a `Link`, a deep link, the browser's back and forward, a native back button or swipe, and the in-page agent's `navigate` and `goBack` — and returns the function that removes it. `useNavigationGuard(guard)` from `akanjs/webkit` registers one for the page that is on screen. Answering `false`, throwing or rejecting keeps the page, and `router.navigation()`, which now also follows `back()`, rejects with `NavigationRefused`. A refused browser back or forward is undone before the page redraws, and an async guard that later allows it replays the move. Guards answer one at a time, and a move started while one decides is refused. Redirects, `notFound()`, `setLang` and moves off the origin are not asked; a closing tab is `beforeunload`'s.
|
|
47
|
+
- **Transactions.** Every service has `this.transaction(fn)` and `this.afterCommit(fn)`. `transaction` runs `fn` as one database transaction over every model and returns its value, typed; when `fn` throws it rolls back and rethrows that error. A transaction opened inside another joins it with no savepoint, so what the inner one wrote before throwing still commits if the outer catches the error. On Postgres a failed statement aborts the whole transaction even when caught, so `transaction` rolls back and rethrows it. On SQLite it holds the database's only write lock, so keep `fn` short and never wait on the network inside it. `afterCommit(fn)` runs `fn` after the commit and before `transaction` resolves, never after a rollback, and at once outside a transaction; a callback that throws is logged, not thrown. Cache, `memory`, outside calls and `listenPost` listeners, live sync included, are not part of the transaction: the listeners run after the commit. A task the transaction started without awaiting never joins it once it has ended. `@Transaction()` from `akanjs/server` is deprecated in favour of `this.transaction`.
|
|
48
|
+
- **Writes that keep `updatedAt`.** `{ touch: false }` leaves `updatedAt` as stored, so bookkeeping such as a lease heartbeat or a mirrored snapshot no longer reads as an edit in `updatedAt` periods and sorts. Query-level writes take it in their options (`updateOne`, `updateMany`, `updateById`) or as `.set(update, { touch: false })` on the generated `update<Filter>` / `updateOne<Filter>`, and run no hooks, as before. The document path takes it as `doc.save({ touch: false })`, `pickAndWrite(id, data, { touch: false })` and `pickOneAndWrite(query, data, { touch: false })`, with save hooks and live sync running as usual.
|
|
49
|
+
- **Conditional `upsertById`.** `upsertById(data, { where })` writes over a stored row only when that row matches `where`, and never over a removed row unless `withRemoved: true` is passed too; an id no row holds is inserted. It is one statement on SQLite and Postgres, so a writer whose lease lapsed cannot land over a newer row, or bring back a removed one, between its check and its write. With `where` it answers `updateOne`'s result: `modifiedCount` is 0 when the stored row did not match, and `upsertedId` is set when it inserted. `where: {}` writes over any live row and refuses a removed one. Without `where`, `upsertById` writes as before.
|
|
50
|
+
- **Env transform.** `option.transformEnv(fn)` in a lib's or app's `lib/option.ts` rewrites the server env once, as the server is built and before any option, lib or `env(...)` injection reads it. It runs in every replica, in `bun main.js ops …` and in the test server; each lib's transform runs in mount order and the app's last, each given what the one before returned. `fn` gets a copy, so code that imports the env module itself still reads its values.
|
|
51
|
+
- **MCP curation and tests.** `option.setMcp()` takes `excludeSignals` and `excludeEndpoints`, which keep a whole signal or single entries, generated ones included, off `/mcp`'s `tools/list`, `tools/call` and views while HTTP still serves them. The lists from every lib and the app add up. The boot log reports each exclusion and warns about a name that matches nothing served. `akanjs/test` exports `getServedMcpCatalogue({ as, ui })`, the tools and prompts the running test server's `/mcp` lists a guest or a test agent's account across every page, with `findTool`, `undescribed` and `listingCost`; `ui: true` gives the listing of a client that draws MCP Apps views. `TestServer#servedMcpCatalogue` does the same for a server you start.
|
|
52
|
+
- **In-page agent.** The agent is told that `navigate` and `goBack` replace the screen's tools, which arrive on its next turn, so a navigation ends the turn. An unknown tool name is answered with the tools listed now, noting when the screen changed under the call. `navigate` reports where a redirect or the not-found page actually took it, and answers a path with no route with the paths the screen links to. `OpenaiLlm` with `host` set to `https://api.openai.com/v1` keeps image input, as with no host, and `OpenaiLlm.acceptsOf(llmOption)` lets an app's own chat-completions adaptor claim the same.
|
|
53
|
+
- **Faster pages.** Server-rendered HTML and RSC flight responses are compressed with brotli, or gzip for a client that takes only gzip, as they stream; the shell still reaches the browser before the render ends, and a disconnect still cancels the render. The akanjs.com home page drops from 444 KB to about 48 KB. Behind the gateway each replica compresses for the client's `Accept-Encoding`, and the gateway relays its bytes. Text responses from the web routes — `public/` files without a `.br`/`.gz` sidecar, `robots.txt`, `sitemap.xml`, the CSR shell — are compressed the same way, and a page in the HTML result cache keeps its compressed bytes, which `AKAN_HTML_RESULT_CACHE_MAX_BYTES` now counts. Every compressible response varies on `Accept-Encoding`, compressed or not. A page no longer carries its locale's dictionary in the RSC flight: a build writes one immutable module per locale, `/_akan/dict/<lang>-<hash>.js`, which the page preloads and a navigation into another locale loads before it commits. `akanjs/common` and `tailwind-merge` load once from the import map, recipes build on `tailwind-variants/lite`, and a server-rendered page no longer loads react-spring or the native plugins. A font in `rootLayout().fonts([...])` takes `preload: false`, loading when text first uses it. A `public/` file is served with `stale-while-revalidate=86400` beside its 5-minute `max-age`.
|
|
54
|
+
- **UI exports.** `akanjs/ui` exports `SelectLanguage`, `ThemeToggle` and `DevModeToggle` by name; import them that way in a `"use client"` file, where `System.ThemeToggle` bundles the whole app shell. `akanjs/client` exports `isNativeApp`, `isNativeShell`, `nativePlatform`, `desktopPlatform` and `openExternalUrl`. `fetch.instance.connectOnUse()` is the socket's new default mode.
|
|
55
|
+
- **View modal in the URL.** `Data.ListContainer` and `Model.AdminPanel` take `viewInUrl`, which keeps the open view modal in the URL as `?action=view&modelId=<id>` beside the page's other params: a link carrying them opens that record whether or not the loaded page lists it, a link to another record replaces the open one, and an id that fails to load closes the modal and drops them. They also take `renderViewTitle`, the view's own title (`false` drops its title bar). The view's bottom bar carries the record's `actions` other than "view", with labels: "edit" opens the edit form and goes back to the view when it closes or saves, "remove" asks the row's confirm and then closes the view, and a function form is asked with the record and its place on the loaded page (-1 when unlisted); `viewActions={false}` turns the bar off. The container draws one view modal, not one per row. `Model.ViewModal` draws a title bar only when given `renderTitle` and an action bar only when `renderAction` answers something other than `null`, and without an `id` opens for whichever record `view<Model>` loads. `Data.Item` cards are more compact.
|
|
56
|
+
- **Desktop updates, protocol v2.** Desktop apps built with 3.1.1 read `<url>/<os>-<arch>/<channel>/app.json`, the signed envelope phones read: they trust every key of `native.updates.publicKeys` except `revokedKeys`, apply the revocations a release carries, and take only releases made for their own native target. `akan publish-update --revoke` and `--reset-sequence` work for desktop releases. For apps built with 3.1.0, `publish-update` still writes `<channel>.json` and `.sig` when the first key signs, and warns when a release is invisible to them.
|
|
57
|
+
- **Desktop apps update themselves.** About 30 seconds after it starts and every hour after that, a desktop app looks for a release on its channel and downloads it in the background. As the app quits, the download moves into place without a relaunch, and the next launch runs it on trial, so a release that fails still rolls back. A quit while a release is on trial installs nothing, a debug build never updates itself, and on Windows and Linux only one running copy of an app without single-instance updates itself. `native.updates.automatic: false`, or a target's `updates.automatic`, leaves it all to the app's own `updates` calls. `updates.download()` called while a download runs, the background one included, now answers with that download instead of rejecting with `CANCELLED`.
|
|
58
|
+
- **`akan push-update --platform desktop`** (or `macos`, `windows`, `linux`, with `--arch arm64|x64`) uploads to the Akan Cloud project the release `publish-update` last signed, unchanged: the envelope, the archive and the patches it names, the old pair when it is the same release, and a `release.json` naming the CLI version, the build time and the app's signer (the macOS Team ID, the Windows Authenticode subject, or none). The envelope must verify against the target's keys. The command prints whether the project published, staged or rejected the release, and the URL installed apps read. A desktop release can be up to 1 GiB.
|
|
59
|
+
- **Desktop secrets.** A desktop app's carried server keeps a secret the user gives it, such as their own API key, with `NativeSecret.set(key, value)`, `get(key)` and `remove(key)` from `akanjs/server`, which ask the shell that started it. The shell keeps them encrypted in the server's data folder, with the key in the macOS Keychain, the Windows Credential Manager or the Linux Secret Service, under a store of the server's own (a debug build has its own), apart from the page's `secretStorage`, so no page script reads them. On macOS without a Team ID, any program of the same user can read that key through `security`. A read during the server's boot waits for the window, so read a secret when a request needs it. Anywhere but a carried server, `NativeSecret.available` is false and every call rejects without sending anything.
|
|
60
|
+
- **`akan test <target> [files]`** runs only the comma-separated test files or folders you name, read from the target or the workspace root; it is the way to run one signal test. Outside production, a server or test started inside `apps/<app>` or `libs/<lib>` without `AKAN_RUNTIME_DIR` keeps its sockets and logs in the workspace root's `local/<apps|libs>/<name>/runtime`, where `akan test` and `akan start` put them, and leaves a live dev server's log socket alone.
|
|
61
|
+
- **Locked images.** `akan build` writes a `bun.lock` beside the image's `package.json`, cut down from the workspace's lockfile to what the server needs with every locked version and the root's registry `overrides` and `resolutions` kept, and the generated Dockerfile installs it with `bun install --production --frozen-lockfile`. A workspace without a text `bun.lock` gets a warning and the unlocked install as before; a hand-written `docker` string adds `--frozen-lockfile` itself, and `patchedDependencies` and a pin to a path or a workspace package are not carried.
|
|
62
|
+
- **`akan create-workspace`** puts `@akanjs/cli` in the new workspace's `devDependencies` at its own version, and `akan update` moves it with `akanjs` and `@akanjs/devkit`. `--dir` is resolved against the current directory, so `--dir /abs/path` creates `/abs/path/<name>`.
|
|
63
|
+
- **`akan login`** saves the session as soon as the cloud hands it out and reads the account with retries, so a failed read no longer loses it. Network errors are retried with backoff and reported, a 4xx refusal other than 408 or 429 ends the wait with the cloud's reason, and a session the cloud does not take is cleared with a message to sign in again.
|
|
64
|
+
- **Version mismatch.** When `akan`'s version differs from the workspace's `akanjs`, the warning names a command that fixes it: the same command through the matching CLI (`bunx -p @akanjs/cli@<akanjs version> akan …`, or `bunx akan …` when the workspace pins that CLI), plus `akan update` when the CLI is the newer one. It no longer prints for `akan update`, `akan create-workspace`, `--help` or `--version`.
|
|
65
|
+
- **`akan verify [targets]`** syncs, lints with fixes, typechecks, fast-builds and checks SSR over what git reports as changed, in one run, and prints one header line plus one line per diagnostic, repeats folded, errors first, at most 20 (`--max <n>`, `0` for all). With no target it covers the changed files under `apps/`, `libs/` and `pkgs/`, typechecking a changed library too and a framework package against its own `tsconfig.json`. The build runs only when nothing failed before it. Each code comment added since the last commit is an `akan.comment.added` warning. `--files` replaces the git scope, `--lint-only` and `--no-build` shorten the run, `--format json` prints one JSON line, and it exits 0 on a pass, 1 on a failure and 2 on an unknown target. `akan code` runs it for its `akan_verify` tool and end-of-turn check, so a typecheck error now reaches the model, and `akan workflow apply` and MCP `run_validation` validate with it, typecheck and build included.
|
|
66
|
+
- **Agent guide.** `AGENTS.md` keeps only the rules that apply everywhere, under 12 KB instead of about 83 KB, and lists 16 path rules and 16 skills that load only when they apply: Claude Code reads `.claude/rules/akan/<area>.md` and Cursor `.cursor/rules/akan-<area>.mdc` when a matching file is touched, and the procedures sit in `.claude/skills/akan-*` and `.agents/skills/akan-*`, whose `references/` hold the full guidelines. A rule a lint plugin or gate enforces names its check, and the Work Loop checks with `akan verify` and, after a page, UI or runtime change, `akan preview`. `akan agent install` gains the targets `rules`, `skills` and `hooks`, and `--check` writes nothing, lists the files install would create, change or remove, and exits 1 if there are any; it ignores the version stamp, so it fits a CI step. A generated file carries `<!-- akan:generated -->`: a file without it is yours, never removed and overwritten only with `--force`, and generated files a newer CLI no longer produces are removed. `akan doctor` warns with `agent-guide-oversized` when `AGENTS.md` plus the largest app or library `AGENTS.md` pass 80 % of the 32 KiB Codex reads, and with `agent-guide-leftover` for the old `docs/` guides. A new workspace's `.gitignore` keeps `.claude/settings.local.json`, `.claude/worktrees/` and `.claude/scheduled_tasks.lock` out of git. The `workspaceOnboarding` guideline is removed.
|
|
67
|
+
- **Claude Code hooks.** `akan agent install hooks` adds opt-in hooks to `.claude/settings.json`, keeping every other hook there: an edit to a generated file is refused with a pointer to its source, an edited file is linted with fixes and a remaining error sent back, and before the session stops what it edited is checked with `akan verify`, a failure or an added comment sent back once. `all` never installs them.
|
|
68
|
+
- **`akan preview <app> [paths]`** opens each page in a headless browser and prints one verdict line per page (`ok`, `warn` or `FAIL`) with its status, title, load time and where a redirect landed, then its uncaught errors, the server errors logged while it loaded, console errors and warnings, and layout problems; it exits 1 when a page failed. A page fails on a 4xx or 5xx, an empty body, the server's error page, the dev build overlay, an uncaught or console error, a server error or a timeout, and warns on a console warning, a page wider than the viewport, text the colour of its background or a dropped daisyUI colour class. It reuses this checkout's dev server on the app's port, or starts `akan start <app>` and stops it afterwards (`--keep` leaves it running; its output goes to `local/apps/<app>/runtime/preview/server.log`). Paths are comma-separated, a path without a locale gets the default one, and `--changed` adds the pages uncommitted page and layout changes reach, `/` when no route changed, at most 8. `--csr` boots each page as a native app does, `--desktop` uses 1280×800 instead of 390×844, `--screenshot` saves a PNG, `--timeout` defaults to 60 seconds and `--format json` prints the full report. The engine is Bun.WebView: WebKit on macOS, so nothing is downloaded, and an installed Chrome or Chromium elsewhere (`--engine`). `@akanjs/devkit/preview` exports `PreviewView` and `PreviewChrome`, and the `akan code` preview check now reports console warnings too.
|
|
69
|
+
- **Client errors in the dev log.** In `akan start`, every page the dev server serves, in a browser, a CSR boot or a native shell, reports its uncaught errors and unhandled rejections, logged as `ClientReport` error records naming the page, its boot and a native shell's platform and target, so `akan logs` and the dev terminal show them. A page reports at most 50, with control characters stripped.
|
|
70
|
+
- **`akan context --app <app>`** lists the app's routes: path, file, search keys, and whether a guard applies, its own or a layout's. Past 60 routes the largest folders fold into one line each; `--format json` lists every route.
|
|
71
|
+
- **Size and write gates.** For an app or library, `akan lint` fails on what no check caught before, and each failure names its rule and, where one exists, the `akan workflow plan …` command that builds the right shape:
|
|
72
|
+
- **Size** (`akan.size.file`, `akan.size.component`): a `Zone`, `Util`, `Template`, `Unit`, `View`, `*.store.ts`, or `ui/` / `webkit/` file over 400 lines; a service or document over 500; an abstract over 300; a component or hook over 250.
|
|
73
|
+
- **Store reach** (`akan.component.store-access`): a component or hook reaching `st` more than 16 times, form setters aside.
|
|
74
|
+
- **Where a write starts** (`akan.write.placement`): a `fetch.<mutation>` outside a `*.store.ts`, or a writing `st.do.*` in `ui/` or `webkit/`, which take the write as a prop or hook argument instead.
|
|
75
|
+
- **Chained writes** (`akan.store.multi-write`, `akan.store.root-write-chain`, `akan.component.write-chain`): two or more writes, or one per item in a loop or a `map`, sent by one store action, store `static` or component or hook handler, directly, through a sibling action or through another store via `RootStore`. Exclusive paths count apart, and a retry or a poll counts its write once.
|
|
76
|
+
- **Per-item service calls** (`akan.service.call-in-loop`): a service method an `endpoint` or `slice` reaches that calls a service, a model or a generated query once per item. `internal` jobs, counted loops and a `get<Model>` / `load<Model>` in a `map` callback, which the DataLoader batches, are spared.
|
|
77
|
+
- **Baselines.** `akan quality baseline` writes an `akan.baseline.json` at each app or library root that needs one, recording each breach under its rule with what was measured (`--force` rewrites one). After that `akan lint` fails only on what grows or is new, and lowers an entry as its code shrinks. It refuses a baseline that adds or raises an entry against its copy at the default branch's fork point, and with `--no-fix` one above what the code measures. A size, store-reach or per-item entry may be added or raised when its key carries a `reasons` line; the write rules take no exceptions. `akan quality scan` counts each app's adopted debt and reasoned exceptions (`akan.baseline.exceptions`), and `libs/shared` and `libs/util` ship their own baselines.
|
|
78
|
+
- **Write-path workflows.** `akan workflow plan add-mutation` builds a whole write path in one apply: the document chain method, the service method, the mutation, a one-line store action, the dictionary entry and signal test stubs. `--guard` is required and checked against the guards the target reaches, `None` refused, and a name another reachable module claims is refused as the server would refuse it at boot. `--with <ref>.<method>` runs other writes inside the mutation's `this.transaction()`, `--after <ref>.<method>` runs side effects in `this.afterCommit()`, and either is refused when the workflow cannot supply its arguments. Under a guard that only checks sign-in, the service method gets a `TODO` for the acting user's right to the record and the tests a refusal to fill in; a `Public` mutation gets a rate limit. `akan workflow plan create-interaction` creates a `webkit/use<X>.tsx` hook over a pure `common/<x>.ts` class and its test, for drag, selection, keyboard and layout logic that would otherwise grow inside a component.
|
|
79
|
+
- **Lists past 500 rows.** A client list asked for more than 500 rows is still served 500, and the server now logs it once per list. The lint rule `no-oversized-list-limit` fails on a literal limit above 500 in `fetch.<model>List<Suffix>(…, skip, limit)`, in `{ limit }` passed to `fetch.init…`, `st.do.init…` or `st.do.refresh…`, and in `st.do.setLimitOf…`. A store list asked for more than 500 reads `hasMoreOf…` as true after a full 500, so `loadMoreOf…` fetches the rest instead of stopping.
|
|
80
|
+
- **`akan quality scan`** also warns about a `lib/` module or `ui/` entry whose comments pass 1 % of its lines once it holds five (`akan.comment.density`), a `// deprecated` comment that should be a `/** @deprecated */` tag (`akan.comment.deprecated-marker`), an export in an app's `common/`, `webkit/`, `srvkit/` or `ui/` that only tests use (`akan.global.test-only-export`), and a `*Kind` / `*Type` / `*Mode` enum value compared in three or more files outside its module (`akan.global.kind-branch`).
|
|
81
|
+
|
|
82
|
+
**Fixes**
|
|
83
|
+
|
|
84
|
+
- `save()` on a document of a model with a `field.secret` field, or one read with a `select`, writes only the fields it changed, as for any other document, and after `refresh()` too. It wrote back every field the document held, undoing what another write changed in between, such as a counter's increment or a status move. A field the read left out is written only when you set it. Outside production, saving a document that was never read, which still writes every field it holds, logs a warning once per model.
|
|
85
|
+
- A pubsub publishes `0`, `false` and `""`, which it dropped with `Failed to serialize data`; a `{ nullable: true }` pubsub (or one returning `Any`) delivers `null`, and publishing `undefined` to it delivers `null`.
|
|
86
|
+
- An endpoint's answer keeps the `""` a nullable `String` field holds, so a field declared `field(String, { default: "" })`, in a model or an embedded scalar, reaches the client as `""` rather than `null`. An argument or input left `""` is still sent as not given.
|
|
87
|
+
- A `router.push`, `router.replace` or `router.enterDeepLink` called before the router is initialized, as from a page's mount effect, waits for it instead of throwing `Router is not initialized`.
|
|
88
|
+
- `fetch.ws` listeners on `"connect"` and `"disconnect"` fire. The `.reconnect()` overlay runs only where `AKAN_PUBLIC_ENV` is `local`.
|
|
89
|
+
- In the browser, a Korean `RecentTime` or `dayjs().locale("ko")` no longer falls back to English after hydration.
|
|
90
|
+
- A server-rendered page's client components read their own request's locale and path while other requests render, and an `x-locale` header counts only when it names one of the app's locales.
|
|
91
|
+
- `usePageLocation()` on an SSR page reads the URL's query, so `?filter=` links, the dashboard's selected tile and `viewInUrl` see it. It read none there: the SSR page wrapper carried no search params and never followed a shallow replace. The server render now reads the request's query and the base store starts from the address bar, so the first browser render matches it; the pathname stays the one the page was rendered for.
|
|
92
|
+
- A HEAD request to a page, or a request the page redirects after its render has started, no longer logs `[SSR] render failed`. The status and `Location` it answers are unchanged.
|
|
93
|
+
- The built-in 404 page hydrates without a hydration error in an app whose root layout sets `.theme("dark")` or `.theme("light")`, which put a `data-theme` on `<html>` the 404 document never named.
|
|
94
|
+
- The `Clipboard` button from `akanjs/ui` has an accessible name, `base.copy`.
|
|
95
|
+
- A toast shown with a `duration` of 0 or less, such as `msg.loading(key, { key, duration: 0 })`, stays until a toast with its `key` replaces it or the user closes it. It disappeared at once.
|
|
96
|
+
- In `Data.ListContainer` and `Model.AdminPanel`, editing a record the loaded page does not list opens the edit form, where it opened nothing. A dialog whose title or action slot goes away no longer keeps showing the last one.
|
|
97
|
+
- A streamed API response is no longer buffered whole before it is sent: from the producer's first pause it streams out compressed. A body complete when the handler returns is compressed with its length as before, and a stream that passes 4 MiB before its first pause is sent uncompressed. `application/wasm` is compressible, and a `.br`/`.gz` sidecar beside a `.wasm` in `public/` is served.
|
|
98
|
+
- A plain GET on the websocket path answers `426 Upgrade Required` instead of `500`, at the gateway too, so a crawler or an uptime probe no longer counts as a server failure.
|
|
99
|
+
- A fetch client created after another in the same process, such as a lib's on the server, finds the endpoints the earlier one registered.
|
|
100
|
+
- A service declared with `serve(..., { serverMode })` is enabled or not by the `SERVER_MODE` in effect when the server boots, not when its module is imported.
|
|
101
|
+
- A desktop app whose carried server gives up during a trial before the window opens quits into the previous release instead of waiting behind the alert, and until it restarts it offers, downloads and applies nothing, so it can no longer move the rolled-back release back. A rollback whose file moves failed keeps the release it moves back, and the tries it counted, for the next start. An app is never offered the release it was built from.
|
|
102
|
+
- A macOS desktop app no longer takes minutes to download an update while its window is hidden or covered: `download()` holds an App Nap activity until it ends, through the shell's new `activity.begin` / `activity.end` ops, which still allow idle system sleep.
|
|
103
|
+
- In `akan code`, a sub-agent started by `task` runs on the model the session is on, the one `--model` or `/model` chose, with that model's `models.json` entry. It ran on `deepseek/deepseek-flash` whenever DeepSeek had a key.
|
|
104
|
+
- `akan push-update` exits 2 when the project rejects a release or refuses it with a `release.error.*` key, for phone packs too, and every 400 refusal prints the data it carries.
|
|
105
|
+
- A workspace that typechecks code reaching `@akanjs/devkit/akanConfig`, `/pipeline` or `/mobile` no longer fails with `Cannot find module '@akanjs/native/api'`.
|
|
106
|
+
- `akan lint`, `akan typecheck` and every sync that formats generated files run where only Bun is installed, and `akan lint <app>` passes for an app whose libs carry env modules.
|
|
107
|
+
- A new app's `tsconfig.json` no longer names the Next.js TS plugin or `.next/types`, a new workspace's `bunfig.toml` no longer loads `bun-plugin-tailwind`, and `akan doctor` warns (`config-leftover`) about those lines in an existing workspace.
|
|
108
|
+
- When a desktop app quits and its carried server outlasts the grace, the shell waits for the killed server to exit, so on Windows the server's files are no longer still locked after the quit.
|
|
109
|
+
- On Windows, `akan verify-dist-package` names a dist file with `/` and leaves out its `node_modules`, as on macOS and Linux.
|
|
110
|
+
- In `akan start`, a tab's hot-update socket connects through the dev gateway again. Under Bun 1.4.4 the replica refused it, so a save no longer reached an open tab.
|
|
111
|
+
- A desktop or iOS build no longer stops when the file `android.googleServices` names is missing. Only an Android build reads it, and that build still stops without it.
|
|
112
|
+
- On macOS, `akan start` measures the dev builder against `AKAN_BUILDER_MAX_RSS_MB` by the memory it holds, as the builder reports it. Pages it already freed no longer count, so a builder that gave memory back is no longer replaced, and a fresh one no longer makes a low ceiling look unmeetable.
|
|
113
|
+
- A workspace, app, library, package, module, service, scalar, CRUD page or migration the CLI creates is written as Biome prints it, as are the `.mcp.json` and `.cursor/mcp.json` that `akan mcp-install` writes, so the first `akan lint` no longer rewrites files nobody has touched.
|
|
114
|
+
- A new app in a workspace created with `--libs true` imports `Auth` from `@libs/shared/ui`. Its root layout imported `@shared/ui`, a name nothing in the workspace resolves.
|
|
115
|
+
- `akan workflow apply` of `add-enum-field`, or of `add-field --type enum`, works on the sample `task` module a new workspace ships, where it failed with `primitive-post-edit-dictionary-verify-failed` and wrote nothing: the enum block landed inside the dictionary's header comment. `akan workflow plan add-field` takes `--surfaces template,unit` and `--include-in-light`, the inputs `akan workflow explain add-field` lists, and explain prints each camelCase input's flag beside it.
|
|
116
|
+
|
|
3
117
|
## 3.1.0
|
|
4
118
|
|
|
5
119
|
### Minor Changes
|
package/README.ko.md
CHANGED
|
@@ -46,6 +46,28 @@ akan start <app-name> --open
|
|
|
46
46
|
워크스페이스 이름과 앱 이름을 물은 뒤 `akan` CLI와 워크스페이스 의존성을 설치하고 샘플 앱을 만들어 줍니다. 앱은
|
|
47
47
|
`http://localhost:8282`에서 열립니다.
|
|
48
48
|
|
|
49
|
+
### 기존 저장소 안에서 시작하기
|
|
50
|
+
|
|
51
|
+
앱이 놓일 폴더와 실행 방식이 정해진 작업이라면, 예를 들어 다른 저장소의 하위 폴더, Node와 npm만 있는 호스트, 스펙이 정한
|
|
52
|
+
URL이라면, 코딩 에이전트는 묻지 않고 아래대로 진행하면 됩니다.
|
|
53
|
+
|
|
54
|
+
- **그 폴더에 바로 만들기:** `bunx create-akan-workspace@latest . --app <app> --dir <folder> --sample false`.
|
|
55
|
+
`<folder>` 자체에 쓰고, 이미 있는 파일은 덮어쓰지 않고 멈추며, 샘플 모듈과 페이지는 만들지 않습니다. 함께 생기는
|
|
56
|
+
`AGENTS.md`, `CLAUDE.md`, `.claude/`, `.agents/`, `.cursor/`, `.mcp.json`은 지우지 마세요. 에이전트가 따를 규칙과
|
|
57
|
+
도구가 들어 있습니다.
|
|
58
|
+
- **`<folder>`에서 작업하기.** `akan`은 워크스페이스 루트에서만 실행되고, 그곳에서 연 에이전트 세션이 워크스페이스의
|
|
59
|
+
규칙과 MCP 서버를 불러옵니다.
|
|
60
|
+
- **변경마다 `akan verify <app>`로 확인하기.** sync, lint, 타입체크, 빌드, SSR 검사를 한 번에 하고 실패한 것만
|
|
61
|
+
출력합니다. `akan preview <app> --changed`는 변경이 닿는 페이지를 열어 봅니다.
|
|
62
|
+
- **정해진 URL:** 페이지는 기본적으로 `/<locale>/…` 아래에 있습니다. 기본 로케일을 프리픽스 없는 경로
|
|
63
|
+
(`/pokemon/deoxys`)로 내보내려면 `akan.config.ts`에 `i18n: { defaultLocale: "en", locales: ["en"], localePrefix:
|
|
64
|
+
"as-needed" }`를 두세요. `<Link>`도 따라갑니다.
|
|
65
|
+
- **Node와 npm만 있는 호스트:** `npm install -g bun`으로 생성기를 실행할 런타임을 받으세요. 호스트에도 설치되도록
|
|
66
|
+
워크스페이스의 `dependencies`에 `bun`을 두고, package script에서는 `devDependencies`의 `@akanjs/cli`로 `akan`을
|
|
67
|
+
부르세요. 전역 설치 뒤 `akan`을 찾지 못하면 `bun pm bin -g`가 출력하는 폴더를 `PATH`에 더하세요.
|
|
68
|
+
- **Docker 없이 프로덕션 빌드 실행:** `akan build <app>` 뒤 `cd dist/apps/<app> && PORT=3000 bun main.js`. 빌드에
|
|
69
|
+
필요한 env가 함께 들어 있습니다.
|
|
70
|
+
|
|
49
71
|
## 이 모든 게 한 줄입니다
|
|
50
72
|
|
|
51
73
|
```ts
|
package/README.md
CHANGED
|
@@ -45,6 +45,28 @@ akan start <app-name> --open
|
|
|
45
45
|
The creator asks for a workspace name and an app name, installs the `akan` CLI and the workspace's dependencies, and
|
|
46
46
|
generates a sample app. The app opens on `http://localhost:8282`.
|
|
47
47
|
|
|
48
|
+
### Inside an existing repository
|
|
49
|
+
|
|
50
|
+
When a task fixes where the app lives and how it runs, for example a folder in another repository, a host with only
|
|
51
|
+
Node and npm, or URLs given by a spec, a coding agent needs these, and nothing has to ask:
|
|
52
|
+
|
|
53
|
+
- **Create the workspace in that folder:** `bunx create-akan-workspace@latest . --app <app> --dir <folder> --sample false`.
|
|
54
|
+
It writes into `<folder>` itself, refuses to overwrite a file already there, and leaves out the sample module and
|
|
55
|
+
pages. Keep the `AGENTS.md`, `CLAUDE.md`, `.claude/`, `.agents/`, `.cursor/` and `.mcp.json` it writes: they carry
|
|
56
|
+
the rules and tools an agent works through.
|
|
57
|
+
- **Work from `<folder>`.** `akan` runs only from the workspace root, and an agent session opened there loads the
|
|
58
|
+
workspace's rules and MCP server.
|
|
59
|
+
- **Check each change with `akan verify <app>`.** It syncs, lints, typechecks, builds and checks SSR, and prints only
|
|
60
|
+
what failed. `akan preview <app> --changed` opens the pages a change reaches.
|
|
61
|
+
- **Exact URLs:** pages live under `/<locale>/…`. To serve the default locale at the bare path (`/pokemon/deoxys`),
|
|
62
|
+
set `i18n: { defaultLocale: "en", locales: ["en"], localePrefix: "as-needed" }` in `akan.config.ts`; `<Link>`
|
|
63
|
+
follows.
|
|
64
|
+
- **A host with only Node and npm:** `npm install -g bun` gives the creator its runtime. Put `bun` in the workspace's
|
|
65
|
+
`dependencies` so the host installs it too, and let the package scripts call `akan` from the `@akanjs/cli` in
|
|
66
|
+
`devDependencies`. If `akan` is not found after a global install, add the folder `bun pm bin -g` prints to `PATH`.
|
|
67
|
+
- **Run the production build without Docker:** `akan build <app>`, then `cd dist/apps/<app> && PORT=3000 bun main.js`.
|
|
68
|
+
The build carries the env it needs.
|
|
69
|
+
|
|
48
70
|
## All Of It Is One Line
|
|
49
71
|
|
|
50
72
|
```ts
|
package/base/baseEnv.ts
CHANGED
package/client/cookie.ts
CHANGED
|
@@ -4,17 +4,17 @@ import {
|
|
|
4
4
|
bearerTokenOf,
|
|
5
5
|
type CookieEntry,
|
|
6
6
|
CookieJar,
|
|
7
|
-
CookieSweep,
|
|
8
7
|
decodeJwtPayload,
|
|
9
8
|
isOwnAuthToken,
|
|
10
9
|
Logger,
|
|
11
10
|
legacyAuthTokenKey,
|
|
12
11
|
readSessionToken,
|
|
13
12
|
retiredAuthTokenKeys,
|
|
13
|
+
type SessionCookieDuplicate,
|
|
14
14
|
SessionCookies,
|
|
15
15
|
} from "akanjs/common";
|
|
16
16
|
import type { Account } from "akanjs/fetch";
|
|
17
|
-
import { cookieJar as serverCookieJar, headers as serverHeaders } from "akanjs/fetch";
|
|
17
|
+
import { SessionCookie, cookieJar as serverCookieJar, headers as serverHeaders } from "akanjs/fetch";
|
|
18
18
|
import { secretStorage } from "./storage";
|
|
19
19
|
import { fetch } from "./useClient";
|
|
20
20
|
|
|
@@ -43,6 +43,9 @@ const writeStoredCookies = (stored: Record<string, string>) => {
|
|
|
43
43
|
localStorage.setItem(storedCookiesKey, JSON.stringify(stored));
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
const encodeCookieValue = (value: string) =>
|
|
47
|
+
value.replace(/[\s\p{Cc};,"\\]|%(?![0-9A-Fa-f]{2})/gu, (char) => encodeURIComponent(char));
|
|
48
|
+
|
|
46
49
|
/** Every cookie this page or request holds, a name sent more than once included. */
|
|
47
50
|
export const cookieJar = (): CookieJar => {
|
|
48
51
|
if (getEnv().side === "server") return serverCookieJar();
|
|
@@ -64,14 +67,15 @@ export const setCookie = (
|
|
|
64
67
|
{ path = "/", sameSite = "lax", secure = true }: CookieOptions = {},
|
|
65
68
|
) => {
|
|
66
69
|
if (getEnv().side === "server") return;
|
|
70
|
+
const cookieValue = encodeCookieValue(value);
|
|
67
71
|
if (!pageKeepsCookies()) {
|
|
68
|
-
writeStoredCookies({ ...readStoredCookies(), [key]:
|
|
72
|
+
writeStoredCookies({ ...readStoredCookies(), [key]: cookieValue });
|
|
69
73
|
return;
|
|
70
74
|
}
|
|
71
75
|
const pathAttr = path ? `; path=${path}` : "";
|
|
72
76
|
const secureAttr = secure ? "; Secure" : "";
|
|
73
77
|
|
|
74
|
-
document.cookie = `${key}=${
|
|
78
|
+
document.cookie = `${key}=${cookieValue}${pathAttr}; SameSite=${sameSite}${secureAttr}`;
|
|
75
79
|
};
|
|
76
80
|
|
|
77
81
|
/** `undefined` for a name held more than once, as for one never set. */
|
|
@@ -110,10 +114,19 @@ const sentAuthToken = (): string | undefined => {
|
|
|
110
114
|
}
|
|
111
115
|
};
|
|
112
116
|
|
|
117
|
+
/** The session's claims from its readable cookie: a JWT without its signature, so never a credential. */
|
|
118
|
+
export const getSessionClaims = (): string | undefined =>
|
|
119
|
+
isCsrClient() ? undefined : cookieJar().read(SessionCookie.claimsKey()).value || undefined;
|
|
120
|
+
|
|
121
|
+
const serverHoldsSession = () => getEnv().side !== "server" && !!getSessionClaims();
|
|
122
|
+
|
|
113
123
|
/** The auth token this app holds: on a CSR client the one `fetch` sends first, since a native shell's page keeps no
|
|
114
|
-
* session cookie; then the app-scoped cookie, unless it is held more than once
|
|
124
|
+
* session cookie; then the app-scoped cookie, unless it is held more than once; then, while the server holds the
|
|
125
|
+
* session where no script reads it, the token this page was handed at sign-in, if any. */
|
|
115
126
|
export const getAuthToken = (): string | undefined =>
|
|
116
|
-
(isCsrClient() ? sentAuthToken() : undefined) ??
|
|
127
|
+
(isCsrClient() ? sentAuthToken() : undefined) ??
|
|
128
|
+
readSessionToken(cookieJar()).token ??
|
|
129
|
+
(serverHoldsSession() ? sentAuthToken() : undefined);
|
|
117
130
|
|
|
118
131
|
/** The auth token this app holds in client storage — the OS credential store in a native app, localStorage elsewhere. */
|
|
119
132
|
export const getStoredAuthToken = async (): Promise<string | undefined> =>
|
|
@@ -130,7 +143,7 @@ export const getAccount = <AddData = unknown>(): Account<AddData> => {
|
|
|
130
143
|
const embedded = globalThis.__AKAN_EMBED__;
|
|
131
144
|
if (embedded)
|
|
132
145
|
return embedded.account ? ({ ...defaultAccount, ...embedded.account } as Account<AddData>) : defaultAccount;
|
|
133
|
-
const jwt = getAuthToken() ?? bearerTokenOf(getHeader("authorization"));
|
|
146
|
+
const jwt = getAuthToken() ?? bearerTokenOf(getHeader("authorization")) ?? getSessionClaims();
|
|
134
147
|
if (!jwt) return defaultAccount;
|
|
135
148
|
const account = decodeJwtPayload<Account<AddData>>(jwt);
|
|
136
149
|
if (account.appName !== getEnv().appName || account.environment !== getEnv().environment) return defaultAccount;
|
|
@@ -145,12 +158,17 @@ interface SetAuthOption {
|
|
|
145
158
|
const announceAuth = () => {
|
|
146
159
|
if (typeof window !== "undefined") window.dispatchEvent(new Event("akan:auth"));
|
|
147
160
|
};
|
|
148
|
-
/** Sets the active auth token on fetch, the
|
|
161
|
+
/** Sets the active auth token on fetch, then keeps it where the next launch finds it: client storage on a CSR client,
|
|
162
|
+
* the session cookie elsewhere unless the server already holds it there (`SessionCookie.issue`). */
|
|
149
163
|
export const setAuth = ({ jwt }: SetAuthOption) => {
|
|
150
164
|
fetch.setJwt(jwt);
|
|
151
165
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
166
|
+
if (isCsrClient()) void secretStorage.setItem(storedAuthTokenKey(), jwt);
|
|
167
|
+
else {
|
|
168
|
+
|
|
169
|
+
void secretStorage.removeItem(storedAuthTokenKey());
|
|
170
|
+
if (!serverHoldsSession()) setCookie(authTokenKey(), jwt);
|
|
171
|
+
}
|
|
154
172
|
|
|
155
173
|
for (const key of retiredAuthTokenKeys()) removeCookie(key);
|
|
156
174
|
void secretStorage.removeItem(legacyAuthTokenKey);
|
|
@@ -161,17 +179,17 @@ interface InitAuthOption {
|
|
|
161
179
|
jwt?: string;
|
|
162
180
|
}
|
|
163
181
|
|
|
164
|
-
const sweepDocumentCookies = (
|
|
182
|
+
const sweepDocumentCookies = (duplicates: SessionCookieDuplicate[]) => {
|
|
165
183
|
const { hostname = "", pathname = "/" } = pageLocation() ?? {};
|
|
166
184
|
|
|
167
|
-
for (const entry of
|
|
185
|
+
for (const entry of SessionCookies.sweepOf(duplicates, hostname, pathname)) document.cookie = entry;
|
|
168
186
|
};
|
|
169
187
|
|
|
170
188
|
const readCookieSession = (): string | null | undefined => {
|
|
171
189
|
const first = readSessionToken(cookieJar());
|
|
172
190
|
if (!first.duplicate) return first.token;
|
|
173
191
|
SessionCookies.report([first.duplicate], pageLocation()?.host ?? "");
|
|
174
|
-
sweepDocumentCookies([first.duplicate
|
|
192
|
+
sweepDocumentCookies([first.duplicate]);
|
|
175
193
|
const second = readSessionToken(cookieJar());
|
|
176
194
|
return second.duplicate ? null : second.token;
|
|
177
195
|
};
|
|
@@ -205,9 +223,11 @@ export const initAuth = ({ jwt }: InitAuthOption = {}) => {
|
|
|
205
223
|
Logger.verbose(`JWT ${token ? "restored" : "not found"}`);
|
|
206
224
|
};
|
|
207
225
|
|
|
226
|
+
/** Signs this page out; an HttpOnly session the server holds ends only with `SessionCookie.expire`. */
|
|
208
227
|
export const resetAuth = () => {
|
|
209
228
|
fetch.setJwt(null);
|
|
210
229
|
removeCookie(authTokenKey());
|
|
230
|
+
removeCookie(SessionCookie.claimsKey());
|
|
211
231
|
for (const key of retiredAuthTokenKeys()) removeCookie(key);
|
|
212
232
|
void secretStorage.removeItem(storedAuthTokenKey());
|
|
213
233
|
void secretStorage.removeItem(legacyAuthTokenKey);
|
package/client/csrLocation.ts
CHANGED
|
@@ -9,6 +9,7 @@ export class CsrLocation {
|
|
|
9
9
|
|
|
10
10
|
#view: URL | null = null;
|
|
11
11
|
#entry: { idx: number | undefined; href: string } | null = null;
|
|
12
|
+
#onBack: ((idx: number) => void) | null = null;
|
|
12
13
|
|
|
13
14
|
/** Reads `href`, a path on the app's origin, as this document's location from now on; a view never moves. */
|
|
14
15
|
view(href: string) {
|
|
@@ -78,7 +79,14 @@ export class CsrLocation {
|
|
|
78
79
|
return { ...record, __akanRouter: { ...record.__akanRouter, idx } };
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/** Hears each `back()` as it moves, with the index of the entry it lands on; one listener, the router's. */
|
|
83
|
+
onBack(listener: (idx: number) => void) {
|
|
84
|
+
this.#onBack = listener;
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
back() {
|
|
88
|
+
const idx = this.entry().idx;
|
|
89
|
+
if (idx !== undefined) this.#onBack?.(idx - 1);
|
|
82
90
|
window.history.back();
|
|
83
91
|
}
|
|
84
92
|
|
package/client/csrTypes.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } from "@react-spring/web";
|
|
2
3
|
import type { ReactDOMAttributes } from "@use-gesture/react/dist/declarations/src/types";
|
|
3
4
|
import type { PromiseOrObject } from "akanjs/base";
|
|
4
5
|
import { type ForwardRefExoticComponent, type ReactNode, type RefObject, useContext } from "react";
|
|
5
|
-
import type { AnimatedComponent, AnimatedProps, Interpolation, SpringValue } from "react-spring";
|
|
6
6
|
import type { TailwindBreakpoint } from "./frameConfig";
|
|
7
7
|
import type { RouteDefinition } from "./route/RouteDefinition";
|
|
8
8
|
import type { RouterInstance } from "./router";
|
|
@@ -366,6 +366,8 @@ export interface PathContextType {
|
|
|
366
366
|
pageType: CsrPageType;
|
|
367
367
|
pageKey?: string;
|
|
368
368
|
location: Location;
|
|
369
|
+
/** An SSR page's: `location` is the request's, and in the browser the store holds the router's live one. */
|
|
370
|
+
followsRouter?: boolean;
|
|
369
371
|
prefix?: string;
|
|
370
372
|
gestureEnabled: boolean;
|
|
371
373
|
setGestureEnabled: (enabled: boolean) => void;
|
package/client/index.ts
CHANGED
|
@@ -12,7 +12,12 @@ export * from "./locale";
|
|
|
12
12
|
export * from "./makePageProto";
|
|
13
13
|
export * from "./mcpView";
|
|
14
14
|
export * from "./nativeHost";
|
|
15
|
-
export
|
|
15
|
+
export {
|
|
16
|
+
type NavigationAttempt,
|
|
17
|
+
type NavigationGuard,
|
|
18
|
+
type NavigationKind,
|
|
19
|
+
NavigationRefused,
|
|
20
|
+
} from "./navigationGuards";
|
|
16
21
|
export * from "./resolveServerUrl";
|
|
17
22
|
export * from "./route";
|
|
18
23
|
export * from "./router";
|
package/client/makePageProto.tsx
CHANGED
|
@@ -21,6 +21,15 @@ interface ErrPayload extends ErrRestoreOption {
|
|
|
21
21
|
data?: Record<string, unknown>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const htmlEntities: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
25
|
+
const escapeRichParams = (param: { [key: string]: string | number } | undefined) =>
|
|
26
|
+
Object.fromEntries(
|
|
27
|
+
Object.entries(param ?? {}).map(([key, value]) => [
|
|
28
|
+
key,
|
|
29
|
+
String(value).replace(/[&<>"']/g, (char) => htmlEntities[char] ?? char),
|
|
30
|
+
]),
|
|
31
|
+
);
|
|
32
|
+
|
|
24
33
|
const getPageInfo = (): { locale: string; path: string } => {
|
|
25
34
|
const { defaultLocale, locales } = parseAkanI18nEnv();
|
|
26
35
|
const localeSet = new Set(locales);
|
|
@@ -159,7 +168,7 @@ export const makePageProto = <
|
|
|
159
168
|
|
|
160
169
|
dangerouslySetInnerHTML={{
|
|
161
170
|
__html: translator.translate(lang, key, {
|
|
162
|
-
...param,
|
|
171
|
+
...escapeRichParams(param),
|
|
163
172
|
br: `<br />`,
|
|
164
173
|
}),
|
|
165
174
|
}}
|
|
@@ -2,7 +2,7 @@ import { Logger } from "akanjs/common";
|
|
|
2
2
|
|
|
3
3
|
export type NavigationKind = "push" | "replace" | "back" | "pop";
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface NavigationAttempt {
|
|
6
6
|
/** `pop` is the browser's back or forward; `back` is `router.back()`, a native back button or a swipe. */
|
|
7
7
|
kind: NavigationKind;
|
|
8
8
|
/** The route on screen, without its locale and basePath, as `/docs/1?tab=a`. */
|
|
@@ -12,10 +12,10 @@ export interface Navigation {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** Answers `false` to keep the page; a guard that throws or rejects keeps it too. */
|
|
15
|
-
export type NavigationGuard = (navigation:
|
|
15
|
+
export type NavigationGuard = (navigation: NavigationAttempt) => boolean | Promise<boolean>;
|
|
16
16
|
|
|
17
17
|
export class NavigationRefused extends Error {
|
|
18
|
-
constructor(navigation:
|
|
18
|
+
constructor(navigation: NavigationAttempt) {
|
|
19
19
|
super(`A navigation guard kept ${navigation.from} instead of moving to ${navigation.to ?? "the previous page"}`);
|
|
20
20
|
this.name = "NavigationRefused";
|
|
21
21
|
}
|
|
@@ -36,7 +36,7 @@ export class NavigationGuards {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
decide(navigation:
|
|
39
|
+
decide(navigation: NavigationAttempt): boolean | Promise<boolean> {
|
|
40
40
|
if (this.#isDeciding) return false;
|
|
41
41
|
const guards = [...this.#guards];
|
|
42
42
|
for (const [idx, guard] of guards.entries()) {
|
|
@@ -51,13 +51,13 @@ export class NavigationGuards {
|
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
static async #decideRest(first: Promise<boolean>, rest: NavigationGuard[], navigation:
|
|
54
|
+
static async #decideRest(first: Promise<boolean>, rest: NavigationGuard[], navigation: NavigationAttempt) {
|
|
55
55
|
if (!(await first)) return false;
|
|
56
56
|
for (const guard of rest) if (!(await NavigationGuards.#ask(guard, navigation))) return false;
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
static #ask(guard: NavigationGuard, navigation:
|
|
60
|
+
static #ask(guard: NavigationGuard, navigation: NavigationAttempt): boolean | Promise<boolean> {
|
|
61
61
|
const refuse = (error: unknown) => {
|
|
62
62
|
Logger.error(`A navigation guard failed, so ${navigation.from} was kept: ${String(error)}`);
|
|
63
63
|
return false;
|