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/rscWorker.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type AkanI18nConfig,
|
|
14
14
|
DEFAULT_AKAN_I18N,
|
|
15
15
|
getBasePathFromPathname,
|
|
16
|
+
LocalePath,
|
|
16
17
|
Logger,
|
|
17
18
|
parseSearchParams,
|
|
18
19
|
} from "akanjs/common";
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
import { LogForwarder } from "./logging/logForwarder";
|
|
58
59
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
59
60
|
import { RouteElementComposer, type RouteNotFoundInPlace } from "./routeElementComposer";
|
|
61
|
+
import { RouteShellGate } from "./routeShellGate";
|
|
60
62
|
import {
|
|
61
63
|
type AkanRscPatchDecision,
|
|
62
64
|
createAkanRouterState,
|
|
@@ -69,7 +71,7 @@ import {
|
|
|
69
71
|
resolveAkanRscPatchDecision,
|
|
70
72
|
} from "./routeState";
|
|
71
73
|
import { type PagesContext, RouteTreeBuilder } from "./routeTreeBuilder";
|
|
72
|
-
import { encodeAkanRedirectDigest } from "./rscHttp";
|
|
74
|
+
import { concatBytes, encodeAkanRedirectDigest } from "./rscHttp";
|
|
73
75
|
import { RscPagePrompts } from "./rscPagePrompts";
|
|
74
76
|
import {
|
|
75
77
|
type CachedRscResult,
|
|
@@ -623,6 +625,7 @@ export class RscRenderer {
|
|
|
623
625
|
let effectivePatchDecision = safePatchDecision;
|
|
624
626
|
const notFoundProbe = { hit: false };
|
|
625
627
|
const notFound = this.#notFoundInPlace(urlObj, notFoundProbe);
|
|
628
|
+
const gate = new RouteShellGate();
|
|
626
629
|
if (match && safePatchDecision.status === "patch" && safePatchDecision.patch) {
|
|
627
630
|
const suffixElement = await this.#renderMatchedSuffix(
|
|
628
631
|
urlObj,
|
|
@@ -638,11 +641,11 @@ export class RscRenderer {
|
|
|
638
641
|
reason: "suffix-compose-fallback",
|
|
639
642
|
commonPrefixLength: safePatchDecision.commonPrefixLength,
|
|
640
643
|
};
|
|
641
|
-
element = await this.#renderMatched(urlObj, match, searchParams, notFound, { guarded: true });
|
|
644
|
+
element = await this.#renderMatched(urlObj, match, searchParams, notFound, { guarded: true, gate });
|
|
642
645
|
} else element = suffixElement;
|
|
643
646
|
} else if (match)
|
|
644
|
-
element = await this.#renderMatched(urlObj, match, searchParams, notFound, { guarded: admitted });
|
|
645
|
-
else element = await this.#renderNotFound(urlObj);
|
|
647
|
+
element = await this.#renderMatched(urlObj, match, searchParams, notFound, { guarded: admitted, gate });
|
|
648
|
+
else element = await this.#renderNotFound(urlObj, gate);
|
|
646
649
|
const traceCacheKey =
|
|
647
650
|
effectivePatchDecision.status === "patch" ? (patchCacheEntry?.key ?? cacheEntry?.key) : cacheEntry?.key;
|
|
648
651
|
const ssrBlocking = getRequestFrameState<PageState>()?.ssr === "block";
|
|
@@ -662,6 +665,7 @@ export class RscRenderer {
|
|
|
662
665
|
status: match ? undefined : 404,
|
|
663
666
|
trace,
|
|
664
667
|
notFoundProbe,
|
|
668
|
+
gate,
|
|
665
669
|
onComplete: ({ chunks, bytes, chunksCount, control, lateControlSent }) => {
|
|
666
670
|
const cacheState = shouldStoreRouteCache({
|
|
667
671
|
policy: getRequestPolicy(),
|
|
@@ -894,6 +898,7 @@ export class RscRenderer {
|
|
|
894
898
|
trace?: RscTraceMetadata;
|
|
895
899
|
/** Raised by a render that answered `router.notFound()` in place, which React never reports as an error. */
|
|
896
900
|
notFoundProbe?: { hit: boolean };
|
|
901
|
+
gate?: RouteShellGate;
|
|
897
902
|
onComplete?: (
|
|
898
903
|
result: Omit<FlightRenderResult, "cancelled">,
|
|
899
904
|
) => Promise<RouteCacheRenderState> | RouteCacheRenderState;
|
|
@@ -927,6 +932,9 @@ export class RscRenderer {
|
|
|
927
932
|
let sentChunk = false;
|
|
928
933
|
let lateControlSent = false;
|
|
929
934
|
const chunks: Uint8Array[] = [];
|
|
935
|
+
|
|
936
|
+
let holding = options.gate?.holding ? options.gate : null;
|
|
937
|
+
let held: Uint8Array | null = null;
|
|
930
938
|
let reportedTheme: AkanTheme | undefined;
|
|
931
939
|
const sendMeta = () => {
|
|
932
940
|
if (!options.requestId || sentMeta) return;
|
|
@@ -959,6 +967,24 @@ export class RscRenderer {
|
|
|
959
967
|
const readProbe = () => {
|
|
960
968
|
if (!controlRef.current && options.notFoundProbe?.hit) controlRef.current = { type: "not-found" };
|
|
961
969
|
};
|
|
970
|
+
const emit = (chunk: Uint8Array) => {
|
|
971
|
+
if (options.collectChunks) chunks.push(chunk);
|
|
972
|
+
if (!options.requestId) return;
|
|
973
|
+
sendMeta();
|
|
974
|
+
sendLateTheme();
|
|
975
|
+
this.#send({ type: "chunk", requestId: options.requestId, data: chunk });
|
|
976
|
+
sentChunk = true;
|
|
977
|
+
};
|
|
978
|
+
const releaseHeld = () => {
|
|
979
|
+
holding = null;
|
|
980
|
+
if (!held) return;
|
|
981
|
+
if (controlRef.current) {
|
|
982
|
+
sendMeta();
|
|
983
|
+
sendLateControl();
|
|
984
|
+
}
|
|
985
|
+
emit(held);
|
|
986
|
+
held = null;
|
|
987
|
+
};
|
|
962
988
|
|
|
963
989
|
const sendLateControl = () => {
|
|
964
990
|
const control = controlRef.current;
|
|
@@ -981,16 +1007,26 @@ export class RscRenderer {
|
|
|
981
1007
|
this.#send({ type: "late-not-found", requestId: options.requestId });
|
|
982
1008
|
};
|
|
983
1009
|
try {
|
|
1010
|
+
let pendingRead: ReturnType<typeof reader.read> | null = null;
|
|
984
1011
|
for (;;) {
|
|
985
1012
|
if (options.requestId && this.#cancelledRenderRequests.has(options.requestId))
|
|
986
1013
|
return await cancelWith(null, true);
|
|
987
|
-
|
|
1014
|
+
pendingRead ??= reader.read();
|
|
1015
|
+
if (holding && (await Promise.race([pendingRead.then(() => false), holding.opened.then(() => true)]))) {
|
|
1016
|
+
readProbe();
|
|
1017
|
+
if (controlRef.current?.type === "error") return await cancelWith(controlRef.current, false);
|
|
1018
|
+
releaseHeld();
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const { value, done } = await pendingRead;
|
|
1022
|
+
pendingRead = null;
|
|
988
1023
|
readProbe();
|
|
989
|
-
if (controlRef.current && !sentChunk
|
|
1024
|
+
if (controlRef.current && !sentChunk && (!holding || controlRef.current.type === "error"))
|
|
1025
|
+
return await cancelWith(controlRef.current, false);
|
|
990
1026
|
if (controlRef.current && sentChunk) sendLateControl();
|
|
991
1027
|
if (done) break;
|
|
992
1028
|
|
|
993
|
-
if (
|
|
1029
|
+
if (chunksCount === 0 && options.requestId) {
|
|
994
1030
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
995
1031
|
readProbe();
|
|
996
1032
|
if (controlRef.current) return await cancelWith(controlRef.current, false);
|
|
@@ -998,13 +1034,8 @@ export class RscRenderer {
|
|
|
998
1034
|
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value as ArrayBufferLike);
|
|
999
1035
|
bytes += chunk.byteLength;
|
|
1000
1036
|
chunksCount += 1;
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1003
|
-
sendMeta();
|
|
1004
|
-
sendLateTheme();
|
|
1005
|
-
this.#send({ type: "chunk", requestId: options.requestId, data: chunk });
|
|
1006
|
-
sentChunk = true;
|
|
1007
|
-
}
|
|
1037
|
+
if (holding) held = held ? concatBytes(held, chunk) : chunk;
|
|
1038
|
+
else emit(chunk);
|
|
1008
1039
|
}
|
|
1009
1040
|
} catch (error) {
|
|
1010
1041
|
if (options.requestId && this.#cancelledRenderRequests.has(options.requestId)) {
|
|
@@ -1016,6 +1047,7 @@ export class RscRenderer {
|
|
|
1016
1047
|
reader.releaseLock();
|
|
1017
1048
|
}
|
|
1018
1049
|
readProbe();
|
|
1050
|
+
if (holding && controlRef.current?.type !== "error") releaseHeld();
|
|
1019
1051
|
if (controlRef.current && sentChunk) sendLateControl();
|
|
1020
1052
|
if (controlRef.current && !sentChunk)
|
|
1021
1053
|
return { chunks, bytes, chunksCount, control: controlRef.current, lateControlSent, cancelled: false };
|
|
@@ -1065,6 +1097,7 @@ export class RscRenderer {
|
|
|
1065
1097
|
trace?: RscTraceMetadata;
|
|
1066
1098
|
}): Promise<boolean> {
|
|
1067
1099
|
try {
|
|
1100
|
+
const gate = new RouteShellGate();
|
|
1068
1101
|
const element = await this.#renderFallbackDocument({
|
|
1069
1102
|
kind,
|
|
1070
1103
|
route,
|
|
@@ -1074,9 +1107,17 @@ export class RscRenderer {
|
|
|
1074
1107
|
url,
|
|
1075
1108
|
error: kind === "error" && process.env.NODE_ENV !== "production" ? error : undefined,
|
|
1076
1109
|
digest: kind === "error" ? "AKAN_RENDER_ERROR" : undefined,
|
|
1110
|
+
gate,
|
|
1077
1111
|
});
|
|
1078
1112
|
if (!element) return false;
|
|
1079
|
-
return await this.#trySendFlight(
|
|
1113
|
+
return await this.#trySendFlight(
|
|
1114
|
+
requestId,
|
|
1115
|
+
element,
|
|
1116
|
+
clientManifest,
|
|
1117
|
+
kind === "not-found" ? 404 : 500,
|
|
1118
|
+
trace,
|
|
1119
|
+
gate,
|
|
1120
|
+
);
|
|
1080
1121
|
} catch (fallbackError) {
|
|
1081
1122
|
this.#logger.error(
|
|
1082
1123
|
`render[${requestId}] custom ${kind} fallback failed: ${
|
|
@@ -1116,8 +1157,9 @@ export class RscRenderer {
|
|
|
1116
1157
|
clientManifest: ClientManifest,
|
|
1117
1158
|
status: number,
|
|
1118
1159
|
trace?: RscTraceMetadata,
|
|
1160
|
+
gate?: RouteShellGate,
|
|
1119
1161
|
): Promise<boolean> {
|
|
1120
|
-
const result = await this.#renderFlightElement(element, clientManifest, { requestId, status, trace });
|
|
1162
|
+
const result = await this.#renderFlightElement(element, clientManifest, { requestId, status, trace, gate });
|
|
1121
1163
|
if (result.cancelled) return true;
|
|
1122
1164
|
if (result.control) return false;
|
|
1123
1165
|
this.#recordFlight(result);
|
|
@@ -1275,6 +1317,7 @@ export class RscRenderer {
|
|
|
1275
1317
|
url,
|
|
1276
1318
|
error,
|
|
1277
1319
|
digest,
|
|
1320
|
+
gate,
|
|
1278
1321
|
}: {
|
|
1279
1322
|
kind: "not-found" | "error";
|
|
1280
1323
|
route: PathRoute | LayoutFallbackRoute;
|
|
@@ -1284,6 +1327,7 @@ export class RscRenderer {
|
|
|
1284
1327
|
url: URL;
|
|
1285
1328
|
error?: unknown;
|
|
1286
1329
|
digest?: string;
|
|
1330
|
+
gate?: RouteShellGate;
|
|
1287
1331
|
}): Promise<ReactNode | null> {
|
|
1288
1332
|
setRequestFrameState(
|
|
1289
1333
|
await RouteElementComposer.resolveSsrFallbackFrameState({
|
|
@@ -1299,6 +1343,7 @@ export class RscRenderer {
|
|
|
1299
1343
|
pathname,
|
|
1300
1344
|
error,
|
|
1301
1345
|
digest,
|
|
1346
|
+
gate,
|
|
1302
1347
|
});
|
|
1303
1348
|
if (!body) return null;
|
|
1304
1349
|
const viewport = await RscRenderer.#firstRootValue(route.renderRootLayouts, (render) =>
|
|
@@ -1322,7 +1367,10 @@ export class RscRenderer {
|
|
|
1322
1367
|
{routeHeadSnapshot ? null : this.#renderLocaleAlternates(url)}
|
|
1323
1368
|
{this.#renderStylesheet(pathname)}
|
|
1324
1369
|
</head>
|
|
1325
|
-
<body key="body">
|
|
1370
|
+
<body key="body">
|
|
1371
|
+
{body}
|
|
1372
|
+
{gate?.holding ? <RouteShellGate.Wait until={gate.opened} /> : null}
|
|
1373
|
+
</body>
|
|
1326
1374
|
</html>
|
|
1327
1375
|
);
|
|
1328
1376
|
}
|
|
@@ -1359,7 +1407,7 @@ export class RscRenderer {
|
|
|
1359
1407
|
match: { pathRoute: PathRoute; params: Record<string, string> },
|
|
1360
1408
|
searchParams: Record<string, string | string[]>,
|
|
1361
1409
|
notFound: RouteNotFoundInPlace,
|
|
1362
|
-
{ guarded = false }: { guarded?: boolean } = {},
|
|
1410
|
+
{ guarded = false, gate }: { guarded?: boolean; gate?: RouteShellGate } = {},
|
|
1363
1411
|
): Promise<ReactNode> {
|
|
1364
1412
|
this.#logger.verbose(
|
|
1365
1413
|
`composing route element pathname=${url.pathname} search=${url.search || "(none)"} params=${JSON.stringify(match.params)}`,
|
|
@@ -1390,6 +1438,7 @@ export class RscRenderer {
|
|
|
1390
1438
|
searchParams,
|
|
1391
1439
|
navKey: url.pathname + url.search,
|
|
1392
1440
|
notFound,
|
|
1441
|
+
gate,
|
|
1393
1442
|
});
|
|
1394
1443
|
|
|
1395
1444
|
return (
|
|
@@ -1406,7 +1455,10 @@ export class RscRenderer {
|
|
|
1406
1455
|
{routeHeadSnapshot ? null : this.#renderLocaleAlternates(url)}
|
|
1407
1456
|
{this.#renderStylesheet(url.pathname)}
|
|
1408
1457
|
</head>
|
|
1409
|
-
<body key="body">
|
|
1458
|
+
<body key="body">
|
|
1459
|
+
{body}
|
|
1460
|
+
{gate?.holding ? <RouteShellGate.Wait until={gate.opened} /> : null}
|
|
1461
|
+
</body>
|
|
1410
1462
|
</html>
|
|
1411
1463
|
);
|
|
1412
1464
|
}
|
|
@@ -1439,7 +1491,7 @@ export class RscRenderer {
|
|
|
1439
1491
|
});
|
|
1440
1492
|
}
|
|
1441
1493
|
|
|
1442
|
-
async #renderNotFound(url: URL): Promise<ReactNode> {
|
|
1494
|
+
async #renderNotFound(url: URL, gate?: RouteShellGate): Promise<ReactNode> {
|
|
1443
1495
|
const matchedFallback = RouteTreeBuilder.matchFallback(url.pathname, this.#fallbackRoutes);
|
|
1444
1496
|
if (matchedFallback) {
|
|
1445
1497
|
const searchParams = parseSearchParams(url.search);
|
|
@@ -1455,6 +1507,7 @@ export class RscRenderer {
|
|
|
1455
1507
|
searchParams,
|
|
1456
1508
|
pathname: url.pathname,
|
|
1457
1509
|
url,
|
|
1510
|
+
gate,
|
|
1458
1511
|
});
|
|
1459
1512
|
if (fallback) return fallback;
|
|
1460
1513
|
} catch (error) {
|
|
@@ -1562,7 +1615,7 @@ export class RscRenderer {
|
|
|
1562
1615
|
const publicUrl = RscRenderer.#getPublicRequestUrl(url);
|
|
1563
1616
|
for (const lang of this.#i18n.locales) {
|
|
1564
1617
|
const alternateUrl = new URL(publicUrl);
|
|
1565
|
-
alternateUrl.pathname = RscRenderer.#replaceLocalePathSegment(publicUrl.pathname, lang);
|
|
1618
|
+
alternateUrl.pathname = RscRenderer.#replaceLocalePathSegment(publicUrl.pathname, lang, this.#i18n);
|
|
1566
1619
|
languages[lang] = alternateUrl.href;
|
|
1567
1620
|
}
|
|
1568
1621
|
const xDefaultUrl = new URL(publicUrl);
|
|
@@ -1612,10 +1665,9 @@ export class RscRenderer {
|
|
|
1612
1665
|
return publicUrl;
|
|
1613
1666
|
}
|
|
1614
1667
|
|
|
1615
|
-
static #replaceLocalePathSegment(pathname: string, lang: string): string {
|
|
1668
|
+
static #replaceLocalePathSegment(pathname: string, lang: string, i18n: AkanI18nConfig): string {
|
|
1616
1669
|
const parts = pathname.split("/").filter(Boolean);
|
|
1617
|
-
|
|
1618
|
-
return `/${[lang, ...parts.slice(1)].join("/")}`;
|
|
1670
|
+
return LocalePath.join(lang, `/${parts.slice(1).join("/")}`, i18n);
|
|
1619
1671
|
}
|
|
1620
1672
|
}
|
|
1621
1673
|
|
package/server/sitemap.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AkanI18nConfig, trimSlashes, trimTrailingSlashes } from "akanjs/common";
|
|
1
|
+
import { type AkanI18nConfig, LocalePath, trimSlashes, trimTrailingSlashes } from "akanjs/common";
|
|
2
2
|
import type { RouteSeedEntry } from "./artifact";
|
|
3
3
|
|
|
4
4
|
export interface DefaultSitemapOptions {
|
|
@@ -26,7 +26,7 @@ export function createDefaultSitemapXml({ origin, basePath, entries, i18n }: Def
|
|
|
26
26
|
export function createDefaultSitemapUrls({ origin, basePath, entries, i18n }: DefaultSitemapOptions): string[] {
|
|
27
27
|
const normalizedOrigin = trimTrailingSlashes(origin);
|
|
28
28
|
const normalizedBasePath = normalizeSegment(basePath ?? "");
|
|
29
|
-
const paths = entries.flatMap((entry) => expandStaticRoute(entry.pattern, normalizedBasePath, i18n
|
|
29
|
+
const paths = entries.flatMap((entry) => expandStaticRoute(entry.pattern, normalizedBasePath, i18n));
|
|
30
30
|
return paths.map((pathname) => `${normalizedOrigin}${pathname}`);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -46,7 +46,7 @@ export function getSitemapBasePath(
|
|
|
46
46
|
return basePaths.map(normalizeSegment).includes(normalizedHeaderBasePath) ? normalizedHeaderBasePath : undefined;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function expandStaticRoute(pattern: string, basePath: string,
|
|
49
|
+
function expandStaticRoute(pattern: string, basePath: string, i18n: AkanI18nConfig): string[] {
|
|
50
50
|
const segments = pattern.split("/").filter(Boolean);
|
|
51
51
|
const langIndex = segments.indexOf(":lang");
|
|
52
52
|
if (langIndex < 0) return [];
|
|
@@ -59,10 +59,8 @@ function expandStaticRoute(pattern: string, basePath: string, locales: readonly
|
|
|
59
59
|
staticSegments = staticSegments.slice(1);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return `/${parts.map(encodePathSegment).join("/")}`;
|
|
65
|
-
});
|
|
62
|
+
const path = `/${staticSegments.filter(Boolean).map(encodePathSegment).join("/")}`;
|
|
63
|
+
return i18n.locales.map((locale) => LocalePath.join(locale, path, i18n));
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
function normalizePathname(pathname: string): string {
|
|
@@ -608,7 +608,14 @@ export class SsrFromRscRenderer {
|
|
|
608
608
|
inlineBoundaries: input.inlineBoundaries,
|
|
609
609
|
onError: (error, phase) => SsrFromRscRenderer.#reportRenderError(error, input, phase),
|
|
610
610
|
});
|
|
611
|
-
|
|
611
|
+
let htmlStream: ReadableStream<Uint8Array>;
|
|
612
|
+
try {
|
|
613
|
+
htmlStream = await inRequestScope(renderHtml);
|
|
614
|
+
} catch (error) {
|
|
615
|
+
stderrSuppressor?.stop();
|
|
616
|
+
void rscForClient.cancel(error).catch(() => {});
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
612
619
|
|
|
613
620
|
const withHeadScripts = SsrFromRscRenderer.#injectHeadScriptsIntoHead(htmlStream, {
|
|
614
621
|
importmap: input.importmap,
|
|
@@ -638,7 +645,7 @@ export class SsrFromRscRenderer {
|
|
|
638
645
|
const registry = new SsrChunkRegistry<Record<string, unknown>>(
|
|
639
646
|
parsePositiveInt(process.env.AKAN_SSR_CHUNK_REGISTRY_MAX_ENTRIES) ?? DEFAULT_SSR_CHUNK_REGISTRY_MAX_ENTRIES,
|
|
640
647
|
);
|
|
641
|
-
|
|
648
|
+
const loadChunk = async (chunkId: string) => {
|
|
642
649
|
if (registry.get(chunkId)) {
|
|
643
650
|
SsrFromRscRenderer.#chunkRegistryStats.ssrChunkCacheHitCount += 1;
|
|
644
651
|
return;
|
|
@@ -651,6 +658,14 @@ export class SsrFromRscRenderer {
|
|
|
651
658
|
SsrFromRscRenderer.#chunkRegistryStats.ssrChunkRegistrySize = registry.size;
|
|
652
659
|
SsrFromRscRenderer.#chunkRegistryStats.ssrChunkEvictionCount = registry.evictionCount;
|
|
653
660
|
};
|
|
661
|
+
g.__webpack_chunk_load__ = (chunkId: string) => {
|
|
662
|
+
const load = loadChunk(chunkId);
|
|
663
|
+
const loading = SsrFromRscRenderer.#chunkLoads();
|
|
664
|
+
const settle = () => loading.delete(load);
|
|
665
|
+
loading.add(load);
|
|
666
|
+
load.then(settle, settle);
|
|
667
|
+
return load;
|
|
668
|
+
};
|
|
654
669
|
g.__webpack_require__ = (id: string) => {
|
|
655
670
|
const mod = registry.get(id);
|
|
656
671
|
if (!mod) throw new Error(`[ssrFromRsc] module not loaded yet: ${id}`);
|
|
@@ -662,14 +677,31 @@ export class SsrFromRscRenderer {
|
|
|
662
677
|
const stream = await renderToReadableStream(root, {
|
|
663
678
|
bootstrapScriptContent: options.bootstrap,
|
|
664
679
|
|
|
665
|
-
|
|
680
|
+
progressiveChunkSize: Number.POSITIVE_INFINITY,
|
|
666
681
|
onError: (error) => options.onError(error),
|
|
667
682
|
});
|
|
668
683
|
if (options.waitForAllReady || options.inlineBoundaries) await stream.allReady;
|
|
669
|
-
else
|
|
684
|
+
else {
|
|
685
|
+
SsrFromRscRenderer.holdPostShellErrors(stream, (error) => options.onError(error, "post-shell"));
|
|
686
|
+
await SsrFromRscRenderer.#settleChunkLoads();
|
|
687
|
+
}
|
|
670
688
|
return stream;
|
|
671
689
|
}
|
|
672
690
|
|
|
691
|
+
static async #settleChunkLoads(): Promise<void> {
|
|
692
|
+
const loading = SsrFromRscRenderer.#chunkLoads();
|
|
693
|
+
for (let round = 0; round < 4 && loading.size > 0; round++) {
|
|
694
|
+
await Promise.allSettled(loading);
|
|
695
|
+
for (let turn = 0; turn < 2; turn++) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
static #chunkLoads(): Set<Promise<void>> {
|
|
700
|
+
const g = globalThis as unknown as { __rsc_ssr_chunk_loads__?: Set<Promise<void>> };
|
|
701
|
+
g.__rsc_ssr_chunk_loads__ ??= new Set();
|
|
702
|
+
return g.__rsc_ssr_chunk_loads__;
|
|
703
|
+
}
|
|
704
|
+
|
|
673
705
|
static holdPostShellErrors(stream: { allReady: Promise<void> }, report: (error: unknown) => void): void {
|
|
674
706
|
void stream.allReady.catch(report);
|
|
675
707
|
}
|
package/server/ssrTypes.ts
CHANGED
|
@@ -63,9 +63,12 @@ export interface SsrFromRscInput {
|
|
|
63
63
|
injectThemeInitScript?: boolean;
|
|
64
64
|
lateControl?: Promise<SsrLateControl | null>;
|
|
65
65
|
onCancel?: (reason?: unknown) => void;
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Buffers until `stream.allReady` and writes every Suspense boundary in place (`pageConfig.ssr: "block"`);
|
|
68
|
+
* `AKAN_SSR_WAIT_FOR_ALL_READY=1` forces it globally.
|
|
69
|
+
*/
|
|
67
70
|
waitForAllReady?: boolean;
|
|
68
|
-
/** Buffers like `waitForAllReady
|
|
71
|
+
/** Buffers like `waitForAllReady`, for a reader that runs no script. */
|
|
69
72
|
inlineBoundaries?: boolean;
|
|
70
73
|
}
|
|
71
74
|
|
package/server/staticPath.ts
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
4
|
+
const isWithin = (child: string, parent: string) =>
|
|
5
|
+
child === parent || child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
|
6
|
+
|
|
7
|
+
const realPathOf = (target: string): string | null => {
|
|
8
|
+
try {
|
|
9
|
+
return fs.realpathSync(target);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const libLinkTarget = (baseDir: string, filePath: string): string | null => {
|
|
16
|
+
const [libs, lib] = path.relative(baseDir, filePath).split(path.sep);
|
|
17
|
+
if (libs !== "libs" || !lib) return null;
|
|
18
|
+
const target = realPathOf(path.join(baseDir, libs, lib));
|
|
19
|
+
if (!target || path.basename(target) !== path.basename(baseDir)) return null;
|
|
20
|
+
const libDir = path.dirname(target);
|
|
21
|
+
return path.basename(libDir) === lib && path.basename(path.dirname(libDir)) === "libs" ? target : null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const staysInTree = (baseDir: string, filePath: string): boolean => {
|
|
25
|
+
let real: string;
|
|
26
|
+
try {
|
|
27
|
+
real = fs.realpathSync(filePath);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
30
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
31
|
+
}
|
|
32
|
+
const base = path.resolve(baseDir);
|
|
33
|
+
if (isWithin(real, realPathOf(base) ?? base)) return true;
|
|
34
|
+
const libTarget = libLinkTarget(base, filePath);
|
|
35
|
+
return !!libTarget && isWithin(real, libTarget);
|
|
36
|
+
};
|
|
37
|
+
|
|
3
38
|
export const resolveStaticPath = (baseDir: string, urlPath: string): string | null => {
|
|
4
39
|
let decoded: string;
|
|
5
40
|
try {
|
|
@@ -14,5 +49,5 @@ export const resolveStaticPath = (baseDir: string, urlPath: string): string | nu
|
|
|
14
49
|
if (resolved === normalizedBase) return resolved;
|
|
15
50
|
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
16
51
|
if (!resolved.startsWith(baseWithSep)) return null;
|
|
17
|
-
return resolved;
|
|
52
|
+
return staysInTree(normalizedBase, resolved) ? resolved : null;
|
|
18
53
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AkanI18nConfig } from "akanjs/common";
|
|
2
|
-
import { DEFAULT_AKAN_I18N, getBasePathFromPathname } from "akanjs/common";
|
|
2
|
+
import { DEFAULT_AKAN_I18N, getBasePathFromPathname, LocalePath } from "akanjs/common";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
export type SystemPageKind = "not-found" | "error";
|
|
@@ -80,7 +80,7 @@ export const SystemPageMain = (options: Omit<SystemPageOptions, "lang" | "styles
|
|
|
80
80
|
<a className="akan-system-action" href={options.homeHref}>
|
|
81
81
|
{copy.actionLabel}
|
|
82
82
|
</a>
|
|
83
|
-
<a className="akan-system-action akan-system-secondary" href={options.pathname}>
|
|
83
|
+
<a className="akan-system-action akan-system-secondary" href={getSameOriginHref(options.pathname)}>
|
|
84
84
|
Try again
|
|
85
85
|
</a>
|
|
86
86
|
</div>
|
|
@@ -122,6 +122,8 @@ export function createSystemPageDocument(options: SystemPageOptions): ReactNode
|
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export const getSameOriginHref = (pathname: string): string => `/${pathname.replace(/^[\s/\\]+/, "")}`;
|
|
126
|
+
|
|
125
127
|
export function getSystemPageErrorDetails(error: unknown): string {
|
|
126
128
|
if (error instanceof Error) return error.stack ?? error.message;
|
|
127
129
|
return String(error);
|
|
@@ -144,7 +146,7 @@ export function getSystemPageHomeHref({
|
|
|
144
146
|
i18n,
|
|
145
147
|
headerBasePath,
|
|
146
148
|
});
|
|
147
|
-
return
|
|
149
|
+
return LocalePath.join(locale, basePath ? `/${basePath}` : "/", i18n);
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
export interface SubRouteIndexOptions {
|
package/server/systemPages.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { applySecurityHeaders } from "./assetHeaders";
|
|
2
3
|
import {
|
|
3
4
|
createSubRouteIndexDocument,
|
|
4
5
|
createSystemPageDocument,
|
|
@@ -38,10 +39,13 @@ export async function createSubRouteIndexResponse(options: SubRouteIndexResponse
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function createSystemPageHeaders(): Headers {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
return applySecurityHeaders(
|
|
43
|
+
new Headers({
|
|
44
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
45
|
+
"Cache-Control": "no-store",
|
|
46
|
+
}),
|
|
47
|
+
{ html: true },
|
|
48
|
+
);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
export function createSystemPageFallbackText(kind: SystemPageKind): string {
|
|
@@ -70,10 +74,12 @@ async function renderDocumentResponse({
|
|
|
70
74
|
} catch {
|
|
71
75
|
return new Response(fallbackText(), {
|
|
72
76
|
status,
|
|
73
|
-
headers:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
headers: applySecurityHeaders(
|
|
78
|
+
new Headers({
|
|
79
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
80
|
+
"Cache-Control": "no-store",
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
}
|
|
@@ -10,12 +10,18 @@ import { TunnelDataSocket } from "./TunnelDataSocket";
|
|
|
10
10
|
export interface TunnelAgentOptions {
|
|
11
11
|
/** The gateway, as `wss://tunnel.akanjs.com` — scheme included, no path, no trailing slash. */
|
|
12
12
|
gatewayUrl: string;
|
|
13
|
+
/** Accepts a `ws://` gateway off loopback, whose frames and token would cross the network in cleartext. */
|
|
14
|
+
allowInsecureGateway?: boolean;
|
|
13
15
|
token: string;
|
|
14
16
|
hostnames: string[];
|
|
15
17
|
/** The local app, as `http://localhost:8282`. */
|
|
16
18
|
origin: string;
|
|
17
19
|
/** Where a `tcp` stream dials. Local by construction: an agent is a door into one machine, not a router. */
|
|
18
20
|
tcpHost?: string;
|
|
21
|
+
/** The only ports a `tcp` stream may dial; none by default, so the gateway can reach nothing but `origin`. */
|
|
22
|
+
tcpPorts?: readonly number[];
|
|
23
|
+
/** The data sockets this agent ever holds, whatever the gateway asks for (default 128). */
|
|
24
|
+
maxSockets?: number;
|
|
19
25
|
name?: string;
|
|
20
26
|
/** Attempts on a link that has never gone ready before the agent gives up (default 10). */
|
|
21
27
|
retryLimit?: number;
|
|
@@ -55,9 +61,18 @@ export class TunnelAgent {
|
|
|
55
61
|
#stopped = false;
|
|
56
62
|
|
|
57
63
|
constructor(options: TunnelAgentOptions) {
|
|
64
|
+
TunnelAgent.#assertGateway(options.gatewayUrl, options.allowInsecureGateway ?? false);
|
|
58
65
|
this.#options = options;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
static #assertGateway(gatewayUrl: string, allowInsecure: boolean) {
|
|
69
|
+
const url = URL.parse(gatewayUrl);
|
|
70
|
+
if (url?.protocol === "wss:") return;
|
|
71
|
+
if (url?.protocol === "ws:" && (allowInsecure || ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
72
|
+
return;
|
|
73
|
+
throw new Error(`Tunnel gateway must be a wss:// URL (ws:// only on loopback), not ${gatewayUrl}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
get ready() {
|
|
62
77
|
return this.#ready;
|
|
63
78
|
}
|
|
@@ -220,18 +235,20 @@ export class TunnelAgent {
|
|
|
220
235
|
const ready = this.#ready;
|
|
221
236
|
if (!ready || this.#stopped) return;
|
|
222
237
|
const idle = [...this.#sockets].filter((socket) => !socket.busy).length;
|
|
223
|
-
const
|
|
238
|
+
const ceiling = Math.min(ready.maxSockets, this.#options.maxSockets ?? tunnelWireContract.defaultMaxSockets);
|
|
239
|
+
const room = Math.min(ready.idle - idle, ceiling - this.#sockets.size);
|
|
224
240
|
for (let opened = 0; opened < room; opened += 1) this.#open(ready.sessionId);
|
|
225
241
|
}
|
|
226
242
|
|
|
227
243
|
#open(sessionId: string) {
|
|
228
|
-
const { gatewayUrl, token, origin, tcpHost = "127.0.0.1" } = this.#options;
|
|
244
|
+
const { gatewayUrl, token, origin, tcpHost = "127.0.0.1", tcpPorts = [] } = this.#options;
|
|
229
245
|
const socket = new TunnelDataSocket({
|
|
230
246
|
gatewayUrl,
|
|
231
247
|
token,
|
|
232
248
|
sessionId,
|
|
233
249
|
origin,
|
|
234
250
|
tcpHost,
|
|
251
|
+
tcpPorts,
|
|
235
252
|
logger: this.logger,
|
|
236
253
|
onIdle: () => this.#grow(),
|
|
237
254
|
onBusy: () => this.#grow(),
|
|
@@ -12,6 +12,8 @@ export interface TunnelDataSocketOptions {
|
|
|
12
12
|
origin: string;
|
|
13
13
|
/** Where a `tcp` stream dials; the port comes from the open frame. */
|
|
14
14
|
tcpHost: string;
|
|
15
|
+
/** The ports an open frame may name; any other is refused. */
|
|
16
|
+
tcpPorts: readonly number[];
|
|
15
17
|
logger: Logger;
|
|
16
18
|
onIdle: (socket: TunnelDataSocket) => void;
|
|
17
19
|
onBusy: (socket: TunnelDataSocket) => void;
|
|
@@ -114,7 +116,7 @@ export class TunnelDataSocket {
|
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
#start(open: TunnelOpenFrame) {
|
|
117
|
-
const { origin, tcpHost } = this.#options;
|
|
119
|
+
const { origin, tcpHost, tcpPorts } = this.#options;
|
|
118
120
|
const link: TunnelStreamLink = {
|
|
119
121
|
sendFrame: (frame) => this.#send(frame),
|
|
120
122
|
sendPayload: (payload) => this.#sendPayload(payload),
|
|
@@ -125,7 +127,7 @@ export class TunnelDataSocket {
|
|
|
125
127
|
open.kind === "websocket"
|
|
126
128
|
? new TunnelWebsocketStream(open, link, origin)
|
|
127
129
|
: open.kind === "tcp"
|
|
128
|
-
? new TunnelTcpStream(open, link, tcpHost)
|
|
130
|
+
? new TunnelTcpStream(open, link, tcpHost, tcpPorts)
|
|
129
131
|
: new TunnelHttpStream(open, link, origin);
|
|
130
132
|
this.#options.onBusy(this);
|
|
131
133
|
void this.#stream.run();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TunnelOpenFrame, tunnelWireContract } from "akanjs/common";
|
|
2
|
-
import { type TunnelStream, type TunnelStreamLink, tunnelResetCodeOf } from "./tunnelStream";
|
|
2
|
+
import { type TunnelStream, type TunnelStreamLink, tunnelResetCodeOf, tunnelTargetUrl } from "./tunnelStream";
|
|
3
3
|
|
|
4
4
|
const reframedHeaders = new Set(["content-encoding", "content-length"]);
|
|
5
5
|
|
|
@@ -26,8 +26,13 @@ export class TunnelHttpStream implements TunnelStream {
|
|
|
26
26
|
|
|
27
27
|
async run() {
|
|
28
28
|
const { streamId, method = "GET", path = "/", headers = [] } = this.#open;
|
|
29
|
+
const target = tunnelTargetUrl(this.#origin, path);
|
|
30
|
+
if (!target) {
|
|
31
|
+
this.#link.sendFrame({ type: "reset", streamId, code: "protocol", message: "path leaves the shared origin" });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
29
34
|
try {
|
|
30
|
-
const response = await fetch(
|
|
35
|
+
const response = await fetch(target, {
|
|
31
36
|
method,
|
|
32
37
|
headers: tunnelWireContract.headersOf(headers),
|
|
33
38
|
body: this.#bodyStream,
|