akanjs 3.0.0-alpha.71 → 3.0.0-alpha.72
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/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/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 +39 -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 +13 -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/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
|
}
|
|
@@ -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,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The file a URL path names inside one directory, or `null` when it names something outside it.
|
|
6
|
+
*
|
|
7
|
+
* Every static route the web router serves goes through here, so this is the single place a request path stops
|
|
8
|
+
* being a string and becomes a file. The order matters: percent-decoding first (so `%2e%2e` is seen as `..`),
|
|
9
|
+
* then a NUL check (a truncating byte in a path), then `path.resolve` to collapse the traversal, then
|
|
10
|
+
* containment against the base **with a separator** — without it `/public-secrets` passes a `/public` check.
|
|
11
|
+
*
|
|
12
|
+
* `path.resolve` works on the string and does not follow links, so a symlink inside the tree can still point
|
|
13
|
+
* out of it. Callers check existence after this, so a path that resolves to nothing is handed back and 404s
|
|
14
|
+
* there; only a link that really leads outside is refused.
|
|
15
|
+
*/
|
|
16
|
+
export const resolveStaticPath = (baseDir: string, urlPath: string): string | null => {
|
|
17
|
+
let decoded: string;
|
|
18
|
+
try {
|
|
19
|
+
decoded = decodeURIComponent(urlPath);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (decoded.includes("\0")) return null;
|
|
24
|
+
const normalizedBase = path.resolve(baseDir);
|
|
25
|
+
const rel = decoded.replace(/^[/\\]+/, "");
|
|
26
|
+
const resolved = path.resolve(normalizedBase, rel);
|
|
27
|
+
if (resolved === normalizedBase) return resolved;
|
|
28
|
+
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
29
|
+
if (!resolved.startsWith(baseWithSep)) return null;
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
const realBase = fs.realpathSync(normalizedBase);
|
|
33
|
+
const realBaseWithSep = realBase.endsWith(path.sep) ? realBase : realBase + path.sep;
|
|
34
|
+
const real = fs.realpathSync(resolved);
|
|
35
|
+
if (real !== realBase && !real.startsWith(realBaseWithSep)) return null;
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
};
|
package/server/webRouter.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { type RscRedirectMethod, type RscRedirectStatus, type RscRenderResult, R
|
|
|
55
55
|
import { createDefaultSitemapXml, getSitemapBasePath } from "./sitemap";
|
|
56
56
|
import { SsrFromRscRenderer } from "./ssrFromRscRenderer";
|
|
57
57
|
import type { RscTraceMetadata, SsrManifest } from "./ssrTypes";
|
|
58
|
+
import { resolveStaticPath } from "./staticPath";
|
|
58
59
|
import { createSubRouteIndexResponse, createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
|
|
59
60
|
import { type BaseBuildArtifact, type HttpRoutes, type RenderState, resolveWebConfig } from "./types";
|
|
60
61
|
|
|
@@ -411,16 +412,14 @@ export class WebRouter {
|
|
|
411
412
|
<script type="module" src="/csr.js"></script>
|
|
412
413
|
</body>
|
|
413
414
|
</html>`;
|
|
414
|
-
return new Response(this.#withCsrHmr(htmlText), {
|
|
415
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
416
|
-
});
|
|
415
|
+
return new Response(this.#withCsrHmr(htmlText), { headers: WebRouter.#htmlResponseHeaders(200) });
|
|
417
416
|
},
|
|
418
417
|
}
|
|
419
418
|
: {}),
|
|
420
419
|
[`${clientServePrefix}/*`]: async (req) => {
|
|
421
420
|
this.#requestStats.staticAsset += 1;
|
|
422
421
|
const url = new URL(req.url);
|
|
423
|
-
const filePath =
|
|
422
|
+
const filePath = resolveStaticPath(clientOutputDir, url.pathname.slice(clientServePrefix.length + 1));
|
|
424
423
|
if (!filePath) return new Response("Not Found", { status: 404 });
|
|
425
424
|
return WebRouter.#fileResponse(req, filePath, {
|
|
426
425
|
contentType: Bun.file(filePath).type || "application/javascript",
|
|
@@ -431,7 +430,7 @@ export class WebRouter {
|
|
|
431
430
|
this.#requestStats.staticAsset += 1;
|
|
432
431
|
const url = new URL(req.url);
|
|
433
432
|
if (this.#prodMode) {
|
|
434
|
-
const filePath =
|
|
433
|
+
const filePath = resolveStaticPath(this.#artifactDir, url.pathname.slice("/_akan/".length));
|
|
435
434
|
if (filePath) {
|
|
436
435
|
return WebRouter.#fileResponse(req, filePath, {
|
|
437
436
|
contentType: "text/css; charset=utf-8",
|
|
@@ -449,7 +448,7 @@ export class WebRouter {
|
|
|
449
448
|
"/_akan/fonts/*": (req) => {
|
|
450
449
|
this.#requestStats.staticAsset += 1;
|
|
451
450
|
const url = new URL(req.url);
|
|
452
|
-
const filePath =
|
|
451
|
+
const filePath = resolveStaticPath(this.#artifactDir, url.pathname.slice("/_akan/".length));
|
|
453
452
|
if (!filePath) return new Response("Not Found", { status: 404 });
|
|
454
453
|
return WebRouter.#fileResponse(req, filePath, {
|
|
455
454
|
contentType: Bun.file(filePath).type || "font/woff2",
|
|
@@ -524,10 +523,10 @@ export class WebRouter {
|
|
|
524
523
|
this.#requestStats.staticAsset += 1;
|
|
525
524
|
const firebaseConfig = await WebRouter.#resolveFirebaseClientConfig();
|
|
526
525
|
return new Response(WebRouter.#createFirebaseMessagingServiceWorker(firebaseConfig), {
|
|
527
|
-
headers: {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
},
|
|
526
|
+
headers: WebRouter.#baseAssetHeaders({
|
|
527
|
+
contentType: "application/javascript; charset=utf-8",
|
|
528
|
+
cacheControl: "no-store",
|
|
529
|
+
}),
|
|
531
530
|
});
|
|
532
531
|
},
|
|
533
532
|
"/*": async (req) => {
|
|
@@ -544,12 +543,10 @@ export class WebRouter {
|
|
|
544
543
|
const csrHtml = await this.#resolveCsrHtml(csrOutputDir, url.pathname);
|
|
545
544
|
if (!csrHtml) return this.#csrUnavailableResponse(url.pathname);
|
|
546
545
|
const html = await Bun.file(csrHtml).text();
|
|
547
|
-
return new Response(this.#withCsrHmr(html), {
|
|
548
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
549
|
-
});
|
|
546
|
+
return new Response(this.#withCsrHmr(html), { headers: WebRouter.#htmlResponseHeaders(200) });
|
|
550
547
|
}
|
|
551
548
|
|
|
552
|
-
const csrAssetPath = path.extname(url.pathname) ?
|
|
549
|
+
const csrAssetPath = path.extname(url.pathname) ? resolveStaticPath(csrOutputDir, url.pathname) : null;
|
|
553
550
|
if (csrAssetPath && (await Bun.file(csrAssetPath).exists())) {
|
|
554
551
|
this.#requestStats.staticAsset += 1;
|
|
555
552
|
return WebRouter.#fileResponse(req, csrAssetPath, {
|
|
@@ -559,7 +556,7 @@ export class WebRouter {
|
|
|
559
556
|
}
|
|
560
557
|
}
|
|
561
558
|
|
|
562
|
-
const filePath =
|
|
559
|
+
const filePath = resolveStaticPath(publicDir, url.pathname);
|
|
563
560
|
if (filePath) {
|
|
564
561
|
if (await Bun.file(filePath).exists()) {
|
|
565
562
|
this.#requestStats.staticAsset += 1;
|
|
@@ -607,12 +604,9 @@ export class WebRouter {
|
|
|
607
604
|
const htmlCacheEntry = htmlCacheDecision.entry;
|
|
608
605
|
const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
|
|
609
606
|
if (cachedHtml) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
"X-Akan-Cache": "HIT",
|
|
614
|
-
},
|
|
615
|
-
});
|
|
607
|
+
const cachedHeaders = WebRouter.#htmlResponseHeaders(200);
|
|
608
|
+
cachedHeaders.set("X-Akan-Cache", "HIT");
|
|
609
|
+
return new Response(cachedHtml, { headers: cachedHeaders });
|
|
616
610
|
}
|
|
617
611
|
const rscResult = await this.#rsc.renderWithMeta(req, {
|
|
618
612
|
clientManifest: manifest.clientManifest,
|
|
@@ -993,7 +987,7 @@ export class WebRouter {
|
|
|
993
987
|
static #htmlResponseHeaders(status: number): Headers {
|
|
994
988
|
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
995
989
|
if (status >= 400) headers.set("Cache-Control", "no-store");
|
|
996
|
-
return headers;
|
|
990
|
+
return WebRouter.#applySecurityHeaders(headers, { html: true });
|
|
997
991
|
}
|
|
998
992
|
#withCsrHmr(html: string): string {
|
|
999
993
|
if (this.#prodMode) return html;
|
|
@@ -1249,7 +1243,7 @@ self.addEventListener("notificationclick", (event) => {
|
|
|
1249
1243
|
i18n: artifact.i18n,
|
|
1250
1244
|
});
|
|
1251
1245
|
const filename = basePath ? `${basePath}.html` : "index.html";
|
|
1252
|
-
const filePath =
|
|
1246
|
+
const filePath = resolveStaticPath(csrOutputDir, filename);
|
|
1253
1247
|
return filePath && fs.existsSync(filePath) ? filePath : null;
|
|
1254
1248
|
}
|
|
1255
1249
|
|
|
@@ -1334,10 +1328,27 @@ self.addEventListener("notificationclick", (event) => {
|
|
|
1334
1328
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
1335
1329
|
}
|
|
1336
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* Headers every response carries, whatever it is.
|
|
1333
|
+
*
|
|
1334
|
+
* `nosniff` because `Bun.file().type` falls back to `application/octet-stream` for an extension it does not
|
|
1335
|
+
* know, and a sniffing browser then guesses again — on a `public/` tree the app serves from its own origin,
|
|
1336
|
+
* that guess is script execution. `Referrer-Policy` because route paths carry ids.
|
|
1337
|
+
*
|
|
1338
|
+
* `X-Frame-Options` is on the HTML only: framing an image or a stylesheet means nothing, and the header is
|
|
1339
|
+
* what stops a page authenticated by a `SameSite=None` cookie from being clickjacked inside somebody else's.
|
|
1340
|
+
*/
|
|
1341
|
+
static #applySecurityHeaders(headers: Headers, { html = false } = {}): Headers {
|
|
1342
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
1343
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1344
|
+
if (html) headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
1345
|
+
return headers;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1337
1348
|
static #baseAssetHeaders(options: { contentType: string; cacheControl?: string }): Headers {
|
|
1338
1349
|
const headers = new Headers({ "Content-Type": options.contentType });
|
|
1339
1350
|
if (options.cacheControl) headers.set("Cache-Control", options.cacheControl);
|
|
1340
|
-
return headers;
|
|
1351
|
+
return WebRouter.#applySecurityHeaders(headers);
|
|
1341
1352
|
}
|
|
1342
1353
|
|
|
1343
1354
|
static #weakEtag(size: number, mtimeMs: number): string {
|
|
@@ -1359,21 +1370,4 @@ self.addEventListener("notificationclick", (event) => {
|
|
|
1359
1370
|
const sinceMs = Date.parse(ifModifiedSince);
|
|
1360
1371
|
return Number.isFinite(sinceMs) && sinceMs >= lastModifiedMs;
|
|
1361
1372
|
}
|
|
1362
|
-
|
|
1363
|
-
static #safeResolve(baseDir: string, urlPath: string): string | null {
|
|
1364
|
-
let decoded: string;
|
|
1365
|
-
try {
|
|
1366
|
-
decoded = decodeURIComponent(urlPath);
|
|
1367
|
-
} catch {
|
|
1368
|
-
return null;
|
|
1369
|
-
}
|
|
1370
|
-
if (decoded.includes("\0")) return null;
|
|
1371
|
-
const normalizedBase = path.resolve(baseDir);
|
|
1372
|
-
const rel = decoded.replace(/^[/\\]+/, "");
|
|
1373
|
-
const resolved = path.resolve(normalizedBase, rel);
|
|
1374
|
-
if (resolved === normalizedBase) return resolved;
|
|
1375
|
-
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
1376
|
-
if (!resolved.startsWith(baseWithSep)) return null;
|
|
1377
|
-
return resolved;
|
|
1378
|
-
}
|
|
1379
1373
|
}
|
package/service/adapt.ts
CHANGED
|
@@ -8,11 +8,7 @@ export interface Adaptor {
|
|
|
8
8
|
onDestroy(): Promise<void> | void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export type AdaptorCls<
|
|
12
|
-
Methods = any,
|
|
13
|
-
|
|
14
|
-
InjectMap extends Record<string, InjectInfo> = {},
|
|
15
|
-
> = Cls<
|
|
11
|
+
export type AdaptorCls<Methods = any, InjectMap extends Record<string, InjectInfo> = Record<never, never>> = Cls<
|
|
16
12
|
Methods & ExtractInjectInfoObject<InjectMap> & Adaptor,
|
|
17
13
|
{ readonly [INJECT_META]: InjectMap; readonly refName: string }
|
|
18
14
|
>;
|