akanjs 3.1.1 → 3.1.2-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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/server/imageOptimizer.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import { isIP } from "node:net";
|
|
3
5
|
import path from "node:path";
|
|
4
|
-
import { Logger } from "akanjs/common";
|
|
6
|
+
import { Logger, TrustedProxy } from "akanjs/common";
|
|
5
7
|
import { isAnimatedImage } from "./animatedImage";
|
|
6
8
|
import { matchesIfNoneMatch } from "./contentEncoding";
|
|
7
9
|
import { ImageOptimizerError } from "./imageOptimizerError";
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
type AkanImagePattern,
|
|
14
16
|
getAkanImageWidths,
|
|
15
17
|
mergeAkanImageConfig,
|
|
18
|
+
type ResolvedAkanImageConfig,
|
|
16
19
|
} from "./types";
|
|
17
20
|
|
|
18
21
|
export interface ImageOptimizerOptions {
|
|
@@ -20,6 +23,7 @@ export interface ImageOptimizerOptions {
|
|
|
20
23
|
cacheDir: string;
|
|
21
24
|
prodMode: boolean;
|
|
22
25
|
config?: Partial<AkanImageConfig>;
|
|
26
|
+
resolveHost?: (hostname: string) => Promise<{ address: string }[]>;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
interface ParsedImageRequest {
|
|
@@ -67,12 +71,17 @@ export class ImageOptimizer {
|
|
|
67
71
|
#publicDir: string;
|
|
68
72
|
#cacheDir: string;
|
|
69
73
|
#prodMode: boolean;
|
|
70
|
-
#config:
|
|
74
|
+
#config: ResolvedAkanImageConfig;
|
|
71
75
|
#semaphore: Semaphore;
|
|
76
|
+
readonly #resolveHost: (hostname: string) => Promise<{ address: string }[]>;
|
|
77
|
+
readonly #encodeSlots: number;
|
|
78
|
+
#encoding = 0;
|
|
79
|
+
#cacheBytes: number | null = null;
|
|
80
|
+
#pruning: Promise<void> | null = null;
|
|
72
81
|
readonly #inflight = new Map<string, Promise<OptimizedImage>>();
|
|
73
82
|
#logger = new Logger("ImageOptimizer");
|
|
74
83
|
|
|
75
|
-
constructor({ publicDir, cacheDir, prodMode, config }: ImageOptimizerOptions) {
|
|
84
|
+
constructor({ publicDir, cacheDir, prodMode, config, resolveHost }: ImageOptimizerOptions) {
|
|
76
85
|
this.#publicDir = publicDir;
|
|
77
86
|
this.#cacheDir = cacheDir;
|
|
78
87
|
this.#prodMode = prodMode;
|
|
@@ -83,7 +92,18 @@ export class ImageOptimizer {
|
|
|
83
92
|
this.#logger.warn(`${dropped.join(", ")} needs an OS codec this platform does not have; serving webp instead`);
|
|
84
93
|
}
|
|
85
94
|
this.#config = { ...merged, formats: formats.length ? formats : ["image/webp"] };
|
|
86
|
-
|
|
95
|
+
const concurrency = ImageOptimizer.#resolveConcurrency(this.#config.maxConcurrency);
|
|
96
|
+
this.#semaphore = new Semaphore(concurrency);
|
|
97
|
+
this.#encodeSlots = this.#config.maxQueue > 0 ? concurrency + this.#config.maxQueue : Number.POSITIVE_INFINITY;
|
|
98
|
+
this.#resolveHost =
|
|
99
|
+
resolveHost ??
|
|
100
|
+
(async (hostname) =>
|
|
101
|
+
await Promise.race([
|
|
102
|
+
lookup(hostname, { all: true }),
|
|
103
|
+
new Promise<never>((_resolve, reject) =>
|
|
104
|
+
setTimeout(() => reject(new Error("DNS lookup timed out")), this.#config.fetchTimeoutMs).unref(),
|
|
105
|
+
),
|
|
106
|
+
]));
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
static #resolveConcurrency(configured: number): number {
|
|
@@ -105,7 +125,7 @@ export class ImageOptimizer {
|
|
|
105
125
|
} catch (error) {
|
|
106
126
|
const message = error instanceof ImageOptimizerError ? error.message : "Unable to optimize image";
|
|
107
127
|
const status = error instanceof ImageOptimizerError ? error.status : 500;
|
|
108
|
-
return new Response(message, { status });
|
|
128
|
+
return new Response(message, { status, headers: status === 503 ? { "retry-after": "1" } : undefined });
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
|
|
@@ -145,9 +165,8 @@ export class ImageOptimizer {
|
|
|
145
165
|
return null;
|
|
146
166
|
}
|
|
147
167
|
if (!pointer.file || pointer.expireAt <= Date.now()) return null;
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
const buffer = Buffer.from(await file.arrayBuffer());
|
|
168
|
+
const buffer = await ImageOptimizer.#readCached(pointer.file);
|
|
169
|
+
if (!buffer) return null;
|
|
151
170
|
return {
|
|
152
171
|
buffer,
|
|
153
172
|
contentType: pointer.contentType,
|
|
@@ -165,7 +184,7 @@ export class ImageOptimizer {
|
|
|
165
184
|
maxAge: optimized.maxAge,
|
|
166
185
|
expireAt: Date.now() + optimized.maxAge * 1000,
|
|
167
186
|
};
|
|
168
|
-
await
|
|
187
|
+
await this.#writeCache(this.#getPointerPath(parsed), JSON.stringify(pointer));
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
#getPointerPath(parsed: ParsedImageRequest) {
|
|
@@ -191,7 +210,7 @@ export class ImageOptimizer {
|
|
|
191
210
|
let href: string;
|
|
192
211
|
let isRemote = false;
|
|
193
212
|
if (url.startsWith("/")) {
|
|
194
|
-
href = url;
|
|
213
|
+
href = ImageOptimizer.#localHref(this.#config.localPatterns, url);
|
|
195
214
|
const pathname = ImageOptimizer.#safeDecodePathname(url);
|
|
196
215
|
if (!pathname) return { error: '"url" parameter is invalid' };
|
|
197
216
|
if (pathname === ImageOptimizer.#optimizerPath || pathname.startsWith(`${ImageOptimizer.#optimizerPath}/`)) {
|
|
@@ -263,6 +282,7 @@ export class ImageOptimizer {
|
|
|
263
282
|
if (!ImageOptimizer.#hasRemoteMatch(this.#config.remotePatterns, current)) {
|
|
264
283
|
throw new ImageOptimizerError(400, '"url" parameter is not allowed');
|
|
265
284
|
}
|
|
285
|
+
await this.#assertPublicHost(current);
|
|
266
286
|
const controller = new AbortController();
|
|
267
287
|
const timeout = setTimeout(() => controller.abort(), this.#config.fetchTimeoutMs);
|
|
268
288
|
try {
|
|
@@ -277,7 +297,7 @@ export class ImageOptimizer {
|
|
|
277
297
|
current = new URL(location, current);
|
|
278
298
|
continue;
|
|
279
299
|
}
|
|
280
|
-
if (!res.ok) throw new ImageOptimizerError(
|
|
300
|
+
if (!res.ok) throw new ImageOptimizerError(502, "Remote image request failed");
|
|
281
301
|
const buffer = await ImageOptimizer.#readResponseBuffer(res, this.#config.maxRemoteBytes);
|
|
282
302
|
return {
|
|
283
303
|
buffer,
|
|
@@ -293,6 +313,22 @@ export class ImageOptimizer {
|
|
|
293
313
|
throw new ImageOptimizerError(400, "Remote image exceeded maximum redirects");
|
|
294
314
|
}
|
|
295
315
|
|
|
316
|
+
async #assertPublicHost(url: URL) {
|
|
317
|
+
if (this.#config.dangerouslyAllowLocalIP) return;
|
|
318
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
319
|
+
let addresses: string[];
|
|
320
|
+
if (isIP(host)) addresses = [host];
|
|
321
|
+
else {
|
|
322
|
+
try {
|
|
323
|
+
addresses = (await this.#resolveHost(host)).map(({ address }) => address);
|
|
324
|
+
} catch {
|
|
325
|
+
throw new ImageOptimizerError(502, "Remote image request failed");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!addresses.length || addresses.some((address) => TrustedProxy.isPrivateAddress(address)))
|
|
329
|
+
throw new ImageOptimizerError(400, '"url" parameter is not allowed');
|
|
330
|
+
}
|
|
331
|
+
|
|
296
332
|
async #getOptimizedImage(source: ImageSource, params: ParsedImageRequest): Promise<OptimizedImage> {
|
|
297
333
|
const inputType = ImageOptimizer.#detectContentType(source.buffer) || source.contentType;
|
|
298
334
|
if (!inputType.startsWith("image/") || inputType.includes(",")) {
|
|
@@ -320,13 +356,12 @@ export class ImageOptimizer {
|
|
|
320
356
|
outputType,
|
|
321
357
|
upstreamTag: source.upstreamTag,
|
|
322
358
|
});
|
|
323
|
-
const cached =
|
|
324
|
-
if (
|
|
325
|
-
const buffer = Buffer.from(await cached.arrayBuffer());
|
|
359
|
+
const cached = await ImageOptimizer.#readCached(cachePath);
|
|
360
|
+
if (cached) {
|
|
326
361
|
return {
|
|
327
|
-
buffer,
|
|
362
|
+
buffer: cached,
|
|
328
363
|
contentType: outputType,
|
|
329
|
-
etag: ImageOptimizer.#hashBuffer(
|
|
364
|
+
etag: ImageOptimizer.#hashBuffer(cached),
|
|
330
365
|
maxAge,
|
|
331
366
|
cacheFile: cachePath,
|
|
332
367
|
};
|
|
@@ -336,12 +371,15 @@ export class ImageOptimizer {
|
|
|
336
371
|
let contentType = inputType;
|
|
337
372
|
let cacheable = true;
|
|
338
373
|
if (shouldEncode) {
|
|
374
|
+
if (this.#encoding >= this.#encodeSlots) throw new ImageOptimizerError(503, "Image optimizer is busy");
|
|
375
|
+
this.#encoding += 1;
|
|
339
376
|
try {
|
|
340
377
|
buffer = await this.#semaphore.run(() =>
|
|
341
378
|
ImageOptimizer.#optimize(source.buffer, {
|
|
342
379
|
width: params.width,
|
|
343
380
|
quality: params.quality,
|
|
344
381
|
contentType: outputType,
|
|
382
|
+
maxPixels: this.#config.maxInputPixels,
|
|
345
383
|
}),
|
|
346
384
|
);
|
|
347
385
|
contentType = outputType;
|
|
@@ -349,13 +387,12 @@ export class ImageOptimizer {
|
|
|
349
387
|
buffer = source.buffer;
|
|
350
388
|
contentType = inputType;
|
|
351
389
|
cacheable = false;
|
|
390
|
+
} finally {
|
|
391
|
+
this.#encoding -= 1;
|
|
352
392
|
}
|
|
353
393
|
}
|
|
354
394
|
|
|
355
|
-
if (cacheable)
|
|
356
|
-
await fs.mkdir(this.#cacheDir, { recursive: true });
|
|
357
|
-
await Bun.write(cachePath, buffer);
|
|
358
|
-
}
|
|
395
|
+
if (cacheable) await this.#writeCache(cachePath, buffer);
|
|
359
396
|
return {
|
|
360
397
|
buffer,
|
|
361
398
|
contentType,
|
|
@@ -365,6 +402,51 @@ export class ImageOptimizer {
|
|
|
365
402
|
};
|
|
366
403
|
}
|
|
367
404
|
|
|
405
|
+
static async #readCached(cachePath: string): Promise<Buffer | null> {
|
|
406
|
+
try {
|
|
407
|
+
const file = Bun.file(cachePath);
|
|
408
|
+
return (await file.exists()) ? Buffer.from(await file.arrayBuffer()) : null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async #writeCache(file: string, data: Buffer | string) {
|
|
415
|
+
await fs.mkdir(this.#cacheDir, { recursive: true });
|
|
416
|
+
this.#cacheBytes ??= (await this.#listCache()).reduce((total, entry) => total + entry.size, 0);
|
|
417
|
+
await Bun.write(file, data);
|
|
418
|
+
this.#cacheBytes += Buffer.byteLength(data);
|
|
419
|
+
const ceiling = this.#config.maxCacheBytes;
|
|
420
|
+
if (ceiling <= 0 || this.#cacheBytes <= ceiling) return;
|
|
421
|
+
this.#pruning ??= this.#pruneCache(ceiling).finally(() => {
|
|
422
|
+
this.#pruning = null;
|
|
423
|
+
});
|
|
424
|
+
await this.#pruning;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async #listCache(): Promise<{ file: string; size: number; mtimeMs: number }[]> {
|
|
428
|
+
const names = await fs.readdir(this.#cacheDir).catch(() => [] as string[]);
|
|
429
|
+
const entries = await Promise.all(
|
|
430
|
+
names.map(async (name) => {
|
|
431
|
+
const file = path.join(this.#cacheDir, name);
|
|
432
|
+
const stat = await fs.stat(file).catch(() => null);
|
|
433
|
+
return stat?.isFile() ? { file, size: stat.size, mtimeMs: stat.mtimeMs } : null;
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
return entries.filter((entry) => entry !== null);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async #pruneCache(ceiling: number) {
|
|
440
|
+
const entries = (await this.#listCache()).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
441
|
+
let total = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
442
|
+
for (const entry of entries) {
|
|
443
|
+
if (total <= ceiling * 0.8) break;
|
|
444
|
+
await fs.rm(entry.file, { force: true });
|
|
445
|
+
total -= entry.size;
|
|
446
|
+
}
|
|
447
|
+
this.#cacheBytes = total;
|
|
448
|
+
}
|
|
449
|
+
|
|
368
450
|
#getCachePath(input: Record<string, string | number>) {
|
|
369
451
|
const contentType = String(input.outputType);
|
|
370
452
|
return path.join(
|
|
@@ -389,9 +471,11 @@ export class ImageOptimizer {
|
|
|
389
471
|
|
|
390
472
|
static async #optimize(
|
|
391
473
|
buffer: Buffer,
|
|
392
|
-
options: { width: number; quality: number; contentType: string },
|
|
474
|
+
options: { width: number; quality: number; contentType: string; maxPixels: number },
|
|
393
475
|
): Promise<Buffer> {
|
|
394
|
-
const image = new Bun.Image(buffer).resize(options.width, undefined, {
|
|
476
|
+
const image = new Bun.Image(buffer, { maxPixels: options.maxPixels }).resize(options.width, undefined, {
|
|
477
|
+
withoutEnlargement: true,
|
|
478
|
+
});
|
|
395
479
|
|
|
396
480
|
if (options.contentType === ImageOptimizer.#avif) return await image.avif({ quality: options.quality }).toBuffer();
|
|
397
481
|
if (options.contentType === ImageOptimizer.#webp) return await image.webp({ quality: options.quality }).toBuffer();
|
|
@@ -411,6 +495,17 @@ export class ImageOptimizer {
|
|
|
411
495
|
});
|
|
412
496
|
}
|
|
413
497
|
|
|
498
|
+
static #localHref(patterns: Pick<AkanImagePattern, "pathname" | "search">[], href: string): string {
|
|
499
|
+
const url = new URL(href, "http://local.akan");
|
|
500
|
+
const namesQuery = patterns.some(
|
|
501
|
+
(pattern) =>
|
|
502
|
+
pattern.search !== undefined &&
|
|
503
|
+
pattern.search === url.search &&
|
|
504
|
+
(!pattern.pathname || ImageOptimizer.#globMatch(pattern.pathname, url.pathname)),
|
|
505
|
+
);
|
|
506
|
+
return namesQuery ? `${url.pathname}${url.search}` : url.pathname;
|
|
507
|
+
}
|
|
508
|
+
|
|
414
509
|
static #hasLocalMatch(patterns: Pick<AkanImagePattern, "pathname" | "search">[], href: string): boolean {
|
|
415
510
|
const url = new URL(href, "http://local.akan");
|
|
416
511
|
return patterns.some((pattern) => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const BUILD_ENV_FILE = "akan.env.json";
|
|
5
|
+
|
|
6
|
+
export const applyBuildEnv = (dir: string = path.dirname(Bun.main), env: NodeJS.ProcessEnv = process.env): void => {
|
|
7
|
+
const file = path.join(dir, BUILD_ENV_FILE);
|
|
8
|
+
if (!existsSync(file)) return;
|
|
9
|
+
const defaults = JSON.parse(readFileSync(file, "utf8")) as Record<string, string>;
|
|
10
|
+
for (const [key, value] of Object.entries(defaults)) env[key] ??= value;
|
|
11
|
+
};
|
|
@@ -63,21 +63,27 @@ export class LogControlSocket {
|
|
|
63
63
|
|
|
64
64
|
if (await LogControlSocket.#answers(this.path)) throw new Error("another server is listening on it");
|
|
65
65
|
await rm(this.path, { force: true });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this.#
|
|
77
|
-
this.#release(socket)
|
|
66
|
+
|
|
67
|
+
const umask = process.platform === "win32" ? null : process.umask(0o177);
|
|
68
|
+
try {
|
|
69
|
+
this.#server = Bun.listen<ClientState>({
|
|
70
|
+
unix: this.path,
|
|
71
|
+
socket: {
|
|
72
|
+
open: (socket) => {
|
|
73
|
+
socket.data = { inbox: "", pending: "", droppedWhilePending: 0, subscriptions: new Map() };
|
|
74
|
+
},
|
|
75
|
+
data: (socket, chunk) => this.#onData(socket, chunk),
|
|
76
|
+
drain: (socket) => this.#drain(socket),
|
|
77
|
+
close: (socket) => this.#release(socket),
|
|
78
|
+
error: (socket, error) => {
|
|
79
|
+
this.#logger.debug(`control socket client error: ${error.message}`);
|
|
80
|
+
this.#release(socket);
|
|
81
|
+
},
|
|
78
82
|
},
|
|
79
|
-
}
|
|
80
|
-
}
|
|
83
|
+
});
|
|
84
|
+
} finally {
|
|
85
|
+
if (umask !== null) process.umask(umask);
|
|
86
|
+
}
|
|
81
87
|
await chmod(this.path, 0o600);
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { bearerTokenOf, EventStream, Logger } from "akanjs/common";
|
|
3
|
+
import { FailureLimiter } from "../ops/failureLimiter";
|
|
4
|
+
import { StatusAccess } from "../ops/statusAccess";
|
|
3
5
|
import type { LogHub, LogHubEntry } from "./logHub";
|
|
4
6
|
import { LogQueryMatcher } from "./logQuery";
|
|
5
7
|
import { LogStdoutWriter } from "./logStdoutWriter";
|
|
@@ -10,11 +12,12 @@ export class LogStreamRoute {
|
|
|
10
12
|
static readonly heartbeatMs = 15_000;
|
|
11
13
|
|
|
12
14
|
readonly #hub: () => LogHub | null;
|
|
13
|
-
readonly #
|
|
15
|
+
readonly #digest: Buffer;
|
|
16
|
+
readonly #failures = new FailureLimiter({ limit: 10 });
|
|
14
17
|
|
|
15
18
|
constructor(hub: () => LogHub | null, token: string) {
|
|
16
19
|
this.#hub = hub;
|
|
17
|
-
this.#
|
|
20
|
+
this.#digest = StatusAccess.digest(token);
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/** Without a token the route is not registered at all, rather than mounted to answer "forbidden". */
|
|
@@ -31,8 +34,8 @@ export class LogStreamRoute {
|
|
|
31
34
|
return new LogStreamRoute(hub, token);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
handle(req: Request): Response {
|
|
35
|
-
if (!this.#authorized(req))
|
|
37
|
+
handle(req: Request, client?: string | null): Response {
|
|
38
|
+
if (!this.#authorized(req, client))
|
|
36
39
|
return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
37
40
|
const hub = this.#hub();
|
|
38
41
|
if (!hub) return new Response("Log hub is not running", { status: 503 });
|
|
@@ -53,11 +56,12 @@ export class LogStreamRoute {
|
|
|
53
56
|
return stream.response();
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
#authorized(req: Request): boolean {
|
|
59
|
+
#authorized(req: Request, client: string | null | undefined): boolean {
|
|
57
60
|
const token = bearerTokenOf(req.headers.get("authorization"));
|
|
58
|
-
if (!token) return false;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
+
if (!token || this.#failures.blocked(client)) return false;
|
|
62
|
+
const matched = timingSafeEqual(StatusAccess.digest(token), this.#digest);
|
|
63
|
+
if (!matched) this.#failures.fail(client);
|
|
64
|
+
return matched;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
static #resume(hub: LogHub, stream: EventStream, matcher: LogQueryMatcher, lastEventId: number) {
|
|
@@ -22,6 +22,7 @@ export interface RotatingLogWriterOptions {
|
|
|
22
22
|
operationMode: string;
|
|
23
23
|
maxSizeBytes?: number;
|
|
24
24
|
maxFiles?: number;
|
|
25
|
+
fileMode?: number;
|
|
25
26
|
now?: () => Date;
|
|
26
27
|
warn?: (message: string) => void;
|
|
27
28
|
}
|
|
@@ -29,11 +30,13 @@ export interface RotatingLogWriterOptions {
|
|
|
29
30
|
export class RotatingLogWriter {
|
|
30
31
|
static readonly defaultMaxSizeBytes = 50 * 1024 * 1024;
|
|
31
32
|
static readonly defaultMaxFiles = 100;
|
|
33
|
+
static readonly defaultFileMode = 0o600;
|
|
32
34
|
|
|
33
35
|
readonly #logDir: string;
|
|
34
36
|
readonly #basePrefix: string;
|
|
35
37
|
readonly #maxSizeBytes: number;
|
|
36
38
|
readonly #maxFiles: number;
|
|
39
|
+
readonly #fileMode: number;
|
|
37
40
|
readonly #now: () => Date;
|
|
38
41
|
readonly #warn: (message: string) => void;
|
|
39
42
|
readonly #states = new Map<string, RotatingLogWriterState>();
|
|
@@ -50,6 +53,7 @@ export class RotatingLogWriter {
|
|
|
50
53
|
].join("-");
|
|
51
54
|
this.#maxSizeBytes = options.maxSizeBytes ?? RotatingLogWriter.defaultMaxSizeBytes;
|
|
52
55
|
this.#maxFiles = options.maxFiles ?? RotatingLogWriter.defaultMaxFiles;
|
|
56
|
+
this.#fileMode = options.fileMode ?? RotatingLogWriter.defaultFileMode;
|
|
53
57
|
this.#now = options.now ?? (() => new Date());
|
|
54
58
|
this.#warn = options.warn ?? RotatingLogWriter.#defaultWarn;
|
|
55
59
|
}
|
|
@@ -65,6 +69,7 @@ export class RotatingLogWriter {
|
|
|
65
69
|
operationMode: env.operationMode,
|
|
66
70
|
maxSizeBytes: RotatingLogWriter.#parseMaxSizeBytes(process.env.AKAN_LOG_MAX_SIZE_MB),
|
|
67
71
|
maxFiles: RotatingLogWriter.#parseMaxFiles(process.env.AKAN_LOG_MAX_FILES),
|
|
72
|
+
fileMode: RotatingLogWriter.fileMode(),
|
|
68
73
|
});
|
|
69
74
|
} catch (error) {
|
|
70
75
|
RotatingLogWriter.#defaultWarn(
|
|
@@ -109,7 +114,7 @@ export class RotatingLogWriter {
|
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
const filePath = this.#getFilePath(processKey, state.date, state.sequence);
|
|
112
|
-
await appendFile(filePath, chunk);
|
|
117
|
+
await appendFile(filePath, chunk, { mode: this.#fileMode });
|
|
113
118
|
state.sizeBytes += byteLength;
|
|
114
119
|
if (createdState) await this.#cleanupRetention(processKey);
|
|
115
120
|
}
|
|
@@ -182,6 +187,15 @@ export class RotatingLogWriter {
|
|
|
182
187
|
return parsed;
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
static fileMode(value = process.env.AKAN_LOG_FILE_MODE, warn = RotatingLogWriter.#defaultWarn) {
|
|
191
|
+
const raw = value?.trim();
|
|
192
|
+
if (!raw) return RotatingLogWriter.defaultFileMode;
|
|
193
|
+
const parsed = /^[0-7]{3,4}$/.test(raw) ? Number.parseInt(raw, 8) : 0;
|
|
194
|
+
if (parsed > 0 && parsed <= 0o777) return parsed;
|
|
195
|
+
warn(`AKAN_LOG_FILE_MODE="${raw}" is not an octal mode such as 644; using 600`);
|
|
196
|
+
return RotatingLogWriter.defaultFileMode;
|
|
197
|
+
}
|
|
198
|
+
|
|
185
199
|
static #defaultWarn(message: string) {
|
|
186
200
|
process.stderr.write(`[AkanLog] ${message}\n`);
|
|
187
201
|
}
|
|
@@ -70,6 +70,10 @@ export class MachineToken {
|
|
|
70
70
|
return !!MachineToken.#keyPair().publicKey;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
static publicKey(): KeyObject | null {
|
|
74
|
+
return MachineToken.#keyPair().publicKey;
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
static issue(identity: MachineIdentity, { expiresAt, audience }: MachineTokenIssueOptions = {}): string {
|
|
74
78
|
const { privateKey } = MachineToken.#keyPair();
|
|
75
79
|
if (!privateKey) throw new Error("MachineToken.issue needs AKAN_MACHINE_PRIVATE_KEY on the server that issues it");
|
package/server/mcp/McpAuth.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { PromiseOrObject } from "akanjs/base";
|
|
3
|
-
import { bearerTokenOf, decodeJwtPayload } from "akanjs/common";
|
|
3
|
+
import { AppHosts, bearerTokenOf, decodeJwtPayload, IpLiteral } from "akanjs/common";
|
|
4
4
|
import { SignalContext } from "../../signal/signalContext";
|
|
5
5
|
import type { HttpRoutes } from "../types";
|
|
6
6
|
|
|
@@ -90,7 +90,7 @@ export class McpAuth {
|
|
|
90
90
|
|
|
91
91
|
if (this.#props.authorizationServers?.length && !audience.length)
|
|
92
92
|
return { status: 401, error: "invalid_token", description: "The access token names no resource." };
|
|
93
|
-
if (audience.length && !audience.includes(this.#resource(req)))
|
|
93
|
+
if (audience.length && !(this.answersTo(req) && audience.includes(this.#resource(req))))
|
|
94
94
|
return { status: 401, error: "invalid_token", description: "The access token was issued for another resource." };
|
|
95
95
|
const missing = this.#missingScopes(claims);
|
|
96
96
|
|
|
@@ -149,6 +149,14 @@ export class McpAuth {
|
|
|
149
149
|
return McpAuth.origin(req);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
answersTo(req: Request): boolean {
|
|
153
|
+
if (this.#props.resource) return true;
|
|
154
|
+
const host = URL.parse(McpAuth.origin(req))?.host;
|
|
155
|
+
const name = host ? AppHosts.nameOf(host) : null;
|
|
156
|
+
if (!host || !name) return false;
|
|
157
|
+
return !!IpLiteral.parse(host) || name === "localhost" || name.endsWith(".localhost") || AppHosts.allows(host);
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
static origin(req: Request) {
|
|
153
161
|
const url = new URL(req.url);
|
|
154
162
|
const forwarded = (header: string) => req.headers.get(header)?.split(",")[0]?.trim();
|
|
@@ -156,10 +164,10 @@ export class McpAuth {
|
|
|
156
164
|
return `${forwarded("x-forwarded-proto") ?? url.protocol.replace(":", "")}://${host}`;
|
|
157
165
|
}
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
callerKey(req: Request): string {
|
|
160
168
|
const token = McpAuth.#bearer(req);
|
|
161
169
|
|
|
162
|
-
if (!token) return `ip:${SignalContext.clientIpOf(req) ?? "anonymous"}`;
|
|
170
|
+
if (!token || !this.#props.verify) return `ip:${SignalContext.clientIpOf(req) ?? "anonymous"}`;
|
|
163
171
|
const claims = McpAuth.#claims(token);
|
|
164
172
|
const id = [claims?.sid, claims?.jti, claims?.sub].find((value) => typeof value === "string" && value);
|
|
165
173
|
return typeof id === "string" ? `token:${id}` : `token:${createHash("sha256").update(token).digest("base64url")}`;
|
|
@@ -19,6 +19,10 @@ export class McpExecutionContext extends HttpExecutionContext {
|
|
|
19
19
|
this.#arguments = args;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
override defersBody(): boolean {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
override async readArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
23
27
|
const declared = new Set(endpointInfo.args.filter(McpExecutionContext.#describable).map((arg) => arg.name));
|
|
24
28
|
const undeclared = Object.keys(this.#arguments).find((name) => !declared.has(name));
|
package/server/mcp/McpRouter.ts
CHANGED
|
@@ -68,6 +68,7 @@ interface McpCall {
|
|
|
68
68
|
era: McpEra;
|
|
69
69
|
req: Request;
|
|
70
70
|
ui: boolean;
|
|
71
|
+
inProcess?: boolean;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
interface McpErrorOptions {
|
|
@@ -322,19 +323,19 @@ export class McpRouter {
|
|
|
322
323
|
discoverCache,
|
|
323
324
|
);
|
|
324
325
|
case "tools/list":
|
|
325
|
-
return await this.#list(call, "tools", [
|
|
326
|
+
return await this.#list(call, "tools", async () => [
|
|
326
327
|
...document.tools,
|
|
327
328
|
...(await this.#pages.tools(call.ui, document, call.req)),
|
|
328
329
|
]);
|
|
329
330
|
case "resources/list":
|
|
330
|
-
return await this.#list(call, "resources", [
|
|
331
|
+
return await this.#list(call, "resources", async () => [
|
|
331
332
|
...document.resources,
|
|
332
333
|
...(await this.#pages.resources(call.ui, call.req)),
|
|
333
334
|
]);
|
|
334
335
|
case "resources/templates/list":
|
|
335
|
-
return await this.#list(call, "resourceTemplates", document.resourceTemplates);
|
|
336
|
+
return await this.#list(call, "resourceTemplates", () => document.resourceTemplates);
|
|
336
337
|
case "prompts/list":
|
|
337
|
-
return await this.#list(call, "prompts",
|
|
338
|
+
return await this.#list(call, "prompts", () => this.#pages.prompts(call.req));
|
|
338
339
|
case "tools/call": {
|
|
339
340
|
const slot = await this.#acquire(call);
|
|
340
341
|
if ("refused" in slot) return slot.refused;
|
|
@@ -361,12 +362,15 @@ export class McpRouter {
|
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
|
|
364
|
-
async #list<T extends { name: string }>(call: McpCall, key: string,
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
365
|
+
async #list<T extends { name: string }>(call: McpCall, key: string, itemsOf: () => Promise<T[]> | T[]) {
|
|
366
|
+
const list = async () => {
|
|
367
|
+
const visible = await this.#dispatcher.filterForAccount(await itemsOf(), call.req);
|
|
368
|
+
const page = McpRouter.#page(visible, call.params.cursor, this.#props.pageSize ?? defaultPageSize);
|
|
369
|
+
if (!page) return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Invalid cursor.");
|
|
370
|
+
const result = { [key]: page.items, ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) };
|
|
371
|
+
return this.#result(call, result, listCache);
|
|
372
|
+
};
|
|
373
|
+
return call.inProcess ? await list() : await this.#limited(call, list);
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
/** The catalogue as built, before any caller filter: refusals, undescribed entries, `findTool`. */
|
|
@@ -394,7 +398,15 @@ export class McpRouter {
|
|
|
394
398
|
const items: unknown[] = [];
|
|
395
399
|
let cursor: string | undefined;
|
|
396
400
|
do {
|
|
397
|
-
const call: McpCall = {
|
|
401
|
+
const call: McpCall = {
|
|
402
|
+
id: 0,
|
|
403
|
+
method,
|
|
404
|
+
params: cursor ? { cursor } : {},
|
|
405
|
+
era: "legacy",
|
|
406
|
+
req,
|
|
407
|
+
ui,
|
|
408
|
+
inProcess: true,
|
|
409
|
+
};
|
|
398
410
|
const body = (await (await this.#dispatch(call)).json()) as McpJsonRpcResponse;
|
|
399
411
|
const result = body.result as Record<string, unknown> | undefined;
|
|
400
412
|
if (!result) throw new Error(`MCP ${method} failed: ${body.error?.message ?? "no result"}`);
|
|
@@ -416,7 +428,7 @@ export class McpRouter {
|
|
|
416
428
|
|
|
417
429
|
async #acquire(call: McpCall): Promise<{ refused: Response } | { release: () => void }> {
|
|
418
430
|
if (!this.#limiter) return { release: () => {} };
|
|
419
|
-
const verdict = await this.#limiter.acquire(
|
|
431
|
+
const verdict = await this.#limiter.acquire(this.#auth.callerKey(call.req));
|
|
420
432
|
if (verdict.ok) return { release: verdict.release };
|
|
421
433
|
const seconds = Math.max(1, Math.ceil(verdict.retryAfterMs / 1000));
|
|
422
434
|
const message =
|
|
@@ -577,7 +589,7 @@ export class McpRouter {
|
|
|
577
589
|
if ((this.#props.allowedOrigins ?? []).includes(origin)) return true;
|
|
578
590
|
try {
|
|
579
591
|
|
|
580
|
-
return new URL(origin).host === new URL(this.#auth.publicOrigin(req)).host;
|
|
592
|
+
return new URL(origin).host === new URL(this.#auth.publicOrigin(req)).host && this.#auth.answersTo(req);
|
|
581
593
|
} catch {
|
|
582
594
|
return false;
|
|
583
595
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PrimitiveRegistry } from "akanjs/base";
|
|
2
|
-
import { Logger } from "akanjs/common";
|
|
2
|
+
import { Logger, sliceNames } from "akanjs/common";
|
|
3
3
|
import { agentRead, ConstantRegistry, type ConstantType, type MaskModel } from "akanjs/constant";
|
|
4
4
|
import type { McpDocument } from "../../signal/mcp/McpDocument";
|
|
5
5
|
import { Msg, type PromptMessage } from "../../signal/mcp/Msg";
|
|
@@ -151,14 +151,11 @@ export class PagePromptComposer {
|
|
|
151
151
|
const refName = argName.replace(/Id$/, "");
|
|
152
152
|
if (!refName || refName === argName) return undefined;
|
|
153
153
|
const document = this.#props.document;
|
|
154
|
+
const rootList = sliceNames(refName).endpoint.list;
|
|
154
155
|
const listTools = document.tools
|
|
155
156
|
.map((tool) => tool.name)
|
|
156
|
-
.filter((name) => document.findTool(name)?.refName === refName && name.startsWith(
|
|
157
|
-
return (
|
|
158
|
-
listTools.find((name) => /Search/.test(name)) ??
|
|
159
|
-
listTools.find((name) => name === `${refName}List`) ??
|
|
160
|
-
listTools[0]
|
|
161
|
-
);
|
|
157
|
+
.filter((name) => document.findTool(name)?.refName === refName && name.startsWith(rootList));
|
|
158
|
+
return listTools.find((name) => /Search/.test(name)) ?? listTools.find((name) => name === rootList) ?? listTools[0];
|
|
162
159
|
}
|
|
163
160
|
|
|
164
161
|
static #resource(attachment: Attachment): PromptMessage {
|