akanjs 3.1.1 → 3.1.2-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +22 -0
- package/README.md +22 -0
- package/client/cookie.ts +23 -7
- package/client/csrTypes.ts +1 -1
- package/client/makePageProto.tsx +10 -1
- package/client/router.ts +57 -16
- package/common/IpLiteral.ts +4 -2
- package/common/TrustedProxy.ts +45 -2
- package/common/akanNames.ts +169 -0
- package/common/codeAgentProfile.ts +51 -4
- package/common/index.ts +13 -0
- 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/index.ts +1 -0
- package/constant/inputParser.ts +10 -2
- package/constant/requestValue.ts +47 -0
- package/constant/via.ts +3 -13
- 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/queryEvaluator.ts +13 -8
- 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/sessionCookie.ts +37 -0
- package/index.ts +8 -0
- package/package.json +13 -27
- package/server/akanApp.ts +108 -29
- package/server/akanServer.ts +18 -7
- package/server/assetHeaders.ts +20 -1
- package/server/cachePolicy.ts +18 -5
- package/server/devtools/depsSerializer.ts +2 -1
- package/server/devtools/devtoolsRouter.ts +3 -2
- package/server/devtools/signalSerializer.ts +5 -5
- package/server/hmr/clientReport.ts +10 -5
- 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 +20 -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 +50 -40
- 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 +5 -3
- 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 +38 -18
- 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 +4 -0
- 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/QueryCompiler.ts +28 -4
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +45 -11
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +6 -1
- package/service/predefinedAdaptor/sql/types.ts +3 -0
- package/service/predefinedAdaptor/storage.adaptor.ts +11 -10
- package/service/serve.ts +3 -3
- 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 +20 -49
- 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/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/client/cookie.d.ts +7 -2
- package/types/client/csrTypes.d.ts +1 -1
- package/types/client/router.d.ts +4 -1
- package/types/common/akanNames.d.ts +149 -0
- package/types/common/codeAgentProfile.d.ts +4 -1
- package/types/common/index.d.ts +3 -1
- 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/index.d.ts +1 -0
- package/types/constant/requestValue.d.ts +5 -0
- package/types/constant/via.d.ts +3 -8
- 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/fetch/client/sliceInitHandle.d.ts +1 -2
- package/types/fetch/index.d.ts +1 -0
- package/types/fetch/sessionCookie.d.ts +12 -0
- package/types/index.d.ts +8 -0
- package/types/server/akanApp.d.ts +1 -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/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/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/SqlDocumentStore.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +3 -0
- 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/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/Icon.d.ts +15 -0
- 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 +10 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +6 -0
- package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +6 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +7 -0
- 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 +2 -0
- 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/useCsrValues.d.ts +3 -3
- 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 +2 -8
- package/ui/Data/ListContainer.tsx +4 -4
- package/ui/Data/QueryMaker.tsx +3 -2
- package/ui/Data/RefPicker.tsx +3 -2
- package/ui/Data/UnlistedEditModal.tsx +2 -1
- package/ui/Data/ViewActions.tsx +4 -4
- package/ui/Data/dataExport.ts +3 -0
- package/ui/Data/useViewModal.ts +10 -15
- package/ui/Dialog/LegacyModal.tsx +1 -1
- package/ui/Dialog/Provider.tsx +2 -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 +4 -11
- 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 +3 -3
- package/ui/System/Reconnect.tsx +2 -2
- package/ui/System/SSR.tsx +2 -2
- 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/src/acl.rs +166 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +25 -6
- 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 +10 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +2 -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/publish.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +30 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +1 -1
- 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/main.ts +5 -1
- package/vendor/@akanjs/native/packages/desktop/src/paths.ts +24 -1
- 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 +106 -18
- 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/useCsrValues.ts +5 -1
- 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/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/client/cookie.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
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
|
|
|
@@ -114,10 +114,19 @@ const sentAuthToken = (): string | undefined => {
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
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
|
+
|
|
117
123
|
/** The auth token this app holds: on a CSR client the one `fetch` sends first, since a native shell's page keeps no
|
|
118
|
-
* 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. */
|
|
119
126
|
export const getAuthToken = (): string | undefined =>
|
|
120
|
-
(isCsrClient() ? sentAuthToken() : undefined) ??
|
|
127
|
+
(isCsrClient() ? sentAuthToken() : undefined) ??
|
|
128
|
+
readSessionToken(cookieJar()).token ??
|
|
129
|
+
(serverHoldsSession() ? sentAuthToken() : undefined);
|
|
121
130
|
|
|
122
131
|
/** The auth token this app holds in client storage — the OS credential store in a native app, localStorage elsewhere. */
|
|
123
132
|
export const getStoredAuthToken = async (): Promise<string | undefined> =>
|
|
@@ -134,7 +143,7 @@ export const getAccount = <AddData = unknown>(): Account<AddData> => {
|
|
|
134
143
|
const embedded = globalThis.__AKAN_EMBED__;
|
|
135
144
|
if (embedded)
|
|
136
145
|
return embedded.account ? ({ ...defaultAccount, ...embedded.account } as Account<AddData>) : defaultAccount;
|
|
137
|
-
const jwt = getAuthToken() ?? bearerTokenOf(getHeader("authorization"));
|
|
146
|
+
const jwt = getAuthToken() ?? bearerTokenOf(getHeader("authorization")) ?? getSessionClaims();
|
|
138
147
|
if (!jwt) return defaultAccount;
|
|
139
148
|
const account = decodeJwtPayload<Account<AddData>>(jwt);
|
|
140
149
|
if (account.appName !== getEnv().appName || account.environment !== getEnv().environment) return defaultAccount;
|
|
@@ -149,12 +158,17 @@ interface SetAuthOption {
|
|
|
149
158
|
const announceAuth = () => {
|
|
150
159
|
if (typeof window !== "undefined") window.dispatchEvent(new Event("akan:auth"));
|
|
151
160
|
};
|
|
152
|
-
/** 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`). */
|
|
153
163
|
export const setAuth = ({ jwt }: SetAuthOption) => {
|
|
154
164
|
fetch.setJwt(jwt);
|
|
155
165
|
|
|
156
|
-
if (
|
|
157
|
-
|
|
166
|
+
if (isCsrClient()) void secretStorage.setItem(storedAuthTokenKey(), jwt);
|
|
167
|
+
else {
|
|
168
|
+
|
|
169
|
+
void secretStorage.removeItem(storedAuthTokenKey());
|
|
170
|
+
if (!serverHoldsSession()) setCookie(authTokenKey(), jwt);
|
|
171
|
+
}
|
|
158
172
|
|
|
159
173
|
for (const key of retiredAuthTokenKeys()) removeCookie(key);
|
|
160
174
|
void secretStorage.removeItem(legacyAuthTokenKey);
|
|
@@ -209,9 +223,11 @@ export const initAuth = ({ jwt }: InitAuthOption = {}) => {
|
|
|
209
223
|
Logger.verbose(`JWT ${token ? "restored" : "not found"}`);
|
|
210
224
|
};
|
|
211
225
|
|
|
226
|
+
/** Signs this page out; an HttpOnly session the server holds ends only with `SessionCookie.expire`. */
|
|
212
227
|
export const resetAuth = () => {
|
|
213
228
|
fetch.setJwt(null);
|
|
214
229
|
removeCookie(authTokenKey());
|
|
230
|
+
removeCookie(SessionCookie.claimsKey());
|
|
215
231
|
for (const key of retiredAuthTokenKeys()) removeCookie(key);
|
|
216
232
|
void secretStorage.removeItem(storedAuthTokenKey());
|
|
217
233
|
void secretStorage.removeItem(legacyAuthTokenKey);
|
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";
|
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
|
}}
|
package/client/router.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getEnv } from "akanjs/base";
|
|
2
2
|
import {
|
|
3
|
+
type AkanLocalePrefix,
|
|
3
4
|
getBasePathFromPathname,
|
|
5
|
+
LocalePath,
|
|
4
6
|
Logger,
|
|
5
7
|
originFromRequest,
|
|
6
8
|
parseAkanI18nEnv,
|
|
@@ -108,11 +110,6 @@ const getConfiguredBasePaths = () => new Set(parseBasePaths(process.env.AKAN_PUB
|
|
|
108
110
|
const shouldExposeBasePath = () => getEnv().operationMode === "local";
|
|
109
111
|
const CSR_RUNTIME_SEARCH_PARAMS = ["csr", "akanMobileTarget", "akanMobileBasePath", "akanMobileIndexPath"] as const;
|
|
110
112
|
|
|
111
|
-
const getLocaleFromPathname = (pathname: string) => {
|
|
112
|
-
const [firstSegment] = pathname.split("/").filter(Boolean);
|
|
113
|
-
return parseAkanI18nEnv().locales.find((locale) => locale === firstSegment);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
113
|
const normalizeRoutePath = (path: string | undefined) => {
|
|
117
114
|
const normalized = path?.trim();
|
|
118
115
|
if (!normalized) return undefined;
|
|
@@ -163,7 +160,17 @@ declare global {
|
|
|
163
160
|
var __AKAN_ROUTER__: Router | undefined;
|
|
164
161
|
}
|
|
165
162
|
|
|
166
|
-
|
|
163
|
+
const withoutNetworkPath = (pathname: string) => {
|
|
164
|
+
|
|
165
|
+
const path = pathname.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020/\\]+/, "");
|
|
166
|
+
return `/${path}`;
|
|
167
|
+
};
|
|
168
|
+
export const getPathInfo = (
|
|
169
|
+
requestUrl: string,
|
|
170
|
+
lang: string,
|
|
171
|
+
prefix: string,
|
|
172
|
+
localePrefix: AkanLocalePrefix | undefined = parseAkanI18nEnv().localePrefix,
|
|
173
|
+
) => {
|
|
167
174
|
const [urlWithoutHash, hash = ""] = requestUrl.split("#");
|
|
168
175
|
const [url, search = ""] = urlWithoutHash.split("?");
|
|
169
176
|
const langLength = lang.length + 1;
|
|
@@ -177,13 +184,20 @@ export const getPathInfo = (requestUrl: string, lang: string, prefix: string) =>
|
|
|
177
184
|
? pathWithSubRoute.slice(prefixLength)
|
|
178
185
|
: pathWithSubRoute;
|
|
179
186
|
const subRoute = prefix ? `/${prefix}` : "";
|
|
180
|
-
|
|
181
|
-
const
|
|
187
|
+
const langSegment = LocalePath.segmentOf(lang, { defaultLocale: parseAkanI18nEnv().defaultLocale, localePrefix });
|
|
188
|
+
const appPathname = `${langSegment}${subRoute}${path === "/" ? "" : path}`;
|
|
189
|
+
|
|
190
|
+
const pathname = Href.hasWebScheme(path)
|
|
191
|
+
? path
|
|
192
|
+
: langSegment || subRoute
|
|
193
|
+
? appPathname
|
|
194
|
+
: withoutNetworkPath(appPathname);
|
|
182
195
|
const href = `${pathname}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
|
|
183
196
|
return { path, pathname, hash, search, href };
|
|
184
197
|
};
|
|
185
198
|
class Router {
|
|
186
199
|
isInitialized: boolean = false;
|
|
200
|
+
#isCsr: boolean = false;
|
|
187
201
|
#prefix = "";
|
|
188
202
|
#lang = parseAkanI18nEnv().defaultLocale;
|
|
189
203
|
#routePaths = new Set<string>();
|
|
@@ -201,6 +215,7 @@ class Router {
|
|
|
201
215
|
#expectedPops: { idx: number; isAllowed: boolean; onLand?: () => void }[] = [];
|
|
202
216
|
|
|
203
217
|
#beforeInit: (() => void)[] = [];
|
|
218
|
+
#frameParentOrigins: string[] | "*" = [];
|
|
204
219
|
#instance: RouterInstance = {
|
|
205
220
|
push: (href: string) => {
|
|
206
221
|
const { href: fullHref } = this.#getPathInfo(href);
|
|
@@ -218,6 +233,7 @@ class Router {
|
|
|
218
233
|
},
|
|
219
234
|
};
|
|
220
235
|
init(options: SsrClientRouterOption | SsrServerRouterOption | CSRClientRouterOption) {
|
|
236
|
+
this.#isCsr = options.type === "csr";
|
|
221
237
|
this.#prefix = options.prefix ?? "";
|
|
222
238
|
this.#lang = options.lang ?? parseAkanI18nEnv().defaultLocale;
|
|
223
239
|
this.#routePaths = new Set(
|
|
@@ -395,8 +411,17 @@ class Router {
|
|
|
395
411
|
csrLocation.replace(nextState);
|
|
396
412
|
}
|
|
397
413
|
|
|
414
|
+
#localePrefix(): AkanLocalePrefix {
|
|
415
|
+
return this.#isCsr ? "always" : (parseAkanI18nEnv().localePrefix ?? "always");
|
|
416
|
+
}
|
|
417
|
+
#langSegment(lang = this.#lang) {
|
|
418
|
+
return LocalePath.segmentOf(lang, {
|
|
419
|
+
defaultLocale: parseAkanI18nEnv().defaultLocale,
|
|
420
|
+
localePrefix: this.#localePrefix(),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
398
423
|
#getPathInfo(href: string, prefix = this.#prefix) {
|
|
399
|
-
return getPathInfo(href, this.#lang, prefix);
|
|
424
|
+
return getPathInfo(href, this.#lang, prefix, this.#localePrefix());
|
|
400
425
|
}
|
|
401
426
|
#getNavigationPathInfo(href: string) {
|
|
402
427
|
return this.#getPathInfo(href, shouldExposeBasePath() ? this.#prefix : "");
|
|
@@ -417,12 +442,16 @@ class Router {
|
|
|
417
442
|
return `${pathname}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
418
443
|
}
|
|
419
444
|
#getVisiblePathInfo(href: string, lang = this.#lang) {
|
|
420
|
-
return getPathInfo(href, lang, shouldExposeBasePath() ? this.#prefix : "");
|
|
445
|
+
return getPathInfo(href, lang, shouldExposeBasePath() ? this.#prefix : "", this.#localePrefix());
|
|
421
446
|
}
|
|
422
447
|
|
|
423
448
|
#postPathAfterBack() {
|
|
424
449
|
if (this.canGoBack()) this.#backsAwaitingPopstate += 1;
|
|
425
450
|
}
|
|
451
|
+
/** Origins besides the page's own that hear `pathChange` when they frame it in production; `"*"` is any parent. */
|
|
452
|
+
setFrameParentOrigins(origins: string[] | "*") {
|
|
453
|
+
this.#frameParentOrigins = origins;
|
|
454
|
+
}
|
|
426
455
|
/** Tells the frame embedding this page where it went: a new path, or a page of a paginated list. */
|
|
427
456
|
postPathChange(change: { path: string; pathname: string; hash: string } | { page: number }) {
|
|
428
457
|
const message =
|
|
@@ -431,8 +460,13 @@ class Router {
|
|
|
431
460
|
: { type: "pathChange", path: change.path, pathname: change.pathname, hash: change.hash };
|
|
432
461
|
if (!("page" in change)) Logger.info(`pathChange-start:${change.path}${change.hash ? `#${change.hash}` : ""}`);
|
|
433
462
|
|
|
434
|
-
if (isMcpView()) return;
|
|
435
|
-
|
|
463
|
+
if (isMcpView() || window.parent === window) return;
|
|
464
|
+
|
|
465
|
+
const targets =
|
|
466
|
+
getEnv().operationMode === "local" || this.#frameParentOrigins === "*"
|
|
467
|
+
? ["*"]
|
|
468
|
+
: [window.location.origin, ...this.#frameParentOrigins];
|
|
469
|
+
for (const target of targets) window.parent.postMessage(message, target);
|
|
436
470
|
}
|
|
437
471
|
#postDevSyncNavigation(kind: "push" | "replace", href: string) {
|
|
438
472
|
if (globalThis.__AKAN_DEV_SYNC_NAVIGATION_APPLYING__) return;
|
|
@@ -639,7 +673,12 @@ class Router {
|
|
|
639
673
|
const langFromPath = reqPathname.split("/").find(Boolean);
|
|
640
674
|
const lang = (h.get("x-locale") ?? langFromPath ?? this.#lang) as string;
|
|
641
675
|
const basePath = getServerBasePath(reqPathname, lang, h.get("x-base-path") ?? undefined, this.#prefix);
|
|
642
|
-
const { pathname, href: fullHref } = getPathInfo(
|
|
676
|
+
const { pathname, href: fullHref } = getPathInfo(
|
|
677
|
+
href,
|
|
678
|
+
lang,
|
|
679
|
+
shouldExposeBasePath() ? basePath : "",
|
|
680
|
+
this.#localePrefix(),
|
|
681
|
+
);
|
|
643
682
|
Logger.info(`redirect to:${pathname}`);
|
|
644
683
|
throw new AkanRedirectError(fullHref, method, status);
|
|
645
684
|
} else {
|
|
@@ -663,7 +702,8 @@ class Router {
|
|
|
663
702
|
setLang(lang: string) {
|
|
664
703
|
if (getEnv().side === "server") throw new Error("setLang is only available in client side");
|
|
665
704
|
this.#checkInitialized();
|
|
666
|
-
const
|
|
705
|
+
const i18n = { ...parseAkanI18nEnv(), localePrefix: this.#localePrefix() };
|
|
706
|
+
const currentLang = LocalePath.split(csrLocation.pathname, i18n).locale ?? this.#lang;
|
|
667
707
|
const { path, search, hash } = this.#getVisiblePathInfo(csrLocation.href, currentLang);
|
|
668
708
|
this.#lang = lang;
|
|
669
709
|
this.#instance.replace(`${path}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`);
|
|
@@ -679,13 +719,14 @@ class Router {
|
|
|
679
719
|
}
|
|
680
720
|
getFullPath(withLang = true) {
|
|
681
721
|
if (getEnv().side === "server") throw new Error("getPath is only available in client side");
|
|
682
|
-
|
|
722
|
+
const path = `${this.#prefix}${this.getPath()}`;
|
|
723
|
+
return withLang ? `${this.#langSegment()}/${path}`.replace(/^\/+/, "/") : path;
|
|
683
724
|
}
|
|
684
725
|
getPrefix() {
|
|
685
726
|
return this.#prefix;
|
|
686
727
|
}
|
|
687
728
|
getPrefixedPath(path: string) {
|
|
688
|
-
return this.#prefix ? `${this.#lang ?
|
|
729
|
+
return this.#prefix ? `${this.#lang ? this.#langSegment() : ""}/${this.#prefix}${path}` : path;
|
|
689
730
|
}
|
|
690
731
|
}
|
|
691
732
|
const browserRouter = () => {
|
package/common/IpLiteral.ts
CHANGED
|
@@ -4,9 +4,11 @@ export class IpLiteral {
|
|
|
4
4
|
|
|
5
5
|
static parse(entry: string): string | null {
|
|
6
6
|
const trimmed = entry.trim();
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
const bracketed = /^\[([^\]]*)\](?::\d{1,5})?$/.exec(trimmed)?.[1];
|
|
9
|
+
const v6InBrackets = bracketed?.includes(":") ? bracketed : undefined;
|
|
8
10
|
const v4WithPort = /^(\d{1,3}(?:\.\d{1,3}){3}):\d{1,5}$/.exec(trimmed);
|
|
9
|
-
const [address = "", zone, ...rest] = (
|
|
11
|
+
const [address = "", zone, ...rest] = (v6InBrackets ?? v4WithPort?.[1] ?? trimmed).split("%");
|
|
10
12
|
if (rest.length) return null;
|
|
11
13
|
if (zone !== undefined && !(address.includes(":") && /^[A-Za-z0-9_.-]{1,64}$/.test(zone))) return null;
|
|
12
14
|
const bytes = IpBytes.of(IpBytes.normalize(address));
|
package/common/TrustedProxy.ts
CHANGED
|
@@ -27,6 +27,7 @@ export class TrustedProxy {
|
|
|
27
27
|
static readonly #verdicts = new Map<string, boolean>();
|
|
28
28
|
static readonly #verdictLimit = 1024;
|
|
29
29
|
static #warnedIgnoredPeer = false;
|
|
30
|
+
static #warnedUnlistedChain = false;
|
|
30
31
|
|
|
31
32
|
static configure(list: string | null | undefined) {
|
|
32
33
|
TrustedProxy.reset();
|
|
@@ -37,6 +38,7 @@ export class TrustedProxy {
|
|
|
37
38
|
TrustedProxy.#policy = null;
|
|
38
39
|
TrustedProxy.#verdicts.clear();
|
|
39
40
|
TrustedProxy.#warnedIgnoredPeer = false;
|
|
41
|
+
TrustedProxy.#warnedUnlistedChain = false;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
static #load(): TrustPolicy {
|
|
@@ -104,6 +106,7 @@ export class TrustedProxy {
|
|
|
104
106
|
|
|
105
107
|
const judge = TrustedProxy.#load().strict ? (address: string) => TrustedProxy.isTrusted(address) : undefined;
|
|
106
108
|
const recorded = clientAddressFromHeaders(headers, judge);
|
|
109
|
+
if (peerAddress) TrustedProxy.#noteUnlistedChain(headers, peerAddress);
|
|
107
110
|
if (recorded) return { address: recorded, forwarded: true };
|
|
108
111
|
} else if (peerAddress) TrustedProxy.#noteIgnoredPeer(headers, peerAddress);
|
|
109
112
|
return {
|
|
@@ -123,16 +126,56 @@ export class TrustedProxy {
|
|
|
123
126
|
);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
static #noteUnlistedChain(headers: Headers, peerAddress: string) {
|
|
130
|
+
if (TrustedProxy.#warnedUnlistedChain) return;
|
|
131
|
+
const policy = TrustedProxy.#load();
|
|
132
|
+
if (policy.strict || policy.trustAll) return;
|
|
133
|
+
const hops = (headers.get("x-forwarded-for") ?? "")
|
|
134
|
+
.split(",")
|
|
135
|
+
.map((hop) => hop.trim())
|
|
136
|
+
.filter((hop) => hop);
|
|
137
|
+
const realIp = headers.get("x-real-ip")?.trim();
|
|
138
|
+
const last = hops.at(-1);
|
|
139
|
+
const disagrees = realIp ? !!last && IpLiteral.parse(last) !== IpLiteral.parse(realIp) : hops.length > 1;
|
|
140
|
+
if (!disagrees) return;
|
|
141
|
+
const bytes = IpBytes.of(normalizeIpAddress(peerAddress));
|
|
142
|
+
if (!bytes || TrustedProxy.#isLoopback(bytes)) return;
|
|
143
|
+
TrustedProxy.#warnedUnlistedChain = true;
|
|
144
|
+
new Logger("TrustedProxy").warn(
|
|
145
|
+
`AKAN_TRUSTED_PROXIES is unset, so the client address comes from the x-real-ip or leftmost x-forwarded-for that ${normalizeIpAddress(peerAddress)} sent. If that proxy appends to those headers instead of overwriting them, list it in AKAN_TRUSTED_PROXIES so the chain is read from the nearest hop.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
/** For refusing an outbound fetch to a caller-chosen name: judges the address alone, never the proxy list. */
|
|
127
150
|
static isPrivateAddress(address: string): boolean {
|
|
128
151
|
const bytes = IpBytes.of(normalizeIpAddress(address));
|
|
129
|
-
|
|
152
|
+
return !bytes || TrustedProxy.#isNonPublic(bytes);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static #isNonPublic(bytes: Uint8Array): boolean {
|
|
130
156
|
if (TrustedProxy.#isLoopback(bytes) || TrustedProxy.#isPrivateRange(bytes)) return true;
|
|
131
157
|
if (bytes.length === 4) {
|
|
132
158
|
const [a = 0, b = 0] = bytes;
|
|
133
159
|
return a === 0 || (a === 100 && b >= 64 && b <= 127) || a >= 224;
|
|
134
160
|
}
|
|
135
|
-
|
|
161
|
+
const embedded = TrustedProxy.#embeddedIpv4(bytes);
|
|
162
|
+
if (embedded) return TrustedProxy.#isNonPublic(embedded);
|
|
163
|
+
const [first = 0, second = 0, third = 0, fourth = 0, fifth = 0, sixth = 0] = bytes;
|
|
164
|
+
const localNat64 =
|
|
165
|
+
first === 0 && second === 0x64 && third === 0xff && fourth === 0x9b && fifth === 0 && sixth === 1;
|
|
166
|
+
return localNat64 || first === 0xff;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static #embeddedIpv4(bytes: Uint8Array): Uint8Array | null {
|
|
170
|
+
const zero = (from: number, to: number) => bytes.subarray(from, to).every((byte) => byte === 0);
|
|
171
|
+
const groupAt = (idx: number) => ((bytes[idx] ?? 0) << 8) | (bytes[idx + 1] ?? 0);
|
|
172
|
+
const tail = bytes.slice(12);
|
|
173
|
+
if (zero(0, 10) && (groupAt(10) === 0xffff || groupAt(10) === 0)) return tail;
|
|
174
|
+
if (zero(0, 8) && groupAt(8) === 0xffff && groupAt(10) === 0) return tail;
|
|
175
|
+
if (groupAt(0) === 0x64 && groupAt(2) === 0xff9b && zero(4, 12)) return tail;
|
|
176
|
+
if (groupAt(0) === 0x2002) return bytes.slice(2, 6);
|
|
177
|
+
if (groupAt(0) === 0x2001 && groupAt(2) === 0) return tail.map((byte) => byte ^ 0xff);
|
|
178
|
+
return null;
|
|
136
179
|
}
|
|
137
180
|
|
|
138
181
|
static #isLoopback(bytes: Uint8Array): boolean {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { capitalize, lowerlize } from "./capitalize";
|
|
2
|
+
|
|
3
|
+
export const sliceNames = (refName: string, suffix = "") => {
|
|
4
|
+
const [Model, Suffix] = [capitalize(refName), capitalize(suffix)];
|
|
5
|
+
const live = `${refName}Live${Suffix}`;
|
|
6
|
+
return {
|
|
7
|
+
slice: `${refName}${Suffix}`,
|
|
8
|
+
state: {
|
|
9
|
+
defaultModel: `default${Model}${Suffix}`,
|
|
10
|
+
modelInitList: `${refName}InitList${Suffix}`,
|
|
11
|
+
modelInsight: `${refName}Insight${Suffix}`,
|
|
12
|
+
modelList: `${refName}List${Suffix}`,
|
|
13
|
+
modelListLoading: `${refName}ListLoading${Suffix}`,
|
|
14
|
+
modelInitAt: `${refName}InitAt${Suffix}`,
|
|
15
|
+
modelStaleAt: `${refName}StaleAt${Suffix}`,
|
|
16
|
+
lastPageOfModel: `lastPageOf${Model}${Suffix}`,
|
|
17
|
+
pageOfModel: `pageOf${Model}${Suffix}`,
|
|
18
|
+
limitOfModel: `limitOf${Model}${Suffix}`,
|
|
19
|
+
hasMoreOfModel: `hasMoreOf${Model}${Suffix}`,
|
|
20
|
+
isCumulativeOfModel: `isCumulativeOf${Model}${Suffix}`,
|
|
21
|
+
hasInsightOfModel: `hasInsightOf${Model}${Suffix}`,
|
|
22
|
+
queryArgsOfModel: `queryArgsOf${Model}${Suffix}`,
|
|
23
|
+
sortOfModel: `sortOf${Model}${Suffix}`,
|
|
24
|
+
modelSelection: `${refName}Selection${Suffix}`,
|
|
25
|
+
},
|
|
26
|
+
action: {
|
|
27
|
+
initModel: `init${Model}${Suffix}`,
|
|
28
|
+
refreshModel: `refresh${Model}${Suffix}`,
|
|
29
|
+
selectModel: `select${Model}${Suffix}`,
|
|
30
|
+
setPageOfModel: `setPageOf${Model}${Suffix}`,
|
|
31
|
+
loadMoreOfModel: `loadMoreOf${Model}${Suffix}`,
|
|
32
|
+
setLimitOfModel: `setLimitOf${Model}${Suffix}`,
|
|
33
|
+
setQueryArgsOfModel: `setQueryArgsOf${Model}${Suffix}`,
|
|
34
|
+
setSortOfModel: `setSortOf${Model}${Suffix}`,
|
|
35
|
+
applyLiveModel: `applyLive${Model}${Suffix}`,
|
|
36
|
+
watchLiveModel: `watchLive${Model}${Suffix}`,
|
|
37
|
+
},
|
|
38
|
+
endpoint: {
|
|
39
|
+
list: `${refName}List${Suffix}`,
|
|
40
|
+
insight: `${refName}Insight${Suffix}`,
|
|
41
|
+
live,
|
|
42
|
+
subscribeLive: endpointNames(live).subscribe,
|
|
43
|
+
init: `init${Model}${Suffix}`,
|
|
44
|
+
getInit: `get${Model}Init${Suffix}`,
|
|
45
|
+
initResult: `${refName}Init${Suffix}`,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const queryNames = (queryKey: string) => {
|
|
51
|
+
const Key = capitalize(queryKey);
|
|
52
|
+
return {
|
|
53
|
+
list: `list${Key}`,
|
|
54
|
+
listIds: `listIds${Key}`,
|
|
55
|
+
find: `find${Key}`,
|
|
56
|
+
findId: `findId${Key}`,
|
|
57
|
+
pick: `pick${Key}`,
|
|
58
|
+
pickId: `pickId${Key}`,
|
|
59
|
+
exists: `exists${Key}`,
|
|
60
|
+
count: `count${Key}`,
|
|
61
|
+
insight: `insight${Key}`,
|
|
62
|
+
query: `query${Key}`,
|
|
63
|
+
remove: `remove${Key}`,
|
|
64
|
+
removeOne: `removeOne${Key}`,
|
|
65
|
+
purge: `purge${Key}`,
|
|
66
|
+
update: `update${Key}`,
|
|
67
|
+
updateOne: `updateOne${Key}`,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const databaseStateNames = (refName: string) => {
|
|
72
|
+
const className = capitalize(refName);
|
|
73
|
+
return {
|
|
74
|
+
model: refName,
|
|
75
|
+
modelLoading: `${refName}Loading`,
|
|
76
|
+
modelForm: `${refName}Form`,
|
|
77
|
+
modelFormLoading: `${refName}FormLoading`,
|
|
78
|
+
modelFormUploading: `${refName}FormUploading`,
|
|
79
|
+
modelSubmit: `${refName}Submit`,
|
|
80
|
+
modelViewAt: `${refName}ViewAt`,
|
|
81
|
+
modelModal: `${refName}Modal`,
|
|
82
|
+
modelDraft: `${refName}FormDraft`,
|
|
83
|
+
modelOperation: `${refName}Operation`,
|
|
84
|
+
defaultModel: `default${className}`,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const modelNames = (refName: string) => {
|
|
89
|
+
const Model = capitalize(refName);
|
|
90
|
+
return {
|
|
91
|
+
...databaseStateNames(refName),
|
|
92
|
+
getModel: `get${Model}`,
|
|
93
|
+
loadModel: `load${Model}`,
|
|
94
|
+
loadModelMany: `load${Model}Many`,
|
|
95
|
+
createModel: `create${Model}`,
|
|
96
|
+
createModelMany: `create${Model}Many`,
|
|
97
|
+
updateModel: `update${Model}`,
|
|
98
|
+
removeModel: `remove${Model}`,
|
|
99
|
+
modelModel: `${refName}Model`,
|
|
100
|
+
modelLoader: `${refName}Loader`,
|
|
101
|
+
modelCache: `${refName}Cache`,
|
|
102
|
+
modelId: `${refName}Id`,
|
|
103
|
+
lightModel: `light${Model}`,
|
|
104
|
+
viewModel: `view${Model}`,
|
|
105
|
+
getModelView: `get${Model}View`,
|
|
106
|
+
modelView: `${refName}View`,
|
|
107
|
+
editModel: `edit${Model}`,
|
|
108
|
+
getModelEdit: `get${Model}Edit`,
|
|
109
|
+
modelEdit: `${refName}Edit`,
|
|
110
|
+
modelObj: `${refName}Obj`,
|
|
111
|
+
modelObjList: `${refName}ObjList`,
|
|
112
|
+
modelObjInsight: `${refName}ObjInsight`,
|
|
113
|
+
addModelFiles: `add${Model}Files`,
|
|
114
|
+
mergeModel: `merge${Model}`,
|
|
115
|
+
createModelInForm: `create${Model}InForm`,
|
|
116
|
+
updateModelInForm: `update${Model}InForm`,
|
|
117
|
+
checkModelSubmitable: `check${Model}Submitable`,
|
|
118
|
+
submitModel: `submit${Model}`,
|
|
119
|
+
newModel: `new${Model}`,
|
|
120
|
+
setModel: `set${Model}`,
|
|
121
|
+
resetModel: `reset${Model}`,
|
|
122
|
+
invalidateModel: `invalidate${Model}`,
|
|
123
|
+
loadModelFormDraft: `load${Model}FormDraft`,
|
|
124
|
+
restoreModelFormDraft: `restore${Model}FormDraft`,
|
|
125
|
+
discardModelFormDraft: `discard${Model}FormDraft`,
|
|
126
|
+
writeOnModel: `writeOn${Model}`,
|
|
127
|
+
setViewOfModel: `setViewOf${Model}`,
|
|
128
|
+
exportCsvOfModel: `exportCsvOf${Model}`,
|
|
129
|
+
exportJsonOfModel: `exportJsonOf${Model}`,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const moduleNames = (refName: string) => {
|
|
134
|
+
const Model = capitalize(refName);
|
|
135
|
+
return {
|
|
136
|
+
service: `${refName}Service`,
|
|
137
|
+
ServiceClass: `${Model}Service`,
|
|
138
|
+
SliceClass: `${Model}Slice`,
|
|
139
|
+
ModelClass: `${Model}Model`,
|
|
140
|
+
FilterClass: `${Model}Filter`,
|
|
141
|
+
SignalClass: `${Model}Signal`,
|
|
142
|
+
InternalClass: `${Model}Internal`,
|
|
143
|
+
EndpointClass: `${Model}Endpoint`,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const formSetterNames = (className: string, key: string) => {
|
|
148
|
+
const classKeyName = capitalize(key);
|
|
149
|
+
return {
|
|
150
|
+
field: lowerlize(key),
|
|
151
|
+
Field: classKeyName,
|
|
152
|
+
setFieldOnModel: `set${classKeyName}On${className}`,
|
|
153
|
+
addFieldOnModel: `add${classKeyName}On${className}`,
|
|
154
|
+
subFieldOnModel: `sub${classKeyName}On${className}`,
|
|
155
|
+
addOrSubFieldOnModel: `addOrSub${classKeyName}On${className}`,
|
|
156
|
+
|
|
157
|
+
moveFieldOnModel: `move${classKeyName}On${className}`,
|
|
158
|
+
uploadFieldOnModel: `upload${classKeyName}On${className}`,
|
|
159
|
+
|
|
160
|
+
postSetField: `_postSet${classKeyName}`,
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const stateSetterName = (key: string) => `set${capitalize(key)}`;
|
|
165
|
+
|
|
166
|
+
export const endpointNames = (key: string) => {
|
|
167
|
+
const Key = capitalize(key);
|
|
168
|
+
return { subscribe: `subscribe${Key}`, listen: `listen${Key}` };
|
|
169
|
+
};
|