akanjs 3.0.0-alpha.71 → 3.0.0-alpha.73
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/client/cookie.ts +21 -34
- package/client/csrTypes.ts +0 -12
- package/client/frameConfig.ts +13 -0
- package/common/Logger.ts +13 -6
- package/common/TrustedProxy.ts +134 -0
- package/common/clamp.ts +2 -0
- package/common/dynamicRecord.ts +12 -0
- package/common/index.ts +5 -1
- package/common/isValidDate.ts +1 -1
- package/common/requestOrigin.ts +32 -0
- package/common/{httpClient.ts → restClient.ts} +16 -9
- package/constant/types.ts +22 -0
- package/dictionary/base.dictionary.ts +4 -0
- package/dictionary/trans.ts +24 -5
- package/document/dataLoader.ts +0 -4
- package/document/filterMeta.ts +2 -2
- package/document/loaderInfo.ts +4 -1
- package/fetch/client/fetchClient.ts +65 -88
- package/fetch/client/fetchHandle.ts +57 -0
- package/fetch/client/httpClient.ts +42 -14
- package/fetch/client/sliceInitHandle.ts +101 -0
- package/fetch/fetchType/appliedReturn.type.ts +64 -1
- package/fetch/fetchType/sliceFetch.type.ts +5 -5
- package/fetch/requestStorage.ts +19 -5
- package/index.ts +16 -2
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanApp.ts +3 -19
- package/server/akanAppHeaders.ts +10 -4
- package/server/akanOption.ts +14 -1
- package/server/akanServer.ts +3 -0
- package/server/cachePolicy.ts +2 -11
- package/server/decorators.ts +12 -3
- package/server/devtools/constantSerializer.ts +8 -1
- package/server/di/diLifecycle.ts +21 -1
- package/server/di/resolveHierarchy.ts +3 -3
- package/server/imageOptimizer.ts +2 -17
- package/server/mcp/McpDispatcher.ts +8 -2
- package/server/resolver/database.resolver.ts +13 -8
- package/server/resolver/signal.resolver.ts +3 -3
- package/server/routeTreeBuilder.ts +1 -15
- package/server/routing/apiRouter.ts +4 -5
- package/server/routing/appWsData.ts +9 -6
- package/server/staticPath.ts +34 -0
- package/server/webRouter.ts +35 -41
- package/service/adapt.ts +1 -5
- package/service/predefinedAdaptor/cache.adaptor.ts +8 -4
- package/service/predefinedAdaptor/database.adaptor.ts +37 -1766
- package/service/predefinedAdaptor/sql/PendingStoreEnsures.ts +22 -0
- package/service/predefinedAdaptor/sql/QueryCompiler.ts +225 -0
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +812 -0
- package/service/predefinedAdaptor/sql/UpdateCompiler.ts +67 -0
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +151 -0
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +134 -0
- package/service/predefinedAdaptor/sql/driver/bunSqlite.ts +29 -0
- package/service/predefinedAdaptor/sql/driver/libsql.ts +37 -0
- package/service/predefinedAdaptor/sql/driver/postgres.ts +42 -0
- package/service/predefinedAdaptor/sql/types.ts +251 -0
- package/service/predefinedAdaptor/sql/values.ts +91 -0
- package/signal/CrossSiteGuard.ts +71 -0
- package/signal/SignalFailure.ts +51 -0
- package/signal/endpoint.ts +2 -2
- package/signal/exception.ts +43 -6
- package/signal/index.ts +2 -0
- package/signal/intercept.ts +5 -2
- package/signal/internal.ts +10 -2
- package/signal/serializer/fetch.serializer.ts +11 -1
- package/signal/signalContext.ts +25 -28
- package/signal/slice.ts +1 -1
- package/store/SliceRequest.ts +23 -0
- package/store/action.ts +177 -114
- package/store/agent/AgentBridge.ts +3 -1
- package/store/agentic/StExposeBuilder.ts +7 -4
- package/store/agentic/StStateBuilder.ts +9 -4
- package/store/agentic/StToolBuilder.ts +21 -7
- package/store/store.ts +1 -1
- package/store/storeInstance.ts +3 -3
- package/store/storeRegistry.ts +1 -1
- package/types/client/cookie.d.ts +13 -2
- package/types/client/csrTypes.d.ts +0 -1
- package/types/client/frameConfig.d.ts +1 -0
- package/types/common/TrustedProxy.d.ts +24 -0
- package/types/common/clamp.d.ts +2 -0
- package/types/common/dynamicRecord.d.ts +14 -0
- package/types/common/index.d.ts +5 -1
- package/types/common/requestOrigin.d.ts +14 -0
- package/types/common/restClient.d.ts +27 -0
- package/types/constant/types.d.ts +13 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/dataLoader.d.ts +0 -3
- package/types/document/filterMeta.d.ts +2 -2
- package/types/document/loaderInfo.d.ts +2 -2
- package/types/fetch/client/fetchHandle.d.ts +24 -0
- package/types/fetch/client/httpClient.d.ts +2 -0
- package/types/fetch/client/sliceInitHandle.d.ts +29 -0
- package/types/fetch/fetchType/appliedReturn.type.d.ts +38 -1
- package/types/fetch/fetchType/sliceFetch.type.d.ts +5 -5
- package/types/fetch/requestStorage.d.ts +14 -0
- package/types/index.d.ts +17 -2
- package/types/server/akanOption.d.ts +8 -1
- package/types/server/decorators.d.ts +12 -2
- package/types/server/di/diLifecycle.d.ts +9 -1
- package/types/server/routeTreeBuilder.d.ts +1 -2
- package/types/server/routing/appWsData.d.ts +7 -3
- package/types/server/staticPath.d.ts +17 -0
- package/types/service/adapt.d.ts +1 -1
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +15 -407
- package/types/service/predefinedAdaptor/sql/PendingStoreEnsures.d.ts +6 -0
- package/types/service/predefinedAdaptor/sql/QueryCompiler.d.ts +28 -0
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +127 -0
- package/types/service/predefinedAdaptor/sql/UpdateCompiler.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +27 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +27 -0
- package/types/service/predefinedAdaptor/sql/driver/bunSqlite.d.ts +16 -0
- package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/driver/postgres.d.ts +17 -0
- package/types/service/predefinedAdaptor/sql/types.d.ts +226 -0
- package/types/service/predefinedAdaptor/sql/values.d.ts +13 -0
- package/types/signal/CrossSiteGuard.d.ts +44 -0
- package/types/signal/SignalFailure.d.ts +28 -0
- package/types/signal/exception.d.ts +38 -54774
- package/types/signal/index.d.ts +2 -0
- package/types/signal/intercept.d.ts +3 -3
- package/types/signal/internal.d.ts +5 -1
- package/types/signal/serializer/fetch.serializer.d.ts +7 -1
- package/types/store/SliceRequest.d.ts +16 -0
- package/types/store/agentic/StToolBuilder.d.ts +10 -1
- package/types/ui/Agent/Menu.d.ts +1 -1
- package/types/ui/Field/Choice.d.ts +52 -0
- package/types/ui/Field/Collection.d.ts +51 -0
- package/types/ui/Field/DateField.d.ts +32 -0
- package/types/ui/Field/Label.d.ts +9 -0
- package/types/ui/Field/NumberField.d.ts +43 -0
- package/types/ui/Field/Relation.d.ts +80 -0
- package/types/ui/Field/Text.d.ts +101 -0
- package/types/ui/Field/index.d.ts +50 -0
- package/types/ui/Field.d.ts +1 -412
- package/types/ui/Load/Edit_Client.d.ts +3 -1
- package/types/ui/Load/Stream.d.ts +22 -0
- package/types/ui/Load/index.d.ts +2 -0
- package/types/ui/System/Root.d.ts +4 -0
- package/types/ui/index.d.ts +1 -1
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/webkit/useCamera.d.ts +10 -1
- package/types/webkit/usePurchase.d.ts +4 -0
- package/ui/Agent/Menu.tsx +2 -2
- package/ui/Data/TableList.tsx +1 -1
- package/ui/DraggableList.tsx +1 -2
- package/ui/Field/Choice.tsx +172 -0
- package/ui/Field/Collection.tsx +293 -0
- package/ui/Field/DateField.tsx +138 -0
- package/ui/Field/Label.tsx +31 -0
- package/ui/Field/NumberField.tsx +169 -0
- package/ui/Field/Relation.tsx +387 -0
- package/ui/Field/Text.tsx +360 -0
- package/ui/Field/index.tsx +70 -0
- package/ui/Input.tsx +3 -3
- package/ui/Load/Edit.tsx +2 -2
- package/ui/Load/Edit_Client.tsx +11 -3
- package/ui/Load/Pagination.tsx +25 -25
- package/ui/Load/Stream.tsx +38 -0
- package/ui/Load/Units.tsx +50 -37
- package/ui/Load/View.tsx +25 -17
- package/ui/Load/index.ts +2 -0
- package/ui/Model/EditModal.tsx +7 -7
- package/ui/Signal/Message.tsx +7 -7
- package/ui/Signal/PubSub.tsx +4 -1
- package/ui/System/Client.tsx +0 -2
- package/ui/System/Gtag.tsx +1 -1
- package/ui/System/Root.tsx +4 -0
- package/ui/index.ts +1 -1
- package/ui/overlayPosition.ts +1 -2
- package/webkit/useCamera.tsx +22 -6
- package/webkit/useCodepush.tsx +2 -2
- package/webkit/useCsrValues.ts +1 -3
- package/webkit/useFetch.ts +4 -3
- package/webkit/useFrameRuntime.ts +14 -7
- package/webkit/usePurchase.tsx +4 -1
- package/fetch/serializer/fetch.serializer.ts +0 -142
- package/fetch/serializer/index.ts +0 -1
- package/types/common/httpClient.d.ts +0 -20
- package/types/fetch/serializer/fetch.serializer.d.ts +0 -14
- package/types/fetch/serializer/index.d.ts +0 -1
- package/types/fetch/serializer.d.ts +0 -1
- package/ui/Field.tsx +0 -1639
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { LogHub } from "./logging/logHub";
|
|
|
18
18
|
import { LogStreamRoute } from "./logging/logStreamRoute";
|
|
19
19
|
import { RotatingLogWriter } from "./logging/rotatingLogWriter";
|
|
20
20
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
21
|
+
import { resolveStaticPath } from "./staticPath";
|
|
21
22
|
import { getWebConfigFromEnv } from "./types";
|
|
22
23
|
|
|
23
24
|
interface ChildState {
|
|
@@ -669,7 +670,7 @@ export class AkanApp {
|
|
|
669
670
|
if (!this.#web.ssr) return null;
|
|
670
671
|
const clientPrefix = "/_akan/client/";
|
|
671
672
|
if (url.pathname.startsWith(clientPrefix)) {
|
|
672
|
-
const filePath =
|
|
673
|
+
const filePath = resolveStaticPath(
|
|
673
674
|
path.join(this.#artifactDir, "client"),
|
|
674
675
|
url.pathname.slice(clientPrefix.length),
|
|
675
676
|
);
|
|
@@ -684,7 +685,7 @@ export class AkanApp {
|
|
|
684
685
|
|
|
685
686
|
for (const prefix of ["/_akan/styles/", "/_akan/fonts/"]) {
|
|
686
687
|
if (!url.pathname.startsWith(prefix)) continue;
|
|
687
|
-
const filePath =
|
|
688
|
+
const filePath = resolveStaticPath(this.#artifactDir, url.pathname.slice("/_akan/".length));
|
|
688
689
|
if (!filePath) return new Response("Not Found", { status: 404 });
|
|
689
690
|
const file = Bun.file(filePath);
|
|
690
691
|
if (!(await file.exists())) return new Response("Not Found", { status: 404 });
|
|
@@ -1018,23 +1019,6 @@ export class AkanApp {
|
|
|
1018
1019
|
return new Response(Bun.file(filePath).stream(), { headers });
|
|
1019
1020
|
}
|
|
1020
1021
|
|
|
1021
|
-
#safeResolve(baseDir: string, urlPath: string): string | null {
|
|
1022
|
-
let decoded: string;
|
|
1023
|
-
try {
|
|
1024
|
-
decoded = decodeURIComponent(urlPath);
|
|
1025
|
-
} catch {
|
|
1026
|
-
return null;
|
|
1027
|
-
}
|
|
1028
|
-
if (decoded.includes("\0")) return null;
|
|
1029
|
-
const normalizedBase = path.resolve(baseDir);
|
|
1030
|
-
const rel = decoded.replace(/^[/\\]+/, "");
|
|
1031
|
-
const resolved = path.resolve(normalizedBase, rel);
|
|
1032
|
-
if (resolved === normalizedBase) return resolved;
|
|
1033
|
-
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : `${normalizedBase}${path.sep}`;
|
|
1034
|
-
if (!resolved.startsWith(baseWithSep)) return null;
|
|
1035
|
-
return resolved;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
1022
|
#makeProxyHeaders(req: Request, childIdx: number, server?: Bun.Server<GatewayWsData>) {
|
|
1039
1023
|
return makeAkanChildProxyHeaders(req, childIdx, server?.requestIP(req));
|
|
1040
1024
|
}
|
package/server/akanAppHeaders.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TrustedProxy } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
4
4
|
"connection",
|
|
@@ -23,10 +23,16 @@ export function makeAkanChildProxyHeaders(req: Request, childIdx: number, peer?:
|
|
|
23
23
|
for (const key of HOP_BY_HOP_HEADERS) headers.delete(key);
|
|
24
24
|
const forwardedFor = headers.get("x-forwarded-for");
|
|
25
25
|
|
|
26
|
-
const clientAddress =
|
|
26
|
+
const clientAddress = TrustedProxy.clientAddress(headers, peer?.address);
|
|
27
27
|
const host = headers.get("host");
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
|
|
29
|
+
if (clientAddress) {
|
|
30
|
+
headers.set("x-real-ip", clientAddress);
|
|
31
|
+
headers.set("x-forwarded-for", forwardedFor ? `${forwardedFor}, ${clientAddress}` : clientAddress);
|
|
32
|
+
} else {
|
|
33
|
+
headers.delete("x-real-ip");
|
|
34
|
+
headers.delete("x-forwarded-for");
|
|
35
|
+
}
|
|
30
36
|
if (peer && !headers.has("x-forwarded-port")) headers.set("x-forwarded-port", String(peer.port));
|
|
31
37
|
headers.set("x-forwarded-host", headers.get("x-forwarded-host") ?? host ?? new URL(req.url).host);
|
|
32
38
|
headers.set(
|
package/server/akanOption.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BackendEnv, PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import type { Adaptor, AdaptorCls, LlmOption } from "akanjs/service";
|
|
3
|
-
import type { GuardCls, MiddlewareCls } from "akanjs/signal";
|
|
3
|
+
import type { CrossSiteOption, GuardCls, MiddlewareCls } from "akanjs/signal";
|
|
4
4
|
import type { McpServerOption } from "./akanServer";
|
|
5
5
|
import type { WebProxyRegistration } from "./proxy";
|
|
6
6
|
import { HostBasePathWebProxy, LocaleWebProxy } from "./proxy";
|
|
@@ -22,6 +22,7 @@ export class AkanOption<Env extends BackendEnv = BackendEnv> {
|
|
|
22
22
|
readonly #getLlms: ((env: Env) => LlmOption)[] = [];
|
|
23
23
|
#mcp: boolean | McpServerOption | undefined;
|
|
24
24
|
#agentAccess: GuardCls | GuardCls[] | null | undefined;
|
|
25
|
+
#crossSite: CrossSiteOption | undefined;
|
|
25
26
|
constructor() {
|
|
26
27
|
this.#getUses = [];
|
|
27
28
|
}
|
|
@@ -62,6 +63,15 @@ export class AkanOption<Env extends BackendEnv = BackendEnv> {
|
|
|
62
63
|
this.#agentAccess = guards;
|
|
63
64
|
return this;
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Which other origins may drive a mutation, on top of the one serving the request and the native shells.
|
|
68
|
+
* Needed only by a browser client hosted somewhere else — a separate admin domain, a partner embed. The gate
|
|
69
|
+
* itself is on by default and `{ enabled: false }` is for an API no browser reaches.
|
|
70
|
+
*/
|
|
71
|
+
setCrossSite(crossSite: CrossSiteOption) {
|
|
72
|
+
this.#crossSite = crossSite;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
65
75
|
/** Settings for whichever adaptor fills `LlmAdaptorRole`, injected into it as the `llmOption` use. */
|
|
66
76
|
setLlm(llmOrFn: LlmOption | ((env: Env) => LlmOption)) {
|
|
67
77
|
if (typeof llmOrFn === "function") this.#getLlms.push(llmOrFn);
|
|
@@ -87,6 +97,9 @@ export class AkanOption<Env extends BackendEnv = BackendEnv> {
|
|
|
87
97
|
getAgentAccess(): GuardCls | GuardCls[] | null | undefined {
|
|
88
98
|
return this.#agentAccess;
|
|
89
99
|
}
|
|
100
|
+
getCrossSite(): CrossSiteOption | undefined {
|
|
101
|
+
return this.#crossSite;
|
|
102
|
+
}
|
|
90
103
|
getLlm(env: Env): LlmOption {
|
|
91
104
|
return Object.assign({}, ...this.#getLlms.map((fn) => fn(env)));
|
|
92
105
|
}
|
package/server/akanServer.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
SolidConfig,
|
|
14
14
|
} from "akanjs/service";
|
|
15
15
|
import type { ServerSignal, ServerSignalCls, WebsocketPublishData } from "akanjs/signal";
|
|
16
|
+
import { CrossSiteGuard } from "../signal/CrossSiteGuard";
|
|
16
17
|
import { AgentRelayAccess } from "../signal/guards";
|
|
17
18
|
import { createOpenApiDocument } from "../signal/openapi";
|
|
18
19
|
import { FetchSerializer } from "../signal/serializer";
|
|
@@ -198,6 +199,8 @@ export class AkanServer {
|
|
|
198
199
|
if (mcp !== undefined) this.setMcp(mcp);
|
|
199
200
|
const agentAccess = lib.option.getAgentAccess();
|
|
200
201
|
if (agentAccess !== undefined) AgentRelayAccess.use(agentAccess);
|
|
202
|
+
const crossSite = lib.option.getCrossSite();
|
|
203
|
+
if (crossSite !== undefined) CrossSiteGuard.configure(crossSite);
|
|
201
204
|
});
|
|
202
205
|
this.setMcp(options?.mcp ?? this.mcp);
|
|
203
206
|
this.serverMode = serverMode;
|
package/server/cachePolicy.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { originFromRequest } from "akanjs/common";
|
|
1
2
|
import { type AkanDynamicUsage, type AkanRequestPolicy, parseCookieHeader } from "akanjs/fetch";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_ROUTE_CACHE_TTL_SECONDS = 30;
|
|
@@ -93,17 +94,7 @@ export function combineMinRevalidate(...values: Array<number | false | null | un
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export function getClientFacingOrigin(request: Request, url = new URL(request.url)): string {
|
|
96
|
-
|
|
97
|
-
const forwardedHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
98
|
-
const host = forwardedHost ?? request.headers.get("host")?.split(",")[0]?.trim();
|
|
99
|
-
const proto = forwardedProto ?? url.protocol.slice(0, -1);
|
|
100
|
-
if (host && proto) {
|
|
101
|
-
try {
|
|
102
|
-
return new URL(`${proto}://${host}`).origin;
|
|
103
|
-
} catch {
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return url.origin;
|
|
97
|
+
return originFromRequest(request.headers, url);
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
export function isPublicRouteCacheableRequest(request: Request): boolean {
|
package/server/decorators.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Method decorators for a server class: `@Transaction`, `@Try`, `@CacheMethod`. Kept because `libs/util`'s
|
|
3
|
+
* storage adaptors use `@Try` on their remote calls; new code follows the adaptor rule instead
|
|
4
|
+
* (`catch` → `logger.error` → `return null`).
|
|
5
|
+
*/
|
|
2
6
|
|
|
3
7
|
type DecoratedInstance = {
|
|
4
8
|
logger?: { warn?: (message: string) => void; trace?: (message: string) => void };
|
|
@@ -50,8 +54,13 @@ export const Transaction = (): MethodDecorator => {
|
|
|
50
54
|
}) as MethodDecorator;
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Method decorator that caches method results for the given timeout in milliseconds.
|
|
59
|
+
*
|
|
60
|
+
* Not `Cache` — `akanjs/signal` exports a `Cache` **middleware class**, and an app importing both barrels got
|
|
61
|
+
* one name meaning two things.
|
|
62
|
+
*/
|
|
63
|
+
export const CacheMethod = (timeout = 1000, getCacheKey?: (...args: unknown[]) => string): MethodDecorator => {
|
|
55
64
|
return ((_target: unknown, key: string, descriptor: PropertyDescriptor) => {
|
|
56
65
|
const originMethod = descriptor.value as (this: unknown, ...args: unknown[]) => unknown;
|
|
57
66
|
const cacheMap = new Map<string, unknown>();
|
|
@@ -66,7 +66,14 @@ export class ConstantSerializer {
|
|
|
66
66
|
values[key] = DevtoolsJson.toSafe(value);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
models,
|
|
71
|
+
scalars,
|
|
72
|
+
enums,
|
|
73
|
+
values,
|
|
74
|
+
primitives: PrimitiveRegistry.getNames().sort((a, b) => a.localeCompare(b)),
|
|
75
|
+
relations,
|
|
76
|
+
};
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
static #serializeView(modelRef: ConstantCls | undefined, modelType: ConstantType): ConstantModelView {
|
package/server/di/diLifecycle.ts
CHANGED
|
@@ -267,8 +267,28 @@ export class DiLifecycle {
|
|
|
267
267
|
return selected;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* Run every init stage in dependency order and collect the generated routes.
|
|
272
|
+
*
|
|
273
|
+
* A stage runs its tasks in parallel and reports every failure, which means the ones that *succeeded*
|
|
274
|
+
* alongside a failure are live: connections opened, timers armed, `onInit` done. The error then propagates and
|
|
275
|
+
* the process usually exits, so this rarely mattered — but a caller that catches and retries (a test harness,
|
|
276
|
+
* a dev restart) accumulated them. `destroyAll` already walks the stages in reverse and skips what was never
|
|
277
|
+
* registered, so the wind-down is the one that already exists.
|
|
278
|
+
*/
|
|
271
279
|
async initializeAll(): Promise<SignalRoutes> {
|
|
280
|
+
try {
|
|
281
|
+
return await this.#initializeAll();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
await this.destroyAll().catch((destroyError: unknown) => {
|
|
284
|
+
|
|
285
|
+
this.logger.warn(`Failed to unwind a partial init: ${reasonMessage(destroyError)}`);
|
|
286
|
+
});
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async #initializeAll(): Promise<SignalRoutes> {
|
|
272
292
|
await this.#initializeUses();
|
|
273
293
|
await this.#initializeAdaptor();
|
|
274
294
|
await this.#initializeServerSignal();
|
|
@@ -37,8 +37,8 @@ export function topologicalStages<T extends DependencyNode>(graph: Map<string, T
|
|
|
37
37
|
|
|
38
38
|
const next: string[] = [];
|
|
39
39
|
for (const key of queue) {
|
|
40
|
-
for (const dependent of dependents.get(key)
|
|
41
|
-
const newDegree = inDegree.get(dependent)
|
|
40
|
+
for (const dependent of dependents.get(key) ?? []) {
|
|
41
|
+
const newDegree = (inDegree.get(dependent) ?? 0) - 1;
|
|
42
42
|
inDegree.set(dependent, newDegree);
|
|
43
43
|
if (newDegree === 0) next.push(dependent);
|
|
44
44
|
}
|
|
@@ -47,7 +47,7 @@ export function topologicalStages<T extends DependencyNode>(graph: Map<string, T
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (processed !== graph.size) {
|
|
50
|
-
const remaining = [...graph.keys()].filter((k) => inDegree.get(k)
|
|
50
|
+
const remaining = [...graph.keys()].filter((k) => (inDegree.get(k) ?? 0) > 0);
|
|
51
51
|
const cycle = traceCycle(graph, remaining);
|
|
52
52
|
throw new Error(`Circular dependency detected: ${cycle.join(" → ")}`);
|
|
53
53
|
}
|
package/server/imageOptimizer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Logger } from "akanjs/common";
|
|
|
5
5
|
import { isAnimatedImage } from "./animatedImage";
|
|
6
6
|
import { ImageOptimizerError } from "./imageOptimizerError";
|
|
7
7
|
import { Semaphore } from "./semaphore";
|
|
8
|
+
import { resolveStaticPath } from "./staticPath";
|
|
8
9
|
import {
|
|
9
10
|
type AkanImageConfig,
|
|
10
11
|
type AkanImageFormat,
|
|
@@ -260,7 +261,7 @@ export class ImageOptimizer {
|
|
|
260
261
|
|
|
261
262
|
async #readLocalImage(href: string): Promise<ImageSource> {
|
|
262
263
|
const url = new URL(href, "http://local.akan");
|
|
263
|
-
const filePath =
|
|
264
|
+
const filePath = resolveStaticPath(this.#publicDir, url.pathname);
|
|
264
265
|
if (!filePath) throw new ImageOptimizerError(400, '"url" parameter is not allowed');
|
|
265
266
|
const file = Bun.file(filePath);
|
|
266
267
|
if (!(await file.exists())) throw new ImageOptimizerError(404, "Image not found");
|
|
@@ -455,22 +456,6 @@ export class ImageOptimizer {
|
|
|
455
456
|
}
|
|
456
457
|
}
|
|
457
458
|
|
|
458
|
-
static #safeResolve(baseDir: string, urlPath: string): string | null {
|
|
459
|
-
let decoded: string;
|
|
460
|
-
try {
|
|
461
|
-
decoded = decodeURIComponent(urlPath);
|
|
462
|
-
} catch {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
if (decoded.includes("\0")) return null;
|
|
466
|
-
const normalizedBase = path.resolve(baseDir);
|
|
467
|
-
const rel = decoded.replace(/^[/\\]+/, "");
|
|
468
|
-
const resolved = path.resolve(normalizedBase, rel);
|
|
469
|
-
if (resolved === normalizedBase) return resolved;
|
|
470
|
-
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
471
|
-
return resolved.startsWith(baseWithSep) ? resolved : null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
459
|
static async #readResponseBuffer(res: Response, maxBytes: number): Promise<Buffer> {
|
|
475
460
|
const contentLength = Number.parseInt(res.headers.get("content-length") ?? "0", 10);
|
|
476
461
|
if (contentLength > maxBytes) throw new ImageOptimizerError(413, "Remote image is too large");
|
|
@@ -6,6 +6,7 @@ import { NoDocumentError } from "akanjs/document";
|
|
|
6
6
|
import type { InjectRegistry, LiveRegistry } from "akanjs/service";
|
|
7
7
|
import type { Endpoint, EndpointCls } from "../../signal/endpoint";
|
|
8
8
|
import type { EndpointInfo } from "../../signal/endpointInfo";
|
|
9
|
+
import { Exception } from "../../signal/exception";
|
|
9
10
|
import type { GuardCls } from "../../signal/guard";
|
|
10
11
|
import {
|
|
11
12
|
McpDocument,
|
|
@@ -277,9 +278,14 @@ export class McpDispatcher {
|
|
|
277
278
|
if (!modelType) return value;
|
|
278
279
|
try {
|
|
279
280
|
return mask(ConstantRegistry.getModelRef(refName, modelType), value);
|
|
280
|
-
} catch {
|
|
281
|
+
} catch (error) {
|
|
281
282
|
|
|
282
|
-
|
|
283
|
+
McpDispatcher.logger.error(
|
|
284
|
+
`MCP could not mask a "${refName}" (${modelType}) result, so it was not sent: ${
|
|
285
|
+
error instanceof Error ? error.message : String(error)
|
|
286
|
+
}`,
|
|
287
|
+
);
|
|
288
|
+
throw new Exception.Error("The server failed to complete this request.");
|
|
283
289
|
}
|
|
284
290
|
}
|
|
285
291
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import { applyMixins, capitalize } from "akanjs/common";
|
|
3
|
-
import { type ConstantModel,
|
|
3
|
+
import { type ConstantModel, type QueryOf, resolvePageLimit, resolvePageSkip } from "akanjs/constant";
|
|
4
4
|
import {
|
|
5
5
|
assertFilterFitsCrud,
|
|
6
6
|
CacheDatabase,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
DatabaseAdaptorRole,
|
|
33
33
|
type DocumentStore,
|
|
34
34
|
} from "akanjs/service";
|
|
35
|
-
import { getCurrentTrace, traceDataLoaderBatch } from "akanjs/signal";
|
|
35
|
+
import { Exception, getCurrentTrace, traceDataLoaderBatch } from "akanjs/signal";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Times a store query and records it against the active request trace (no-op when
|
|
@@ -57,13 +57,18 @@ export class DatabaseResolver {
|
|
|
57
57
|
): { adaptor: AdaptorCls<DatabaseInstance>; schema: DocumentSchema } {
|
|
58
58
|
const [modelName, className]: [string, string] = [database.refName, capitalize(database.refName)];
|
|
59
59
|
|
|
60
|
-
const resolveSort = (sortKey?: string | null) =>
|
|
61
|
-
|
|
60
|
+
const resolveSort = (sortKey?: string | null): { [key: string]: 1 | -1 } | null => {
|
|
61
|
+
if (!sortKey) return null;
|
|
62
|
+
const sort = getFilterSortByKey(database.filter, sortKey) as { [key: string]: 1 | -1 } | undefined;
|
|
63
|
+
if (!sort) throw new Exception.BadRequest(`Unknown sort key for ${modelName}: ${sortKey}`);
|
|
64
|
+
return sort;
|
|
65
|
+
};
|
|
62
66
|
const getListQuery = (query?: QueryOf<any>, queryOption?: ListQueryOption) => {
|
|
63
67
|
const find = query ?? {};
|
|
64
68
|
const sort = resolveSort(queryOption?.sort);
|
|
65
|
-
const skip =
|
|
66
|
-
|
|
69
|
+
const skip = resolvePageSkip(queryOption?.skip);
|
|
70
|
+
|
|
71
|
+
const limit = queryOption?.limit === undefined ? 0 : resolvePageLimit(queryOption.limit);
|
|
67
72
|
const select = queryOption?.select;
|
|
68
73
|
const sample = queryOption?.sample;
|
|
69
74
|
return { find, sort, skip, limit, select, sample };
|
|
@@ -71,7 +76,7 @@ export class DatabaseResolver {
|
|
|
71
76
|
const getFindQuery = (query?: QueryOf<any>, queryOption?: FindQueryOption) => {
|
|
72
77
|
const find = query ?? {};
|
|
73
78
|
const sort = resolveSort(queryOption?.sort);
|
|
74
|
-
const skip =
|
|
79
|
+
const skip = resolvePageSkip(queryOption?.skip);
|
|
75
80
|
const select = queryOption?.select;
|
|
76
81
|
const sample = queryOption?.sample ?? false;
|
|
77
82
|
return { find, sort, skip, select, sample };
|
|
@@ -316,7 +321,7 @@ export class DatabaseResolver {
|
|
|
316
321
|
return this.__get(id);
|
|
317
322
|
}
|
|
318
323
|
async __load(id?: string) {
|
|
319
|
-
return (id ? await this.__loader.load(id) : null) as
|
|
324
|
+
return (id ? await this.__loader.load(id) : null) as unknown;
|
|
320
325
|
}
|
|
321
326
|
async [`load${className}`](id?: string) {
|
|
322
327
|
return this.__load(id);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SLICE_META,
|
|
14
14
|
} from "akanjs/base";
|
|
15
15
|
import { capitalize, Logger } from "akanjs/common";
|
|
16
|
-
import { deserialize, serialize } from "akanjs/constant";
|
|
16
|
+
import { deserialize, resolvePageLimit, resolvePageSkip, serialize } from "akanjs/constant";
|
|
17
17
|
import { documentQueryHelper } from "akanjs/document";
|
|
18
18
|
import {
|
|
19
19
|
type AkanJob,
|
|
@@ -275,8 +275,8 @@ export class SignalResolver {
|
|
|
275
275
|
._addInternalArgs(sliceInfo.internalArgs)
|
|
276
276
|
.exec(async function (this: any, ...requestArgs: any) {
|
|
277
277
|
const args = requestArgs.slice(0, argLength);
|
|
278
|
-
const skip =
|
|
279
|
-
const limit =
|
|
278
|
+
const skip = resolvePageSkip(requestArgs[argLength]);
|
|
279
|
+
const limit = resolvePageLimit(requestArgs[argLength + 1]);
|
|
280
280
|
const sort = requestArgs[argLength + 2] ?? "latest";
|
|
281
281
|
const internalArgs = requestArgs.slice(argLength + 3);
|
|
282
282
|
const query = assertSliceQuery(
|
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
LayoutModule,
|
|
4
4
|
LayoutProps,
|
|
5
5
|
PageProps,
|
|
6
|
-
PageState,
|
|
7
6
|
PathRoute,
|
|
8
7
|
ResolveHead,
|
|
9
8
|
Route,
|
|
@@ -20,26 +19,13 @@ import {
|
|
|
20
19
|
routeSegmentToTreePath,
|
|
21
20
|
} from "akanjs/common";
|
|
22
21
|
import { createElement } from "react";
|
|
23
|
-
import { validatePageConfig } from "../client/frameConfig";
|
|
22
|
+
import { defaultPageState, validatePageConfig } from "../client/frameConfig";
|
|
24
23
|
import { resolveHeadExport, resolveMetadataHead } from "./metadata";
|
|
25
24
|
|
|
26
25
|
type RouteModuleKindWithOverrides = "page" | "layout" | "overrides";
|
|
27
26
|
|
|
28
27
|
export type PagesContext = Record<string, () => Promise<RouteModule>>;
|
|
29
28
|
|
|
30
|
-
export const defaultPageState: PageState = {
|
|
31
|
-
transition: "none",
|
|
32
|
-
topSafeArea: 0,
|
|
33
|
-
bottomSafeArea: 0,
|
|
34
|
-
topInset: 0,
|
|
35
|
-
bottomInset: 0,
|
|
36
|
-
gesture: true,
|
|
37
|
-
cache: false,
|
|
38
|
-
ssr: "stream",
|
|
39
|
-
topSafeAreaColor: "transparent",
|
|
40
|
-
bottomSafeAreaColor: "transparent",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
29
|
export interface RouteModuleCacheStats {
|
|
44
30
|
moduleCount: number;
|
|
45
31
|
loadedModuleCount: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dayjs } from "akanjs/base";
|
|
2
2
|
import { type Logger, websocketAuthContract } from "akanjs/common";
|
|
3
3
|
import type { InjectRegistry } from "akanjs/service";
|
|
4
|
-
import { Exception, SignalContext, type WebsocketReqData } from "akanjs/signal";
|
|
4
|
+
import { Exception, SignalContext, SignalFailure, type WebsocketReqData } from "akanjs/signal";
|
|
5
5
|
import { compressResponse } from "../contentEncoding";
|
|
6
6
|
import type { HmrWsData, HmrWsHub } from "../hmr/wsHub";
|
|
7
7
|
import { copyBunRequestFields, type WebProxyRunner } from "../proxy";
|
|
@@ -166,12 +166,11 @@ export class ApiRouter {
|
|
|
166
166
|
ws.send(JSON.stringify({ ...error.toJSON(), timestamp: new Date().toISOString() }));
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
170
169
|
if (!SignalContext.wasReported(error)) {
|
|
171
|
-
logger.error(
|
|
172
|
-
console.error(error);
|
|
170
|
+
logger.error(error instanceof Error ? (error.stack ?? error.message) : String(error));
|
|
173
171
|
}
|
|
174
|
-
|
|
172
|
+
|
|
173
|
+
ws.send(JSON.stringify({ ...SignalFailure.body(error), at: dayjs() }));
|
|
175
174
|
}
|
|
176
175
|
},
|
|
177
176
|
close: (ws) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clientAddressFromHeaders, clientPortFromHeaders, forwardedHeaders,
|
|
1
|
+
import { clientAddressFromHeaders, clientPortFromHeaders, forwardedHeaders, TrustedProxy } from "akanjs/common";
|
|
2
2
|
|
|
3
3
|
const CREDENTIAL_HEADERS = ["authorization", "cookie", "user-agent"] as const;
|
|
4
4
|
|
|
@@ -36,14 +36,11 @@ export class AppWsData {
|
|
|
36
36
|
else data.headers.delete("cookie");
|
|
37
37
|
}
|
|
38
38
|
data.account = undefined;
|
|
39
|
-
data.resolvedAuthorization = undefined;
|
|
40
39
|
}
|
|
41
40
|
createdAt: number;
|
|
42
41
|
headers: Headers;
|
|
43
42
|
cookies: Bun.CookieMap;
|
|
44
43
|
account?: unknown;
|
|
45
|
-
/** The `authorization` value `account` was resolved from, so each frame need not re-verify it. */
|
|
46
|
-
resolvedAuthorization?: string;
|
|
47
44
|
/**
|
|
48
45
|
* Identity of this connection, minted here so every app socket carries one from its first frame and
|
|
49
46
|
* adaptors and endpoints only ever read it. Per-connection and process-local — a reconnect gets a new
|
|
@@ -51,7 +48,13 @@ export class AppWsData {
|
|
|
51
48
|
* It outlives a credential swap on purpose: the socket is still the same socket.
|
|
52
49
|
*/
|
|
53
50
|
socketId: string;
|
|
54
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The caller's address as the nearest proxy recorded it, or null when nothing did.
|
|
53
|
+
*
|
|
54
|
+
* The handshake headers alone cannot say whether a proxy wrote them or the client did, so this trusts them —
|
|
55
|
+
* a socket that reached a child came through the gateway, which already resolved the question. `ipOf` is what
|
|
56
|
+
* an endpoint should read: it has the peer, so it can decide.
|
|
57
|
+
*/
|
|
55
58
|
get ip(): string | null {
|
|
56
59
|
return clientAddressFromHeaders(this.headers);
|
|
57
60
|
}
|
|
@@ -61,7 +64,7 @@ export class AppWsData {
|
|
|
61
64
|
}
|
|
62
65
|
/** The address to answer on: what a proxy recorded, else this socket's own peer. */
|
|
63
66
|
ipOf(ws: Bun.ServerWebSocket<unknown>): string | null {
|
|
64
|
-
return
|
|
67
|
+
return TrustedProxy.clientAddress(this.headers, ws.remoteAddress);
|
|
65
68
|
}
|
|
66
69
|
constructor(headers: Headers) {
|
|
67
70
|
this.createdAt = Date.now();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The file a URL path names inside one directory, or `null` when it names something outside it.
|
|
5
|
+
*
|
|
6
|
+
* Every static route the web router serves goes through here, so this is the single place a request path stops
|
|
7
|
+
* being a string and becomes a file. The order matters: percent-decoding first (so `%2e%2e` is seen as `..`),
|
|
8
|
+
* then a NUL check (a truncating byte in a path), then `path.resolve` to collapse the traversal, then
|
|
9
|
+
* containment against the base **with a separator** — without it `/public-secrets` passes a `/public` check.
|
|
10
|
+
*
|
|
11
|
+
* Symlinks are followed, like nginx and `serve-static` do: `akan sync` builds `public/libs/<lib>` as a link to
|
|
12
|
+
* `<workspaceRoot>/libs/<lib>/public`, so a link out of the tree is how a lib ships its assets, not an attack.
|
|
13
|
+
* Refusing those would 404 every lib asset under `akan start` and nothing else — `akan build` copies `public/`
|
|
14
|
+
* with `dereference: true`, so a built app has no links left to refuse. Traversal is what the string checks
|
|
15
|
+
* above stop, and they run before any of this touches the filesystem.
|
|
16
|
+
*
|
|
17
|
+
* Callers check existence after this, so a path that resolves to nothing is handed back and 404s there.
|
|
18
|
+
*/
|
|
19
|
+
export const resolveStaticPath = (baseDir: string, urlPath: string): string | null => {
|
|
20
|
+
let decoded: string;
|
|
21
|
+
try {
|
|
22
|
+
decoded = decodeURIComponent(urlPath);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (decoded.includes("\0")) return null;
|
|
27
|
+
const normalizedBase = path.resolve(baseDir);
|
|
28
|
+
const rel = decoded.replace(/^[/\\]+/, "");
|
|
29
|
+
const resolved = path.resolve(normalizedBase, rel);
|
|
30
|
+
if (resolved === normalizedBase) return resolved;
|
|
31
|
+
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
32
|
+
if (!resolved.startsWith(baseWithSep)) return null;
|
|
33
|
+
return resolved;
|
|
34
|
+
};
|