akanjs 3.0.0-alpha.93 → 3.0.0-alpha.95
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/common/index.ts +1 -1
- package/common/requestOrigin.ts +9 -2
- package/constant/getDefault.ts +4 -0
- 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/service/predefinedAdaptor/sql/SqlDocumentStore.ts +10 -2
- package/signal/CrossSiteGuard.ts +25 -3
- package/types/common/index.d.ts +1 -1
- package/types/common/requestOrigin.d.ts +6 -0
package/common/index.ts
CHANGED
|
@@ -82,7 +82,7 @@ export { pathSet } from "./pathSet";
|
|
|
82
82
|
export { plainFieldsOf } from "./plainFieldsOf";
|
|
83
83
|
export { randomPick } from "./randomPick";
|
|
84
84
|
export { randomPicks } from "./randomPicks";
|
|
85
|
-
export { isJsonContentType, originFromRequest } from "./requestOrigin";
|
|
85
|
+
export { hostFromRequest, isJsonContentType, originFromRequest } from "./requestOrigin";
|
|
86
86
|
export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient";
|
|
87
87
|
export {
|
|
88
88
|
assertUniqueRoutePatterns,
|
package/common/requestOrigin.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
export const originFromRequest = (headers: Headers, url: URL): string => {
|
|
6
6
|
|
|
7
7
|
const forwardedProto = headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
8
|
-
const
|
|
9
|
-
const host = forwardedHost ?? headers.get("host")?.split(",")[0]?.trim();
|
|
8
|
+
const host = hostFromRequest(headers, url);
|
|
10
9
|
const proto = forwardedProto ?? url.protocol.slice(0, -1);
|
|
11
10
|
if (host && proto) {
|
|
12
11
|
try {
|
|
@@ -17,6 +16,14 @@ export const originFromRequest = (headers: Headers, url: URL): string => {
|
|
|
17
16
|
return url.origin;
|
|
18
17
|
};
|
|
19
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The host a caller addressed, which — unlike the scheme — every proxy that rewrites the request still reports.
|
|
21
|
+
* `Host` is set by the browser from the URL it was told to open, so it names *this* deployment even for a
|
|
22
|
+
* request some other page initiated.
|
|
23
|
+
*/
|
|
24
|
+
export const hostFromRequest = (headers: Headers, url: URL): string =>
|
|
25
|
+
headers.get("x-forwarded-host")?.split(",")[0]?.trim() ?? headers.get("host")?.split(",")[0]?.trim() ?? url.host;
|
|
26
|
+
|
|
20
27
|
/**
|
|
21
28
|
* Whether a request body may be read as JSON.
|
|
22
29
|
*
|
package/constant/getDefault.ts
CHANGED
|
@@ -39,6 +39,10 @@ const buildPlan = (fieldObj: FieldObject): DefaultPlan => {
|
|
|
39
39
|
if (field.fieldType === "hidden" || field.fieldType === "secret") shared[key] = null;
|
|
40
40
|
else if (field.default !== undefined && field.default !== null) {
|
|
41
41
|
if (typeof field.default === "function") perCall.set(key, field.default as () => unknown);
|
|
42
|
+
else if (Array.isArray(field.default)) {
|
|
43
|
+
const items = field.default as unknown[];
|
|
44
|
+
perCall.set(key, () => [...items]);
|
|
45
|
+
}
|
|
42
46
|
else shared[key] = field.default as object;
|
|
43
47
|
} else if (field.isArray) perCall.set(key, () => []);
|
|
44
48
|
else if (field.nullable) shared[key] = null;
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
import { UpdateCompiler } from "./UpdateCompiler";
|
|
45
45
|
import { decodeDateValue, encodeSqlValue, jsonStr } from "./values";
|
|
46
46
|
|
|
47
|
+
const freshDefault = (value: unknown) => (Array.isArray(value) ? [...(value as unknown[])] : value);
|
|
48
|
+
|
|
47
49
|
export class SqlDocumentStore {
|
|
48
50
|
readonly schema: DocumentSchema;
|
|
49
51
|
readonly table: string;
|
|
@@ -331,7 +333,10 @@ export class SqlDocumentStore {
|
|
|
331
333
|
const value = data[key];
|
|
332
334
|
if (value === undefined) {
|
|
333
335
|
if (props.default !== undefined && props.default !== null) {
|
|
334
|
-
doc[key] = typeof props.default === "function" ? props.default(data) : props.default;
|
|
336
|
+
doc[key] = freshDefault(typeof props.default === "function" ? props.default(data) : props.default);
|
|
337
|
+
} else if (props.isClass && props.isScalar && !props.nullable) {
|
|
338
|
+
|
|
339
|
+
doc[key] = getDefault((props.modelRef as { [FIELD_META]: FieldMap })[FIELD_META] as never);
|
|
335
340
|
} else if (!props.nullable && !["removedAt"].includes(key)) {
|
|
336
341
|
if (["id", "createdAt", "updatedAt"].includes(key)) continue;
|
|
337
342
|
throw new Error(`Missing required field: ${key}`);
|
|
@@ -597,9 +602,12 @@ export class SqlDocumentStore {
|
|
|
597
602
|
if (value === undefined) {
|
|
598
603
|
const def = props.default;
|
|
599
604
|
if (def != null) {
|
|
600
|
-
result[key] = typeof def === "function" ? (def as (data: unknown) => unknown)(payload) : def;
|
|
605
|
+
result[key] = freshDefault(typeof def === "function" ? (def as (data: unknown) => unknown)(payload) : def);
|
|
601
606
|
} else if (props.nullable) {
|
|
602
607
|
result[key] = null;
|
|
608
|
+
} else if (props.isClass && props.isScalar) {
|
|
609
|
+
|
|
610
|
+
result[key] = getDefault((props.modelRef as { [FIELD_META]: FieldMap })[FIELD_META] as never);
|
|
603
611
|
} else {
|
|
604
612
|
result[key] =
|
|
605
613
|
((props as Record<string, unknown>).modelRef as { [DEFAULT_VALUE]?: unknown })?.[DEFAULT_VALUE] ?? null;
|
package/signal/CrossSiteGuard.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isJsonContentType, Logger
|
|
1
|
+
import { hostFromRequest, isJsonContentType, Logger } from "akanjs/common";
|
|
2
2
|
import { Exception } from "./exception";
|
|
3
3
|
|
|
4
4
|
export interface CrossSiteOption {
|
|
@@ -62,10 +62,32 @@ export class CrossSiteGuard {
|
|
|
62
62
|
if (!CrossSiteGuard.#enabled) return;
|
|
63
63
|
const origin = req.headers.get("origin");
|
|
64
64
|
if (origin === null) return;
|
|
65
|
-
if (origin !== "null" && (origin
|
|
65
|
+
if (origin !== "null" && (CrossSiteGuard.#isSameSite(origin, req, url) || CrossSiteGuard.#allowed.has(origin)))
|
|
66
66
|
return;
|
|
67
67
|
|
|
68
|
-
CrossSiteGuard.logger.warn(
|
|
68
|
+
CrossSiteGuard.logger.warn(
|
|
69
|
+
`Refused "${key}" from cross-site origin ${origin} (request host ${hostFromRequest(req.headers, url)})`,
|
|
70
|
+
);
|
|
69
71
|
throw new Exception.Forbidden("This request was not permitted.");
|
|
70
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Host, not full origin — the same comparison `McpRouter` makes, for the same reason.
|
|
76
|
+
*
|
|
77
|
+
* The host is what the browser wrote from the URL it was told to open, so a page on another site POSTing here
|
|
78
|
+
* still arrives with *our* host and its own `Origin`: comparing hosts refuses every cross-site caller. The
|
|
79
|
+
* scheme is the one part of the origin a proxy routinely loses — a TLS-terminating edge (a Cloudflare tunnel,
|
|
80
|
+
* an ingress without `x-forwarded-proto`) dials us over plain HTTP, so an `https://` caller would be measured
|
|
81
|
+
* against a computed `http://` self and refused on every mutation. What that costs is a page served over
|
|
82
|
+
* plaintext on our own host, which is an attacker who already holds the name.
|
|
83
|
+
*/
|
|
84
|
+
static #isSameSite(origin: string, req: Request, url: URL): boolean {
|
|
85
|
+
try {
|
|
86
|
+
const { protocol, host } = new URL(origin);
|
|
87
|
+
|
|
88
|
+
return host === new URL(`${protocol}//${hostFromRequest(req.headers, url)}`).host;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
71
93
|
}
|
package/types/common/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export { pathSet } from "./pathSet.d.ts";
|
|
|
31
31
|
export { plainFieldsOf } from "./plainFieldsOf.d.ts";
|
|
32
32
|
export { randomPick } from "./randomPick.d.ts";
|
|
33
33
|
export { randomPicks } from "./randomPicks.d.ts";
|
|
34
|
-
export { isJsonContentType, originFromRequest } from "./requestOrigin.d.ts";
|
|
34
|
+
export { hostFromRequest, isJsonContentType, originFromRequest } from "./requestOrigin.d.ts";
|
|
35
35
|
export { RestClient, type RestClientOptions, type RestRequestOptions } from "./restClient.d.ts";
|
|
36
36
|
export { assertUniqueRoutePatterns, compareRouteSpecificity, getPageSourceFileViolation, getRouteExports, isRouteSourceFile, isSpecialRouteLeaf, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
|
|
37
37
|
export { sleep } from "./sleep.d.ts";
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* they answer the same way for the cache layer, the CSRF gate, and anything else that has to compare origins.
|
|
4
4
|
*/
|
|
5
5
|
export declare const originFromRequest: (headers: Headers, url: URL) => string;
|
|
6
|
+
/**
|
|
7
|
+
* The host a caller addressed, which — unlike the scheme — every proxy that rewrites the request still reports.
|
|
8
|
+
* `Host` is set by the browser from the URL it was told to open, so it names *this* deployment even for a
|
|
9
|
+
* request some other page initiated.
|
|
10
|
+
*/
|
|
11
|
+
export declare const hostFromRequest: (headers: Headers, url: URL) => string;
|
|
6
12
|
/**
|
|
7
13
|
* Whether a request body may be read as JSON.
|
|
8
14
|
*
|