akanjs 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +1 -1
- package/README.md +1 -1
- package/base/symbols.ts +4 -0
- package/client/clientRuntime.ts +8 -0
- package/client/cn.ts +7 -0
- package/client/csrTypes.ts +2 -0
- package/client/frameDebug.ts +3 -2
- package/common/Logger.ts +4 -0
- package/common/deepObjectify.ts +21 -7
- package/common/index.ts +14 -0
- package/common/mcpExposure.ts +99 -0
- package/common/pathSet.ts +17 -5
- package/common/routeConvention.ts +30 -0
- package/constant/crystalize.ts +3 -1
- package/constant/fieldInfo.ts +6 -0
- package/constant/getDefault.ts +37 -9
- package/constant/immerify.ts +1 -1
- package/constant/index.ts +2 -0
- package/constant/labelOf.ts +17 -0
- package/constant/mask.ts +60 -0
- package/constant/purify.ts +17 -11
- package/constant/types.ts +14 -12
- package/dictionary/DictionaryLookup.ts +30 -0
- package/dictionary/agent.dictionary.ts +31 -0
- package/dictionary/agentTurn.dictionary.ts +29 -0
- package/dictionary/base.dictionary.ts +8 -0
- package/dictionary/dictInfo.ts +8 -0
- package/dictionary/dictionary.ts +9 -3
- package/dictionary/index.ts +1 -0
- package/document/database.ts +21 -0
- package/document/filterMeta.ts +7 -0
- package/document/index.ts +1 -0
- package/document/into.ts +7 -1
- package/document/noDocumentError.ts +12 -0
- package/fetch/agentTurn.ts +19 -0
- package/fetch/client/fetchClient.ts +71 -13
- package/fetch/client/wsClient.ts +17 -2
- package/fetch/fetchType/endpointFetch.type.ts +10 -4
- package/fetch/index.ts +1 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
- package/package.json +11 -9
- package/server/akanApp.ts +30 -34
- package/server/akanOption.ts +65 -7
- package/server/akanServer.ts +172 -7
- package/server/animatedImage.ts +83 -0
- package/server/artifact/routeClientCache.ts +14 -0
- package/server/assetEncoding.ts +47 -0
- package/server/devtools/signalSerializer.ts +2 -2
- package/server/devtools/types.ts +2 -2
- package/server/di/diLifecycle.ts +31 -3
- package/server/di/predefinedAdaptor.ts +6 -0
- package/server/imageOptimizer.ts +33 -46
- package/server/index.ts +1 -2
- package/server/mcp/McpAuth.ts +177 -0
- package/server/mcp/McpDispatcher.ts +245 -0
- package/server/mcp/McpEventStream.ts +76 -0
- package/server/mcp/McpExecutionContext.ts +105 -0
- package/server/mcp/McpRouter.ts +591 -0
- package/server/mcp/index.ts +5 -0
- package/server/resolver/CascadeRunner.ts +1 -1
- package/server/resolver/database.resolver.ts +42 -5
- package/server/resolver/service.resolver.ts +29 -0
- package/server/resolver/signal.resolver.ts +16 -1
- package/server/routeTreeBuilder.ts +2 -44
- package/server/rscWorker.tsx +6 -2
- package/server/subRouteIndexDocument.tsx +61 -0
- package/server/systemPageDocument.tsx +2 -2
- package/server/systemPages.tsx +49 -16
- package/server/vendor/akanjs-fetch.ts +1 -0
- package/server/webRouter.ts +159 -35
- package/service/agent.service.ts +14 -0
- package/service/base.service.ts +2 -1
- package/service/index.ts +1 -0
- package/service/predefinedAdaptor/database.adaptor.ts +78 -24
- package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
- package/service/predefinedAdaptor/index.ts +3 -0
- package/service/predefinedAdaptor/insightQuery.ts +183 -0
- package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
- package/service/predefinedAdaptor/role.adaptor.ts +2 -0
- package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
- package/service/serviceModule.ts +17 -0
- package/service/types.ts +4 -0
- package/signal/agent/AgentCatalogue.ts +165 -0
- package/signal/agent/index.ts +1 -0
- package/signal/agent.signal.ts +42 -0
- package/signal/agentTurn.ts +8 -0
- package/signal/agentTurnStream.ts +48 -0
- package/signal/base.signal.ts +2 -1
- package/signal/endpointInfo.ts +38 -2
- package/signal/guard.ts +13 -1
- package/signal/guards.ts +52 -1
- package/signal/index.ts +6 -0
- package/signal/mcp/McpDocument.ts +286 -0
- package/signal/mcp/McpProgress.ts +106 -0
- package/signal/mcp/McpUriTemplate.ts +85 -0
- package/signal/mcp/Msg.ts +368 -0
- package/signal/mcp/index.ts +5 -0
- package/signal/mcp/mcpProtocol.ts +120 -0
- package/signal/middleware.ts +7 -3
- package/signal/openapi/openapi.ts +32 -171
- package/signal/schema/JsonSchemaBuilder.ts +211 -0
- package/signal/schema/index.ts +1 -0
- package/signal/signalContext.ts +85 -9
- package/signal/slice.ts +9 -9
- package/signal/types.ts +1 -1
- package/store/action.ts +11 -22
- package/store/actionTag.ts +33 -0
- package/store/agent/AgentBridge.ts +323 -0
- package/store/agent/AgentContext.ts +74 -0
- package/store/agent/AgentPrompts.ts +82 -0
- package/store/agent/AgentVisibility.ts +68 -0
- package/store/agent/ScreenReader.ts +208 -0
- package/store/agent/StoreCatalogue.ts +351 -0
- package/store/agent/StoreSurfaceSource.ts +122 -0
- package/store/agent/index.ts +9 -0
- package/store/agent/storeSurface.ts +28 -0
- package/store/agent/types.ts +50 -0
- package/store/agentic/StToolBuilder.ts +199 -0
- package/store/agentic/attachAgentic.ts +22 -0
- package/store/agentic/index.ts +5 -0
- package/store/agentic/readableValue.ts +17 -0
- package/store/agentic/useStExpose.ts +18 -0
- package/store/agentic/useStState.ts +35 -0
- package/store/baseSt.ts +7 -0
- package/store/databaseStateNames.ts +31 -0
- package/store/formSetterNames.ts +21 -0
- package/store/index.ts +8 -0
- package/store/rootStore.ts +3 -1
- package/store/sliceRole.ts +36 -0
- package/store/state.ts +2 -12
- package/store/store.ts +19 -1
- package/store/storeInstance.ts +165 -20
- package/store/storeRegistry.ts +16 -1
- package/store/types.ts +18 -0
- package/store/withSelector.ts +7 -8
- package/test/registerDom.ts +4 -0
- package/types/base/symbols.d.ts +4 -0
- package/types/client/cn.d.ts +5 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/Logger.d.ts +2 -0
- package/types/common/deepObjectify.d.ts +4 -2
- package/types/common/index.d.ts +2 -1
- package/types/common/mcpExposure.d.ts +60 -0
- package/types/common/routeConvention.d.ts +8 -0
- package/types/constant/fieldInfo.d.ts +1 -0
- package/types/constant/getDefault.d.ts +5 -0
- package/types/constant/index.d.ts +2 -0
- package/types/constant/labelOf.d.ts +6 -0
- package/types/constant/mask.d.ts +34 -0
- package/types/constant/purify.d.ts +2 -2
- package/types/constant/types.d.ts +2 -2
- package/types/dictionary/DictionaryLookup.d.ts +14 -0
- package/types/dictionary/agent.dictionary.d.ts +1 -0
- package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictInfo.d.ts +6 -0
- package/types/dictionary/dictionary.d.ts +11 -9
- package/types/dictionary/index.d.ts +1 -0
- package/types/document/database.d.ts +20 -0
- package/types/document/filterMeta.d.ts +1 -0
- package/types/document/index.d.ts +1 -0
- package/types/document/into.d.ts +6 -1
- package/types/document/noDocumentError.d.ts +12 -0
- package/types/fetch/agentTurn.d.ts +593 -0
- package/types/fetch/client/fetchClient.d.ts +10 -0
- package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
- package/types/fetch/index.d.ts +1 -0
- package/types/server/akanOption.d.ts +35 -7
- package/types/server/akanServer.d.ts +44 -1
- package/types/server/animatedImage.d.ts +7 -0
- package/types/server/artifact/routeClientCache.d.ts +6 -0
- package/types/server/assetEncoding.d.ts +7 -0
- package/types/server/devtools/types.d.ts +2 -2
- package/types/server/di/predefinedAdaptor.d.ts +4 -1
- package/types/server/index.d.ts +0 -2
- package/types/server/mcp/McpAuth.d.ts +62 -0
- package/types/server/mcp/McpDispatcher.d.ts +54 -0
- package/types/server/mcp/McpEventStream.d.ts +18 -0
- package/types/server/mcp/McpExecutionContext.d.ts +44 -0
- package/types/server/mcp/McpRouter.d.ts +60 -0
- package/types/server/mcp/index.d.ts +5 -0
- package/types/server/mcp.d.ts +1 -0
- package/types/server/subRouteIndexDocument.d.ts +8 -0
- package/types/server/systemPageDocument.d.ts +1 -0
- package/types/server/systemPages.d.ts +5 -0
- package/types/server/vendor/akanjs-fetch.d.ts +1 -0
- package/types/service/agent.service.d.ts +12 -0
- package/types/service/base.service.d.ts +4 -0
- package/types/service/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +24 -2
- package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
- package/types/service/predefinedAdaptor/index.d.ts +3 -0
- package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
- package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
- package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
- package/types/service/types.d.ts +4 -1
- package/types/signal/agent/AgentCatalogue.d.ts +100 -0
- package/types/signal/agent/index.d.ts +1 -0
- package/types/signal/agent.d.ts +1 -0
- package/types/signal/agent.signal.d.ts +29 -0
- package/types/signal/agentTurn.d.ts +9 -0
- package/types/signal/agentTurnStream.d.ts +16 -0
- package/types/signal/base.signal.d.ts +1 -1
- package/types/signal/endpointInfo.d.ts +21 -3
- package/types/signal/guard.d.ts +11 -0
- package/types/signal/guards.d.ts +22 -1
- package/types/signal/index.d.ts +6 -0
- package/types/signal/mcp/McpDocument.d.ts +65 -0
- package/types/signal/mcp/McpProgress.d.ts +40 -0
- package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
- package/types/signal/mcp/Msg.d.ts +143 -0
- package/types/signal/mcp/index.d.ts +5 -0
- package/types/signal/mcp/mcpProtocol.d.ts +107 -0
- package/types/signal/mcp.d.ts +1 -0
- package/types/signal/openapi/openapi.d.ts +3 -3
- package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
- package/types/signal/schema/index.d.ts +1 -0
- package/types/signal/schema.d.ts +1 -0
- package/types/signal/signalContext.d.ts +16 -1
- package/types/signal/slice.d.ts +8 -8
- package/types/signal/types.d.ts +1 -1
- package/types/store/actionTag.d.ts +21 -0
- package/types/store/agent/AgentBridge.d.ts +77 -0
- package/types/store/agent/AgentContext.d.ts +16 -0
- package/types/store/agent/AgentPrompts.d.ts +31 -0
- package/types/store/agent/AgentVisibility.d.ts +21 -0
- package/types/store/agent/ScreenReader.d.ts +11 -0
- package/types/store/agent/StoreCatalogue.d.ts +26 -0
- package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
- package/types/store/agent/index.d.ts +9 -0
- package/types/store/agent/storeSurface.d.ts +14 -0
- package/types/store/agent/types.d.ts +49 -0
- package/types/store/agent.d.ts +1 -0
- package/types/store/agentic/StToolBuilder.d.ts +41 -0
- package/types/store/agentic/attachAgentic.d.ts +14 -0
- package/types/store/agentic/index.d.ts +5 -0
- package/types/store/agentic/readableValue.d.ts +3 -0
- package/types/store/agentic/useStExpose.d.ts +8 -0
- package/types/store/agentic/useStState.d.ts +12 -0
- package/types/store/agentic.d.ts +1 -0
- package/types/store/baseSt.d.ts +29 -22
- package/types/store/databaseStateNames.d.ts +25 -0
- package/types/store/formSetterNames.d.ts +16 -0
- package/types/store/index.d.ts +7 -0
- package/types/store/rootStore.d.ts +5 -1
- package/types/store/sliceRole.d.ts +25 -0
- package/types/store/store.d.ts +7 -2
- package/types/store/storeInstance.d.ts +33 -1
- package/types/store/storeRegistry.d.ts +5 -0
- package/types/store/types.d.ts +15 -0
- package/types/store/withSelector.d.ts +7 -8
- package/types/test/registerDom.d.ts +1 -0
- package/types/ui/Agent/Approval.d.ts +7 -0
- package/types/ui/Agent/Bubble.d.ts +13 -0
- package/types/ui/Agent/Chat.d.ts +24 -0
- package/types/ui/Agent/Context.d.ts +9 -0
- package/types/ui/Agent/Dock.d.ts +16 -0
- package/types/ui/Agent/Guide.d.ts +9 -0
- package/types/ui/Agent/Section.d.ts +10 -0
- package/types/ui/Agent/StateKey.d.ts +16 -0
- package/types/ui/Agent/Tool.d.ts +15 -0
- package/types/ui/Agent/Transcript.d.ts +13 -0
- package/types/ui/Agent/Zone.d.ts +23 -0
- package/types/ui/Agent/fetchRunner.d.ts +12 -0
- package/types/ui/Agent/index.d.ts +17 -0
- package/types/ui/Agent/index_.d.ts +1 -0
- package/types/ui/Agent/sessionHistory.d.ts +12 -0
- package/types/ui/Agent.d.ts +1 -0
- package/types/ui/Badge.d.ts +1 -1
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Dropdown.d.ts +2 -0
- package/types/ui/Image.d.ts +3 -3
- package/types/ui/Layout/BottomInset.d.ts +2 -1
- package/types/ui/Layout/index.d.ts +1 -1
- package/types/ui/Signal/style.d.ts +4 -1
- package/types/ui/UiOverride/context.d.ts +14 -2
- package/types/ui/agentAttrs.d.ts +14 -0
- package/types/ui/index.d.ts +4 -1
- package/types/ui/overlayLayer.d.ts +24 -0
- package/types/ui/recipe/badgeRecipe.d.ts +7 -3
- package/types/ui/recipe/buttonRecipe.d.ts +7 -3
- package/types/ui/recipe/inputRecipe.d.ts +5 -1
- package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
- package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
- package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
- package/types/vendor/use-agentic/Agentic.d.ts +21 -0
- package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
- package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
- package/types/vendor/use-agentic/index.d.ts +13 -0
- package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
- package/types/vendor/use-agentic/types.d.ts +133 -0
- package/types/vendor/use-agentic/useAgent.d.ts +4 -0
- package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
- package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
- package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
- package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
- package/types/vendor/use-agentic.d.ts +1 -0
- package/types/webkit/index.d.ts +3 -0
- package/types/webkit/lazy.d.ts +12 -0
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/types/webkit/useEscapeKey.d.ts +5 -0
- package/types/webkit/useFrameRuntime.d.ts +6 -1
- package/types/webkit/useScreenScope.d.ts +18 -0
- package/ui/Agent/Approval.tsx +29 -0
- package/ui/Agent/Bubble.tsx +74 -0
- package/ui/Agent/Chat.tsx +225 -0
- package/ui/Agent/Context.tsx +38 -0
- package/ui/Agent/Dock.tsx +72 -0
- package/ui/Agent/Guide.tsx +16 -0
- package/ui/Agent/Section.tsx +24 -0
- package/ui/Agent/StateKey.tsx +44 -0
- package/ui/Agent/Tool.tsx +66 -0
- package/ui/Agent/Transcript.tsx +33 -0
- package/ui/Agent/Zone.tsx +48 -0
- package/ui/Agent/fetchRunner.ts +30 -0
- package/ui/Agent/index.ts +12 -0
- package/ui/Agent/index_.tsx +4 -0
- package/ui/Agent/sessionHistory.ts +33 -0
- package/ui/Badge.tsx +3 -3
- package/ui/BottomSheet.tsx +5 -0
- package/ui/Button.tsx +6 -2
- package/ui/Constant/Doc.tsx +1 -1
- package/ui/CsrImage.tsx +1 -1
- package/ui/Data/ListContainer.tsx +1 -1
- package/ui/DatePicker.tsx +5 -4
- package/ui/Dialog/Modal.tsx +12 -15
- package/ui/DraggableList.tsx +3 -1
- package/ui/Dropdown.tsx +33 -11
- package/ui/Field.tsx +30 -16
- package/ui/Image.tsx +3 -3
- package/ui/Input.tsx +14 -5
- package/ui/Layout/BottomInset.tsx +6 -1
- package/ui/Load/Units.tsx +11 -2
- package/ui/Load/View.tsx +7 -2
- package/ui/Loading/ProgressBar.tsx +8 -1
- package/ui/Menu.tsx +7 -8
- package/ui/Model/EditModal.tsx +37 -2
- package/ui/Model/SureToRemove.tsx +2 -1
- package/ui/Model/index_.tsx +42 -15
- package/ui/ObjectId.tsx +3 -4
- package/ui/Pagination.tsx +3 -4
- package/ui/Popconfirm.tsx +8 -7
- package/ui/Select.tsx +6 -4
- package/ui/Signal/RestApi.tsx +43 -16
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +6 -1
- package/ui/Switch.tsx +2 -0
- package/ui/System/CSR.tsx +6 -2
- package/ui/System/ThemeToggle.tsx +55 -33
- package/ui/ToggleSelect.tsx +4 -3
- package/ui/Tooltip.tsx +2 -1
- package/ui/UiOverride/context.ts +14 -2
- package/ui/agentAttrs.ts +19 -0
- package/ui/index.ts +10 -1
- package/ui/overlayLayer.ts +39 -0
- package/ui/recipe/badgeRecipe.ts +22 -3
- package/ui/recipe/buttonRecipe.ts +51 -2
- package/ui/recipe/factory.ts +2 -2
- package/ui/recipe/inputRecipe.ts +18 -4
- package/vendor/use-agentic/AgentProvider.tsx +39 -0
- package/vendor/use-agentic/AgentScope.tsx +20 -0
- package/vendor/use-agentic/AgentSession.ts +292 -0
- package/vendor/use-agentic/Agentic.tsx +44 -0
- package/vendor/use-agentic/AgenticSurface.ts +294 -0
- package/vendor/use-agentic/WIRE.md +57 -0
- package/vendor/use-agentic/httpRunner.ts +115 -0
- package/vendor/use-agentic/index.ts +14 -0
- package/vendor/use-agentic/surfaceContext.ts +9 -0
- package/vendor/use-agentic/types.ts +144 -0
- package/vendor/use-agentic/useAgent.ts +17 -0
- package/vendor/use-agentic/useAgentGuide.ts +16 -0
- package/vendor/use-agentic/useAgentResource.ts +30 -0
- package/vendor/use-agentic/useAgentState.ts +61 -0
- package/vendor/use-agentic/useAgentTool.ts +55 -0
- package/webkit/bootCsr.tsx +2 -33
- package/webkit/index.ts +3 -0
- package/webkit/lazy.tsx +27 -3
- package/webkit/useCsrValues.ts +121 -4
- package/webkit/useEscapeKey.tsx +42 -0
- package/webkit/useFrameRuntime.ts +65 -32
- package/webkit/useScreenScope.tsx +49 -0
package/server/akanApp.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport,
|
|
|
7
7
|
import { isTraceEnabled } from "akanjs/signal";
|
|
8
8
|
import { makeAkanChildProxyHeaders } from "./akanAppHeaders";
|
|
9
9
|
import type { BuilderCsrReq, BuilderCsrRes, BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
|
|
10
|
+
import { resolveEncodedSidecar } from "./assetEncoding";
|
|
10
11
|
import { isPortInUseError } from "./lifecycle/portInUse";
|
|
11
12
|
import { RotatingLogWriter } from "./logging/rotatingLogWriter";
|
|
12
13
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
@@ -75,6 +76,8 @@ export class AkanApp {
|
|
|
75
76
|
/** Hosted by `akan start`: crash loops should yield to the dev host, which restarts on file edits. */
|
|
76
77
|
readonly #devHosted = process.env.AKAN_COMMAND_TYPE === "start";
|
|
77
78
|
readonly #healthTimeoutMs = AkanApp.#parseHealthTimeoutMs();
|
|
79
|
+
/** Child stderr is bundler/runtime noise the gateway cannot act on; it still reaches the rotating log file. */
|
|
80
|
+
readonly #printChildStderr = process.env.AKAN_CHILD_STDERR === "1";
|
|
78
81
|
readonly #serverPath: string;
|
|
79
82
|
readonly #artifactDir: string;
|
|
80
83
|
readonly #replica: AkanReplicaConfig;
|
|
@@ -156,10 +159,16 @@ export class AkanApp {
|
|
|
156
159
|
return Math.max(min, parsed);
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The command that started this gateway outranks an inherited `NODE_ENV`: the command is a statement about
|
|
164
|
+
* this run, while the env var can arrive from a shell export, a CI image, or the workspace `.env` Bun loads
|
|
165
|
+
* on its own. Handing a dev child `NODE_ENV=production` makes it serve from the production route cache and
|
|
166
|
+
* throw on every route, since only `akan build` writes the routes manifest that cache expects.
|
|
167
|
+
*/
|
|
159
168
|
static #defaultChildNodeEnv() {
|
|
160
|
-
if (process.env.NODE_ENV) return process.env.NODE_ENV;
|
|
161
169
|
if (process.env.AKAN_COMMAND_TYPE === "start") return "development";
|
|
162
170
|
if (process.env.AKAN_COMMAND_TYPE === "build") return "production";
|
|
171
|
+
if (process.env.NODE_ENV) return process.env.NODE_ENV;
|
|
163
172
|
return Bun.main.endsWith(".js") ? "production" : "development";
|
|
164
173
|
}
|
|
165
174
|
|
|
@@ -585,7 +594,11 @@ export class AkanApp {
|
|
|
585
594
|
const result = ws.send(event.data as string | ArrayBuffer);
|
|
586
595
|
if (result === 0) upstream.close();
|
|
587
596
|
});
|
|
588
|
-
upstream.addEventListener("close", (event) =>
|
|
597
|
+
upstream.addEventListener("close", (event) => {
|
|
598
|
+
const code = AkanApp.#sendableCloseCode(event.code);
|
|
599
|
+
if (code === undefined) ws.close();
|
|
600
|
+
else ws.close(code, event.reason);
|
|
601
|
+
});
|
|
589
602
|
upstream.addEventListener("error", () => ws.close(1011, "upstream websocket error"));
|
|
590
603
|
Object.assign(ws.data, { pending });
|
|
591
604
|
}
|
|
@@ -605,8 +618,15 @@ export class AkanApp {
|
|
|
605
618
|
else ws.data.pending?.push(payload as string | ArrayBuffer);
|
|
606
619
|
}
|
|
607
620
|
|
|
621
|
+
static #sendableCloseCode(code: number): number | undefined {
|
|
622
|
+
const sendable = (code >= 1000 && code <= 1014 && (code < 1004 || code > 1006)) || (code >= 3000 && code <= 4999);
|
|
623
|
+
return sendable ? code : undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
608
626
|
#handleWsClose(ws: Bun.ServerWebSocket<GatewayWsData>, code: number, reason: string) {
|
|
609
|
-
|
|
627
|
+
const upstreamCode = AkanApp.#sendableCloseCode(code);
|
|
628
|
+
if (upstreamCode === undefined) ws.data.upstream.close();
|
|
629
|
+
else ws.data.upstream.close(upstreamCode, reason);
|
|
610
630
|
const child = this.#children.get(ws.data.childIdx);
|
|
611
631
|
if (child) child.metrics.activeWebSockets = Math.max(0, (child.metrics.activeWebSockets ?? 1) - 1);
|
|
612
632
|
}
|
|
@@ -808,41 +828,17 @@ export class AkanApp {
|
|
|
808
828
|
const headers = new Headers({ "Content-Type": options.contentType });
|
|
809
829
|
if (options.cacheControl) headers.set("Cache-Control", options.cacheControl);
|
|
810
830
|
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
headers.set("Content-Length", String(gzipBytes.byteLength));
|
|
818
|
-
headers.set("Vary", "Accept-Encoding");
|
|
819
|
-
return new Response(this.#toArrayBuffer(gzipBytes), { headers });
|
|
820
|
-
}
|
|
831
|
+
const sidecar = await resolveEncodedSidecar(req, filePath, options.contentType);
|
|
832
|
+
if (sidecar) {
|
|
833
|
+
headers.set("Content-Encoding", sidecar.encoding);
|
|
834
|
+
headers.set("Content-Length", String(sidecar.bytes.byteLength));
|
|
835
|
+
headers.set("Vary", "Accept-Encoding");
|
|
836
|
+
return new Response(sidecar.bytes, { headers });
|
|
821
837
|
}
|
|
822
838
|
|
|
823
839
|
return new Response(Bun.file(filePath).stream(), { headers });
|
|
824
840
|
}
|
|
825
841
|
|
|
826
|
-
#acceptsGzip(req: Request): boolean {
|
|
827
|
-
const acceptEncoding = req.headers.get("accept-encoding") ?? "";
|
|
828
|
-
return /\bgzip\b/.test(acceptEncoding);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
#isCompressible(contentType: string): boolean {
|
|
832
|
-
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
833
|
-
return (
|
|
834
|
-
type.startsWith("text/") ||
|
|
835
|
-
type === "application/javascript" ||
|
|
836
|
-
type === "application/json" ||
|
|
837
|
-
type === "application/manifest+json" ||
|
|
838
|
-
type === "image/svg+xml"
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
#toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
843
|
-
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
842
|
#safeResolve(baseDir: string, urlPath: string): string | null {
|
|
847
843
|
let decoded: string;
|
|
848
844
|
try {
|
|
@@ -1240,7 +1236,7 @@ export class AkanApp {
|
|
|
1240
1236
|
|
|
1241
1237
|
#writeChildOutputLineRaw(idx: number, role: AkanChildRole, type: "stdout" | "stderr", line: string) {
|
|
1242
1238
|
const prefixedLine = `[child:${idx} ${role}] [${type}] ${line}`;
|
|
1243
|
-
process[type].write(prefixedLine);
|
|
1239
|
+
if (type === "stdout" || this.#printChildStderr) process[type].write(prefixedLine);
|
|
1244
1240
|
this.#logWriter?.write(`${idx}-${role}`, AkanApp.#stripAnsi(prefixedLine));
|
|
1245
1241
|
}
|
|
1246
1242
|
|
package/server/akanOption.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { BackendEnv, PromiseOrObject } from "akanjs/base";
|
|
2
|
+
import type { Adaptor, AdaptorCls, LlmOption } from "akanjs/service";
|
|
3
|
+
import type { AgentRelayPolicy, MiddlewareCls } from "akanjs/signal";
|
|
4
|
+
import type { McpServerOption } from "./akanServer";
|
|
3
5
|
import type { WebProxyRegistration } from "./proxy";
|
|
4
6
|
import { HostBasePathWebProxy, LocaleWebProxy } from "./proxy";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
export interface AdaptorOverride {
|
|
9
|
+
role: AdaptorCls;
|
|
10
|
+
adaptor: AdaptorCls;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* App/library server option builder: use objects, signal middleware, adaptor overrides, web proxies, and the
|
|
15
|
+
* server settings an app owns — MCP, the agent relay's access policy, and the LLM the relay speaks to.
|
|
16
|
+
*/
|
|
17
|
+
export class AkanOption<Env = unknown> {
|
|
8
18
|
readonly #getUses: ((env: Env) => Record<string, PromiseOrObject<unknown>>)[];
|
|
9
19
|
readonly #middlewares: MiddlewareCls[] = [];
|
|
20
|
+
readonly #adaptorOverrides: AdaptorOverride[] = [];
|
|
10
21
|
readonly #webProxies: WebProxyRegistration[] = [];
|
|
22
|
+
readonly #getLlms: ((env: Env) => LlmOption)[] = [];
|
|
23
|
+
#mcp: boolean | McpServerOption | undefined;
|
|
24
|
+
#agentAccess: AgentRelayPolicy | null | undefined;
|
|
11
25
|
constructor() {
|
|
12
26
|
this.#getUses = [];
|
|
13
27
|
}
|
|
14
|
-
use(
|
|
28
|
+
use(
|
|
29
|
+
fnOrObject:
|
|
30
|
+
| ((env: Env & BackendEnv) => Record<string, PromiseOrObject<unknown>>)
|
|
31
|
+
| Record<string, PromiseOrObject<unknown>>,
|
|
32
|
+
) {
|
|
15
33
|
if (typeof fnOrObject === "function")
|
|
16
34
|
this.#getUses.push(fnOrObject as (env: Env) => Record<string, PromiseOrObject<unknown>>);
|
|
17
35
|
else this.#getUses.push(() => fnOrObject);
|
|
@@ -21,22 +39,62 @@ export class AkanOption<Env extends BaseEnv = BaseEnv> {
|
|
|
21
39
|
this.#middlewares.push(...middlewares);
|
|
22
40
|
return this;
|
|
23
41
|
}
|
|
42
|
+
/** Rebinds a predefined adaptor role (e.g. `LlmAdaptorRole`) to the app's own implementation. Last writer wins. */
|
|
43
|
+
applyAdaptor<T extends Adaptor>(role: AdaptorCls<T>, adaptor: AdaptorCls<T>) {
|
|
44
|
+
this.#adaptorOverrides.push({ role: role as AdaptorCls, adaptor: adaptor as AdaptorCls });
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
24
47
|
applyWebProxy(...proxies: WebProxyRegistration[]) {
|
|
25
48
|
this.#webProxies.push(...proxies);
|
|
26
49
|
return this;
|
|
27
50
|
}
|
|
28
|
-
|
|
51
|
+
/**
|
|
52
|
+
* MCP server settings for the app mounting this option, merged over the `AKAN_MCP_*` environment and under an
|
|
53
|
+
* option the server is constructed with. The app's own `option.ts` is the last lib the server reads, so it wins
|
|
54
|
+
* over every library it depends on.
|
|
55
|
+
*/
|
|
56
|
+
setMcp(mcp: boolean | McpServerOption = true) {
|
|
57
|
+
this.#mcp = mcp;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Who may spend the LLM key through the `runAgentTurn` relay — the `AgentRelayAccess` policy, which defaults to
|
|
62
|
+
* allowing everyone because the framework has no account model to gate on. `null` reopens it.
|
|
63
|
+
*/
|
|
64
|
+
setAgentAccess(policy: AgentRelayPolicy | null) {
|
|
65
|
+
this.#agentAccess = policy;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
/** Settings for whichever adaptor fills `LlmAdaptorRole`, injected into it as the `llmOption` use. */
|
|
69
|
+
setLlm(llmOrFn: LlmOption | ((env: Env) => LlmOption)) {
|
|
70
|
+
if (typeof llmOrFn === "function") this.#getLlms.push(llmOrFn);
|
|
71
|
+
else this.#getLlms.push(() => llmOrFn);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
getUses(env: Env & BackendEnv): Record<string, PromiseOrObject<unknown>> {
|
|
29
75
|
const uses = this.#getUses.map((fn) => fn(env));
|
|
30
76
|
return Object.assign({}, ...uses);
|
|
31
77
|
}
|
|
32
78
|
getMiddlewares(): MiddlewareCls[] {
|
|
33
79
|
return this.#middlewares;
|
|
34
80
|
}
|
|
81
|
+
getAdaptorOverrides(): AdaptorOverride[] {
|
|
82
|
+
return this.#adaptorOverrides;
|
|
83
|
+
}
|
|
35
84
|
getWebProxies(): WebProxyRegistration[] {
|
|
36
85
|
return this.#webProxies;
|
|
37
86
|
}
|
|
87
|
+
getMcp(): boolean | McpServerOption | undefined {
|
|
88
|
+
return this.#mcp;
|
|
89
|
+
}
|
|
90
|
+
getAgentAccess(): AgentRelayPolicy | null | undefined {
|
|
91
|
+
return this.#agentAccess;
|
|
92
|
+
}
|
|
93
|
+
getLlm(env: Env & BackendEnv): LlmOption {
|
|
94
|
+
return Object.assign({}, ...this.#getLlms.map((fn) => fn(env)));
|
|
95
|
+
}
|
|
38
96
|
}
|
|
39
97
|
|
|
40
98
|
export function createDefaultAkanOption() {
|
|
41
|
-
return new AkanOption
|
|
99
|
+
return new AkanOption().applyWebProxy(LocaleWebProxy, HostBasePathWebProxy);
|
|
42
100
|
}
|
package/server/akanServer.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BaseEnv, getEnv } from "akanjs/base";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
|
+
import { DictionaryLookup } from "akanjs/dictionary";
|
|
3
4
|
import type {
|
|
4
5
|
Adaptor,
|
|
5
6
|
AdaptorCls,
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
SolidConfig,
|
|
11
12
|
} from "akanjs/service";
|
|
12
13
|
import type { ServerSignal, ServerSignalCls, WebsocketPublishData } from "akanjs/signal";
|
|
14
|
+
import { AgentRelayAccess } from "../signal/guards";
|
|
13
15
|
import { createOpenApiDocument } from "../signal/openapi";
|
|
14
16
|
import { FetchSerializer } from "../signal/serializer";
|
|
15
17
|
import type { AkanLib, AkanLibProps } from "./akanLib";
|
|
@@ -19,6 +21,7 @@ import { DiLifecycle } from "./di/diLifecycle";
|
|
|
19
21
|
import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
|
|
20
22
|
import { isPortInUseError } from "./lifecycle/portInUse";
|
|
21
23
|
import { ShutdownManager } from "./lifecycle/shutdownManager";
|
|
24
|
+
import { type McpAuthOption, McpRouter } from "./mcp";
|
|
22
25
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
23
26
|
import { WebProxyRunner } from "./proxy";
|
|
24
27
|
import { SignalResolver } from "./resolver";
|
|
@@ -36,6 +39,44 @@ export interface AkanServerProps extends AkanLibProps {
|
|
|
36
39
|
|
|
37
40
|
export interface AkanServerOptions {
|
|
38
41
|
openapi?: boolean;
|
|
42
|
+
/** `/mcp` is mounted by default; `false` takes it off, and the object form carries the rest of its settings. */
|
|
43
|
+
mcp?: boolean | McpServerOption;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface McpServerOption {
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Drops every mutation from the catalogue whatever its guards allow — for a deployment that must not be able to
|
|
50
|
+
* write, such as a read replica or a demo. Off by default, and reported per endpoint in the boot log, because a
|
|
51
|
+
* switch that silently unlists a published endpoint cannot tell its author why it vanished.
|
|
52
|
+
*/
|
|
53
|
+
readOnly?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Mount path, `/mcp` by default. The published OAuth resource identifier follows it, so changing it changes
|
|
56
|
+
* the `aud` a token has to carry.
|
|
57
|
+
*/
|
|
58
|
+
path?: string;
|
|
59
|
+
/** Reported as `serverInfo.version`. Defaults to `0.0.0`, the same placeholder the OpenAPI document uses. */
|
|
60
|
+
version?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Free-text usage guidance handed to the model alongside the tool list — the one place to say what this app
|
|
63
|
+
* is for and which tool to reach for first. Defaults to a bare one-liner naming the app.
|
|
64
|
+
*/
|
|
65
|
+
instructions?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Extra origins allowed past the DNS-rebinding check, beyond the server's own host. Needed only for a
|
|
68
|
+
* browser-hosted MCP client; native ones send no `Origin` at all.
|
|
69
|
+
*/
|
|
70
|
+
allowedOrigins?: string[];
|
|
71
|
+
/** Entries per catalogue page. A client that wants the whole list follows `nextCursor` until it stops. */
|
|
72
|
+
pageSize?: number;
|
|
73
|
+
/**
|
|
74
|
+
* The one language the catalogue and its error text are written in, `en` by default. Server-wide on purpose:
|
|
75
|
+
* the document is built once at boot and cached by clients, and it is read by a model rather than by a person.
|
|
76
|
+
*/
|
|
77
|
+
language?: string;
|
|
78
|
+
/** OAuth resource-server identity: which issuers a client may authenticate with, and the scopes to demand. */
|
|
79
|
+
auth?: McpAuthOption;
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
interface AkanAppPrepared {
|
|
@@ -76,6 +117,10 @@ export class AkanServer {
|
|
|
76
117
|
prefix = "/api";
|
|
77
118
|
websocketPrefix = "/ws";
|
|
78
119
|
openapi = AkanServer.#isOpenApiEnvEnabled();
|
|
120
|
+
mcp = AkanServer.#isEnvOn("AKAN_MCP", "AKAN_PUBLIC_MCP");
|
|
121
|
+
mcpReadOnly = AkanServer.#isEnvEnabled("AKAN_MCP_READONLY", "AKAN_PUBLIC_MCP_READONLY");
|
|
122
|
+
mcpAuth: McpAuthOption = AkanServer.#mcpAuthFromEnv();
|
|
123
|
+
mcpOption: Omit<McpServerOption, "enabled" | "readOnly" | "auth"> = AkanServer.#mcpOptionFromEnv();
|
|
79
124
|
serverMode: "federation" | "batch" | "all";
|
|
80
125
|
shutdownTimeoutMs = AkanServer.#defaultShutdownTimeoutMs();
|
|
81
126
|
|
|
@@ -84,7 +129,7 @@ export class AkanServer {
|
|
|
84
129
|
#metricsTimer: Timer | null = null;
|
|
85
130
|
constructor(
|
|
86
131
|
name = "AkanServer",
|
|
87
|
-
env
|
|
132
|
+
env = {},
|
|
88
133
|
serverMode: "federation" | "batch" | "all" = (process.env.SERVER_MODE as
|
|
89
134
|
| "federation"
|
|
90
135
|
| "batch"
|
|
@@ -96,8 +141,16 @@ export class AkanServer {
|
|
|
96
141
|
this.name = name;
|
|
97
142
|
this.logger = new Logger(name);
|
|
98
143
|
this.libs = libs;
|
|
99
|
-
this.env =
|
|
144
|
+
this.env = Object.assign(getEnv(), env);
|
|
100
145
|
this.openapi = options?.openapi ?? this.openapi;
|
|
146
|
+
|
|
147
|
+
libs.forEach((lib) => {
|
|
148
|
+
const mcp = lib.option.getMcp();
|
|
149
|
+
if (mcp !== undefined) this.setMcp(mcp);
|
|
150
|
+
const agentAccess = lib.option.getAgentAccess();
|
|
151
|
+
if (agentAccess !== undefined) AgentRelayAccess.use(agentAccess);
|
|
152
|
+
});
|
|
153
|
+
this.setMcp(options?.mcp ?? this.mcp);
|
|
101
154
|
this.serverMode = serverMode;
|
|
102
155
|
this.#di = new DiLifecycle(this.env, serverMode, ...libs);
|
|
103
156
|
}
|
|
@@ -114,6 +167,16 @@ export class AkanServer {
|
|
|
114
167
|
this.openapi = openapi;
|
|
115
168
|
return this;
|
|
116
169
|
}
|
|
170
|
+
setMcp(mcp: boolean | McpServerOption = true) {
|
|
171
|
+
if (this.status !== "stopped") throw new Error("MCP config must be set before app initialization.");
|
|
172
|
+
this.mcp = typeof mcp === "boolean" ? mcp : (mcp.enabled ?? true);
|
|
173
|
+
if (typeof mcp === "boolean") return this;
|
|
174
|
+
const { enabled: _enabled, readOnly, auth, ...rest } = mcp;
|
|
175
|
+
if (readOnly !== undefined) this.mcpReadOnly = readOnly;
|
|
176
|
+
if (auth) this.mcpAuth = { ...this.mcpAuth, ...AkanServer.#defined(auth) };
|
|
177
|
+
this.mcpOption = { ...this.mcpOption, ...AkanServer.#defined(rest) };
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
117
180
|
setDatabaseConfig(database: DatabaseConfig) {
|
|
118
181
|
if (this.status !== "stopped") throw new Error("Database config must be set before app initialization.");
|
|
119
182
|
this.env.database = database;
|
|
@@ -460,12 +523,30 @@ export class AkanServer {
|
|
|
460
523
|
title: `${this.env.appName} API`,
|
|
461
524
|
version: "0.0.0",
|
|
462
525
|
servers: this.#getOpenApiServers(),
|
|
526
|
+
resolveDescription: AkanServer.#createDescriptionResolver(),
|
|
463
527
|
}),
|
|
464
528
|
),
|
|
465
529
|
},
|
|
466
530
|
}
|
|
467
531
|
: {};
|
|
468
532
|
|
|
533
|
+
const mcpRouter =
|
|
534
|
+
this.mcp && this.serverMode !== "batch"
|
|
535
|
+
? new McpRouter({
|
|
536
|
+
registry: this.#di.registry,
|
|
537
|
+
env: this.env,
|
|
538
|
+
live: this.#di.live,
|
|
539
|
+
middleware: new Map(this.#di.modules.middleware),
|
|
540
|
+
instructions: `Domain tools for the ${this.env.appName} app.`,
|
|
541
|
+
...this.mcpOption,
|
|
542
|
+
readOnly: this.mcpReadOnly,
|
|
543
|
+
auth: this.mcpAuth,
|
|
544
|
+
})
|
|
545
|
+
: null;
|
|
546
|
+
const mcpRoutes: HttpRoutes = mcpRouter?.createRoutes() ?? {};
|
|
547
|
+
|
|
548
|
+
mcpRouter?.report();
|
|
549
|
+
|
|
469
550
|
const devtoolsRoutes = new DevtoolsRouter({
|
|
470
551
|
di: this.#di,
|
|
471
552
|
env: this.env,
|
|
@@ -476,7 +557,17 @@ export class AkanServer {
|
|
|
476
557
|
openapi: this.openapi,
|
|
477
558
|
getStatus: () => this.status,
|
|
478
559
|
}).createRoutes();
|
|
479
|
-
return { ...openapiRoutes, ...devtoolsRoutes };
|
|
560
|
+
return { ...openapiRoutes, ...mcpRoutes, ...devtoolsRoutes };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Rebuilt per request rather than cached on the instance: libs register their dictionaries at module-evaluation
|
|
565
|
+
* time, and the document itself is already re-serialized per request, so a stale snapshot would be the only
|
|
566
|
+
* thing here that could disagree with the rest of the response.
|
|
567
|
+
*/
|
|
568
|
+
static #createDescriptionResolver() {
|
|
569
|
+
const lookup = new DictionaryLookup();
|
|
570
|
+
return (key: string) => lookup.text(key);
|
|
480
571
|
}
|
|
481
572
|
|
|
482
573
|
#getOpenApiServers() {
|
|
@@ -494,14 +585,88 @@ export class AkanServer {
|
|
|
494
585
|
|
|
495
586
|
static #isServerOptions(value: AkanLib | AkanServerOptions | undefined): value is AkanServerOptions {
|
|
496
587
|
return Boolean(
|
|
497
|
-
value &&
|
|
588
|
+
value &&
|
|
589
|
+
!("database" in value) &&
|
|
590
|
+
!("service" in value) &&
|
|
591
|
+
!("scalar" in value) &&
|
|
592
|
+
("openapi" in value || "mcp" in value),
|
|
498
593
|
);
|
|
499
594
|
}
|
|
500
595
|
|
|
501
596
|
static #isOpenApiEnvEnabled() {
|
|
502
|
-
return
|
|
503
|
-
|
|
504
|
-
|
|
597
|
+
return AkanServer.#isEnvEnabled("AKAN_OPENAPI", "AKAN_PUBLIC_OPENAPI");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
static #isEnvEnabled(...names: string[]) {
|
|
601
|
+
return names.some((name) => process.env[name] === "true" || process.env[name] === "1");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* On unless the env says otherwise, which is the opposite of `#isEnvEnabled`.
|
|
606
|
+
*
|
|
607
|
+
* MCP exposure follows an endpoint's guards rather than an opt-in, so there is nothing an app has to declare for
|
|
608
|
+
* its catalogue to be right — and a switch that must be found before anything works is a switch most deployments
|
|
609
|
+
* never find. `AKAN_MCP=false` is the way off.
|
|
610
|
+
*/
|
|
611
|
+
static #isEnvOn(...names: string[]) {
|
|
612
|
+
return !names.some((name) => process.env[name] === "false" || process.env[name] === "0");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Both differ per environment, so they belong in env rather than in the app's source alongside the switch. */
|
|
616
|
+
static #mcpAuthFromEnv(): McpAuthOption {
|
|
617
|
+
const authorizationServers = AkanServer.#envList("AKAN_MCP_AUTH_SERVERS");
|
|
618
|
+
const scopes = AkanServer.#envList("AKAN_MCP_SCOPES");
|
|
619
|
+
return {
|
|
620
|
+
...(authorizationServers?.length ? { authorizationServers } : {}),
|
|
621
|
+
...(scopes?.length ? { scopes } : {}),
|
|
622
|
+
...(process.env.AKAN_MCP_RESOURCE ? { resource: process.env.AKAN_MCP_RESOURCE } : {}),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The rest of `McpServerOption`, spelled as environment — the channel a deployment configures what the source
|
|
628
|
+
* does not through, and the only one a child of the gateway is handed. An app writes these in its `option.ts`,
|
|
629
|
+
* which merges over this: a value written in code wins over the env of the same name.
|
|
630
|
+
*/
|
|
631
|
+
/**
|
|
632
|
+
* Keys whose value is `undefined` are dropped before the merge. "An option written in code wins over the env of
|
|
633
|
+
* the same name" is the contract, and a spread reads an explicitly-`undefined` property as a value — so
|
|
634
|
+
* `{ path: undefined }` from a caller assembling options conditionally erased what the environment supplied.
|
|
635
|
+
*/
|
|
636
|
+
static #defined<T extends object>(source: T): Partial<T> {
|
|
637
|
+
return Object.fromEntries(Object.entries(source).filter(([, value]) => value !== undefined)) as Partial<T>;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
static #mcpOptionFromEnv(): Omit<McpServerOption, "enabled" | "readOnly" | "auth"> {
|
|
641
|
+
const allowedOrigins = AkanServer.#envList("AKAN_MCP_ALLOWED_ORIGINS");
|
|
642
|
+
const pageSize = Number(process.env.AKAN_MCP_PAGE_SIZE);
|
|
643
|
+
return {
|
|
644
|
+
...AkanServer.#mcpPathFromEnv(),
|
|
645
|
+
...(process.env.AKAN_MCP_VERSION ? { version: process.env.AKAN_MCP_VERSION } : {}),
|
|
646
|
+
...(process.env.AKAN_MCP_INSTRUCTIONS ? { instructions: process.env.AKAN_MCP_INSTRUCTIONS } : {}),
|
|
647
|
+
...(allowedOrigins?.length ? { allowedOrigins } : {}),
|
|
648
|
+
...(Number.isInteger(pageSize) && pageSize > 0 ? { pageSize } : {}),
|
|
649
|
+
...(process.env.AKAN_MCP_LANGUAGE ? { language: process.env.AKAN_MCP_LANGUAGE } : {}),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Both consumers build their path by concatenation — the route key, and the RFC 9728 metadata path the resource
|
|
655
|
+
* identifier is inserted into — so `AKAN_MCP_PATH=mcp` served a route named `mcp` and published its metadata at
|
|
656
|
+
* `/.well-known/oauth-protected-resourcemcp`, which no client would ever look for. Normalized rather than
|
|
657
|
+
* rejected: the intent is unambiguous, and a server that refuses to boot over a missing slash is worse.
|
|
658
|
+
*/
|
|
659
|
+
static #mcpPathFromEnv() {
|
|
660
|
+
const path = process.env.AKAN_MCP_PATH?.trim();
|
|
661
|
+
if (!path) return {};
|
|
662
|
+
return { path: path.startsWith("/") ? path : `/${path}` };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
static #envList(name: string) {
|
|
666
|
+
return process.env[name]
|
|
667
|
+
?.split(",")
|
|
668
|
+
.map((entry) => entry.trim())
|
|
669
|
+
.filter(Boolean);
|
|
505
670
|
}
|
|
506
671
|
|
|
507
672
|
/**
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Bun.Image.metadata()` reports only `{ width, height, format }` — there is no frame count — and an
|
|
3
|
+
* animated GIF decodes to its first frame without erroring. So an optimizer that re-encodes whatever
|
|
4
|
+
* decodes would silently drop the animation. These readers answer the question from the container bytes
|
|
5
|
+
* instead, so an animated source can be passed through untouched.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Sub-blocks are a `[length][bytes]…[0]` chain, so a frame's payload cannot be skipped by size alone. */
|
|
9
|
+
const skipGifSubBlocks = (buffer: Buffer, start: number): number => {
|
|
10
|
+
let offset = start;
|
|
11
|
+
while (offset < buffer.length) {
|
|
12
|
+
const size = buffer[offset];
|
|
13
|
+
offset += 1;
|
|
14
|
+
if (size === undefined || size === 0) return offset;
|
|
15
|
+
offset += size;
|
|
16
|
+
}
|
|
17
|
+
return offset;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const colorTableSize = (packed: number): number => ((packed & 0x80) === 0 ? 0 : 3 * 2 ** ((packed & 0x07) + 1));
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Walks the GIF block stream to count image descriptors. Counting raw `0x2C` bytes would overcount —
|
|
24
|
+
* the byte occurs freely inside colour tables and LZW payloads — so every block has to be stepped over.
|
|
25
|
+
*/
|
|
26
|
+
const hasMultipleGifFrames = (buffer: Buffer): boolean => {
|
|
27
|
+
if (buffer.length < 13) return false;
|
|
28
|
+
let offset = 6;
|
|
29
|
+
offset += 7 + colorTableSize(buffer[offset + 4] ?? 0);
|
|
30
|
+
let frames = 0;
|
|
31
|
+
while (offset < buffer.length) {
|
|
32
|
+
const block = buffer[offset];
|
|
33
|
+
offset += 1;
|
|
34
|
+
if (block === 0x3b) break;
|
|
35
|
+
if (block === 0x21) {
|
|
36
|
+
offset = skipGifSubBlocks(buffer, offset + 1);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (block !== 0x2c) break;
|
|
40
|
+
frames += 1;
|
|
41
|
+
if (frames > 1) return true;
|
|
42
|
+
offset += 9 + colorTableSize(buffer[offset + 8] ?? 0);
|
|
43
|
+
offset = skipGifSubBlocks(buffer, offset + 1);
|
|
44
|
+
}
|
|
45
|
+
return frames > 1;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** An animated WebP is an extended (`VP8X`) file carrying an `ANIM` chunk; a still one never has it. */
|
|
49
|
+
const hasWebpAnimChunk = (buffer: Buffer): boolean => {
|
|
50
|
+
let offset = 12;
|
|
51
|
+
while (offset + 8 <= buffer.length) {
|
|
52
|
+
const id = buffer.toString("ascii", offset, offset + 4);
|
|
53
|
+
if (id === "ANIM") return true;
|
|
54
|
+
const size = buffer.readUInt32LE(offset + 4);
|
|
55
|
+
offset += 8 + size + (size % 2);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** APNG marks itself with an `acTL` chunk, which the spec requires before the first `IDAT`. */
|
|
61
|
+
const hasPngAnimationControl = (buffer: Buffer): boolean => {
|
|
62
|
+
let offset = 8;
|
|
63
|
+
while (offset + 8 <= buffer.length) {
|
|
64
|
+
const size = buffer.readUInt32BE(offset);
|
|
65
|
+
const id = buffer.toString("ascii", offset + 4, offset + 8);
|
|
66
|
+
if (id === "acTL") return true;
|
|
67
|
+
if (id === "IDAT" || id === "IEND") return false;
|
|
68
|
+
offset += 12 + size;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const isAnimatedImage = (buffer: Buffer, contentType: string): boolean => {
|
|
74
|
+
try {
|
|
75
|
+
if (contentType === "image/gif") return hasMultipleGifFrames(buffer);
|
|
76
|
+
if (contentType === "image/webp") return hasWebpAnimChunk(buffer);
|
|
77
|
+
if (contentType === "image/png") return hasPngAnimationControl(buffer);
|
|
78
|
+
return false;
|
|
79
|
+
} catch {
|
|
80
|
+
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
@@ -48,12 +48,22 @@ export class RouteClientCache {
|
|
|
48
48
|
};
|
|
49
49
|
readonly #buildRoute: RouteBuildFn;
|
|
50
50
|
readonly #onMerge?: OnMergeFn;
|
|
51
|
+
#revision = 0;
|
|
51
52
|
|
|
52
53
|
constructor({ buildRoute, onMerge }: RouteClientCacheOptions) {
|
|
53
54
|
this.#buildRoute = buildRoute;
|
|
54
55
|
this.#onMerge = onMerge;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Bumped on every mutation of `merged`, including a delta merge that leaves `generation` where it was. It lets a
|
|
60
|
+
* consumer memoize work derived from the manifest — merging the runtime manifest over it, say — without having to
|
|
61
|
+
* copy the manifest to find out whether anything changed. In production nothing after `seed` moves it at all.
|
|
62
|
+
*/
|
|
63
|
+
get revision(): number {
|
|
64
|
+
return this.#revision;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
#getEmptyDelta(): BuildRouteClientResult {
|
|
58
68
|
return {
|
|
59
69
|
manifestDelta: {},
|
|
@@ -75,6 +85,7 @@ export class RouteClientCache {
|
|
|
75
85
|
Object.assign(this.merged.ssrManifest.moduleMap, manifest.ssrManifest.moduleMap);
|
|
76
86
|
for (const abs of manifest.knownEntries) this.merged.knownEntries.add(abs);
|
|
77
87
|
for (const routeId of manifest.routeIds) this.#built.set(routeId, this.#getEmptyDelta());
|
|
88
|
+
this.#revision += 1;
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
async ensure(routeId: string, seeds: string[]): Promise<MergedManifest> {
|
|
@@ -125,6 +136,7 @@ export class RouteClientCache {
|
|
|
125
136
|
for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap))
|
|
126
137
|
this.merged.ssrManifest.moduleMap[url] = byName;
|
|
127
138
|
for (const entry of delta.newEntries) this.merged.knownEntries.add(entry);
|
|
139
|
+
this.#revision += 1;
|
|
128
140
|
this.#built.set(routeId, delta);
|
|
129
141
|
this.#logger.verbose(
|
|
130
142
|
`[route-cache] build done routeId=${routeId} generation=${generation} entries=+${delta.newEntries.length} deps=${delta.clientDeps.length} in ${Date.now() - started}ms`,
|
|
@@ -176,6 +188,7 @@ export class RouteClientCache {
|
|
|
176
188
|
}
|
|
177
189
|
const nextGeneration = this.merged.generation + 1;
|
|
178
190
|
this.merged = this.#getEmptyMerged(nextGeneration);
|
|
191
|
+
this.#revision += 1;
|
|
179
192
|
this.#building.clear();
|
|
180
193
|
this.#logger.verbose(`[route-cache] cleared generation=${nextGeneration} dropped=${dropped.length}`);
|
|
181
194
|
return dropped;
|
|
@@ -209,6 +222,7 @@ export class RouteClientCache {
|
|
|
209
222
|
),
|
|
210
223
|
};
|
|
211
224
|
this.merged = next;
|
|
225
|
+
this.#revision += 1;
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
static #normalizePath(filePath: string): string {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const COMPRESSIBLE_TYPES = new Set([
|
|
2
|
+
"application/javascript",
|
|
3
|
+
"application/json",
|
|
4
|
+
"application/manifest+json",
|
|
5
|
+
"image/svg+xml",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* br is tried first: it is ~15% smaller than gzip across the artifact and ~22% on the CSS bundle.
|
|
10
|
+
* The gzip sidecar stays the fallback because browsers only advertise `br` on secure origins, so a
|
|
11
|
+
* plain-http dev server or an intermediary that rewrites Accept-Encoding still gets a compressed body.
|
|
12
|
+
*/
|
|
13
|
+
const SIDECAR_ENCODINGS = [
|
|
14
|
+
{ encoding: "br", ext: ".br", accept: /(?:^|,)\s*(?:br|\*)(?![\w-])\s*(?:;\s*q=([\d.]+))?/i },
|
|
15
|
+
{ encoding: "gzip", ext: ".gz", accept: /(?:^|,)\s*(?:gzip|\*)(?![\w-])\s*(?:;\s*q=([\d.]+))?/i },
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export interface EncodedSidecar {
|
|
19
|
+
bytes: ArrayBuffer;
|
|
20
|
+
encoding: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const isCompressibleContentType = (contentType: string): boolean => {
|
|
24
|
+
const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
25
|
+
return type.startsWith("text/") || COMPRESSIBLE_TYPES.has(type);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Picks the best precompressed sidecar the caller accepts, or null to serve the file as-is. */
|
|
29
|
+
export const resolveEncodedSidecar = async (
|
|
30
|
+
req: Request,
|
|
31
|
+
filePath: string,
|
|
32
|
+
contentType: string,
|
|
33
|
+
): Promise<EncodedSidecar | null> => {
|
|
34
|
+
if (!isCompressibleContentType(contentType)) return null;
|
|
35
|
+
const acceptEncoding = req.headers.get("accept-encoding") ?? "";
|
|
36
|
+
for (const { encoding, ext, accept } of SIDECAR_ENCODINGS) {
|
|
37
|
+
const match = accept.exec(acceptEncoding);
|
|
38
|
+
|
|
39
|
+
if (!match || (match[1] !== undefined && Number.parseFloat(match[1]) <= 0)) continue;
|
|
40
|
+
const file = Bun.file(`${filePath}${ext}`);
|
|
41
|
+
if (!(await file.exists())) continue;
|
|
42
|
+
const bytes = await file.bytes();
|
|
43
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
44
|
+
return { bytes: buffer, encoding };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
};
|