jskelet 0.1.1 → 0.1.3
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/AGENTS.md +5 -0
- package/CHANGELOG.md +129 -2
- package/README.md +21 -7
- package/bin/jskelet.mjs +6 -6
- package/docs/03-routing.md +48 -9
- package/docs/04-render-ve-sablonlar.md +2 -2
- package/docs/05-islands.md +59 -6
- package/docs/06-cache.md +240 -26
- package/docs/07-yapilandirma.md +108 -7
- package/docs/08-build.md +4 -4
- package/docs/09-dev-araclari.md +5 -0
- package/docs/12-panel-ve-oturum.md +384 -0
- package/docs/README.md +25 -2
- package/docs/en/01-getting-started.md +292 -0
- package/docs/en/02-architecture.md +305 -0
- package/docs/en/03-routing.md +493 -0
- package/docs/en/04-rendering.md +504 -0
- package/docs/en/05-islands.md +492 -0
- package/docs/en/06-caching.md +640 -0
- package/docs/en/07-configuration.md +789 -0
- package/docs/en/08-build.md +383 -0
- package/docs/en/09-dev-tools.md +314 -0
- package/docs/en/10-deployment.md +332 -0
- package/docs/en/11-migration.md +360 -0
- package/docs/en/12-dashboards-and-sessions.md +392 -0
- package/docs/en/README.md +112 -0
- package/package.json +4 -2
- package/src/build/build.mjs +1 -1
- package/src/build/tasks/client.mjs +2 -2
- package/src/build/tasks/fonts.mjs +3 -3
- package/src/build/tasks/icons.mjs +1 -1
- package/src/build/tasks/images.mjs +2 -2
- package/src/client/devtools/overlay.js +196 -164
- package/src/client/devtools/report.js +96 -96
- package/src/client/form.js +192 -0
- package/src/client/index.js +10 -1
- package/src/client/registry.js +78 -4
- package/src/client/swap.js +188 -0
- package/src/config/defaults.js +83 -0
- package/src/config/index.js +129 -18
- package/src/config/pattern.js +1 -1
- package/src/dev-server.mjs +1 -1
- package/src/http/control-flow.js +16 -1
- package/src/http/cookies.js +257 -0
- package/src/http/request-context.js +162 -0
- package/src/index.js +26 -2
- package/src/init.mjs +32 -31
- package/src/log.mjs +8 -2
- package/src/logo.png +0 -0
- package/src/runtime/alias-hooks.mjs +1 -1
- package/src/server/assets.js +1 -1
- package/src/server/create-app.js +12 -4
- package/src/server/data-cache.js +244 -0
- package/src/server/dev/devtools.js +6 -2
- package/src/server/dev/report.js +8 -1
- package/src/server/dev/version-check.mjs +139 -0
- package/src/server/head-hints.js +1 -1
- package/src/server/html-cache.js +32 -6
- package/src/server/middleware/csrf.js +134 -0
- package/src/server/prewarm.js +164 -19
- package/src/server/render.js +256 -20
- package/src/server/router.js +14 -7
- package/src/server/status-page.js +1 -1
- package/src/version.mjs +9 -4
- package/src/views/components/loader.js +1 -1
- package/src/views/helpers/tags.js +53 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* İstek başına yaşayan bağlam.
|
|
3
|
+
*
|
|
4
|
+
* `request-cache.js` ile aynı desen: `AsyncLocalStorage`, bağlam yoksa her şey
|
|
5
|
+
* sessizce devre dışı. Ayrı bir modül olmasının sebebi, taşıdığı bilginin
|
|
6
|
+
* memoizasyondan farklı olması — burada tutulanlar bir isteğin **cache'lenip
|
|
7
|
+
* cache'lenemeyeceğine** dair kararlar:
|
|
8
|
+
*
|
|
9
|
+
* private → route açıkça "bu sayfa kişiye özel" dedi
|
|
10
|
+
* tainted → controller cookie/Authorization okudu, yani çıktı kullanıcıya
|
|
11
|
+
* bağlı olabilir; HTML cache'e yazmak sızıntı olur
|
|
12
|
+
* csrfToken → bu istek için üretilmiş token; `csrfField()` şablonda basar
|
|
13
|
+
* res → token cookie'sini yazabilmek için; başka hiçbir şey için
|
|
14
|
+
* kullanılmaz, şablonlara yanıt nesnesi sızmaz
|
|
15
|
+
*
|
|
16
|
+
* `tainted` bir tahmin değil, gözlem: `markTainted()` yalnızca gerçekten
|
|
17
|
+
* kimliğe dokunan bir erişimden sonra çağrılır (bkz. `guardRequest`).
|
|
18
|
+
*/
|
|
19
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} RequestContext
|
|
23
|
+
* @property {boolean} private Route `private: true` ile mi kaydedildi.
|
|
24
|
+
* @property {boolean} tainted Kimliğe bağlı bir veri okundu mu.
|
|
25
|
+
* @property {string[]} taintReasons Hangi erişimler işaretledi (teşhis için).
|
|
26
|
+
* @property {string | null} csrfToken
|
|
27
|
+
* @property {import('express').Response | null} res
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @type {AsyncLocalStorage<RequestContext>} */
|
|
31
|
+
const storage = new AsyncLocalStorage();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{ private?: boolean, res?: import('express').Response }} [initial]
|
|
35
|
+
* @returns {RequestContext}
|
|
36
|
+
*/
|
|
37
|
+
export function createRequestContext(initial = {}) {
|
|
38
|
+
return {
|
|
39
|
+
private: initial.private === true,
|
|
40
|
+
tainted: false,
|
|
41
|
+
taintReasons: [],
|
|
42
|
+
csrfToken: null,
|
|
43
|
+
res: initial.res ?? null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* İsteği verilen bağlam içinde çalıştırır.
|
|
49
|
+
*
|
|
50
|
+
* @template T
|
|
51
|
+
* @param {RequestContext} context
|
|
52
|
+
* @param {() => T} run
|
|
53
|
+
* @returns {T}
|
|
54
|
+
*/
|
|
55
|
+
export function withRequestContext(context, run) {
|
|
56
|
+
return storage.run(context, run);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns {RequestContext | undefined}
|
|
61
|
+
*/
|
|
62
|
+
export function getRequestContext() {
|
|
63
|
+
return storage.getStore();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Çıktının kullanıcıya bağlı olduğunu bildirir. Bağlam yoksa (script, build,
|
|
68
|
+
* fragment dışı kullanım) sessizce yok sayılır.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} reason Teşhis mesajında görünecek erişim adı.
|
|
71
|
+
*/
|
|
72
|
+
export function markTainted(reason) {
|
|
73
|
+
const context = storage.getStore();
|
|
74
|
+
if (!context) return;
|
|
75
|
+
|
|
76
|
+
context.tainted = true;
|
|
77
|
+
if (!context.taintReasons.includes(reason)) {
|
|
78
|
+
context.taintReasons.push(reason);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/** Okunması çıktıyı kullanıcıya bağlayan başlıklar. */
|
|
84
|
+
const SENSITIVE_HEADERS = new Set([
|
|
85
|
+
"cookie",
|
|
86
|
+
"authorization",
|
|
87
|
+
"proxy-authorization",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/** Okunması çıktıyı kullanıcıya bağlayan `req` alanları. */
|
|
91
|
+
const SENSITIVE_PROPS = new Set(["cookies", "signedCookies", "session", "user"]);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {Record<string, unknown>} headers
|
|
95
|
+
* @returns {Record<string, unknown>}
|
|
96
|
+
*/
|
|
97
|
+
function guardHeaders(headers) {
|
|
98
|
+
return new Proxy(headers, {
|
|
99
|
+
get(target, prop, receiver) {
|
|
100
|
+
if (typeof prop === "string" && SENSITIVE_HEADERS.has(prop.toLowerCase())) {
|
|
101
|
+
markTainted(`req.headers.${prop}`);
|
|
102
|
+
}
|
|
103
|
+
return Reflect.get(target, prop, receiver);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Controller'a giden `req`'i kimliğe dokunan erişimleri işaretleyen bir Proxy
|
|
110
|
+
* ile sarar.
|
|
111
|
+
*
|
|
112
|
+
* Neden gerekli: HTML cache anahtarı yalnızca yol + query. Cookie okuyan bir
|
|
113
|
+
* controller cache'lenebilir bir route'a bağlanmışsa bir kullanıcının HTML'i
|
|
114
|
+
* bir başkasına servis edilir ve bu hiçbir yerde hata olarak görünmez.
|
|
115
|
+
* İşaretleme, sessiz sızıntıyı gürültülü bir uyarıya çeviriyor.
|
|
116
|
+
*
|
|
117
|
+
* Kapsam bilinçli olarak dar: yalnızca *okuma* yakalanır, yazma ve metot
|
|
118
|
+
* çağrıları hedefe dokunulmadan geçer. Proxy'nin prototipi korunduğu için
|
|
119
|
+
* `req instanceof IncomingMessage` gibi kontroller etkilenmez.
|
|
120
|
+
*
|
|
121
|
+
* @param {import('express').Request} req
|
|
122
|
+
* @returns {import('express').Request}
|
|
123
|
+
*/
|
|
124
|
+
export function guardRequest(req) {
|
|
125
|
+
/** @type {Record<string, unknown> | null} */
|
|
126
|
+
let headersProxy = null;
|
|
127
|
+
|
|
128
|
+
return new Proxy(req, {
|
|
129
|
+
get(target, prop, receiver) {
|
|
130
|
+
if (prop === "headers" || prop === "headersDistinct") {
|
|
131
|
+
const raw = Reflect.get(target, prop, receiver);
|
|
132
|
+
if (!raw || typeof raw !== "object") return raw;
|
|
133
|
+
if (prop === "headers") {
|
|
134
|
+
headersProxy ??= guardHeaders(/** @type {Record<string, unknown>} */ (raw));
|
|
135
|
+
return headersProxy;
|
|
136
|
+
}
|
|
137
|
+
return guardHeaders(/** @type {Record<string, unknown>} */ (raw));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (prop === "get" || prop === "header") {
|
|
141
|
+
const original = Reflect.get(target, prop, receiver);
|
|
142
|
+
if (typeof original !== "function") return original;
|
|
143
|
+
return function guardedHeaderLookup(/** @type {string} */ name) {
|
|
144
|
+
if (typeof name === "string" && SENSITIVE_HEADERS.has(name.toLowerCase())) {
|
|
145
|
+
markTainted(`req.get("${name}")`);
|
|
146
|
+
}
|
|
147
|
+
return original.call(target, name);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof prop === "string" && SENSITIVE_PROPS.has(prop)) {
|
|
152
|
+
markTainted(`req.${prop}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const value = Reflect.get(target, prop, receiver);
|
|
156
|
+
// Express metotları kendi iç alanlarına `this` üzerinden erişiyor;
|
|
157
|
+
// proxy'yi receiver olarak bırakmak bu erişimleri de yakalayıp
|
|
158
|
+
// gereksiz işaretlemeye yol açar.
|
|
159
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,19 +5,36 @@
|
|
|
5
5
|
* controller'lar bu yüzeyi kullanır. Alt yollardan (`jskelet/server`)
|
|
6
6
|
* ithal etmek de mümkün; buradaki liste "kararlı" sayılan yüzeydir.
|
|
7
7
|
*/
|
|
8
|
-
export {
|
|
8
|
+
export {
|
|
9
|
+
route,
|
|
10
|
+
fragment,
|
|
11
|
+
renderPage,
|
|
12
|
+
renderView,
|
|
13
|
+
renderNotFound,
|
|
14
|
+
} from "./server/render.js";
|
|
9
15
|
export { renderStatusPage, statusFromError } from "./server/status-page.js";
|
|
10
16
|
export { createApp, startServer } from "./server/create-app.js";
|
|
11
17
|
export {
|
|
12
18
|
notFound,
|
|
13
19
|
redirect,
|
|
14
20
|
permanentRedirect,
|
|
21
|
+
seeOther,
|
|
15
22
|
isNotFoundError,
|
|
16
23
|
isRedirectError,
|
|
17
24
|
NotFoundError,
|
|
18
25
|
RedirectError,
|
|
19
26
|
} from "./http/control-flow.js";
|
|
20
27
|
export { cache, withRequestCache } from "./http/request-cache.js";
|
|
28
|
+
export {
|
|
29
|
+
clearCookie,
|
|
30
|
+
getSignedCookie,
|
|
31
|
+
parseCookies,
|
|
32
|
+
randomToken,
|
|
33
|
+
safeEqual,
|
|
34
|
+
serializeCookie,
|
|
35
|
+
setCookie,
|
|
36
|
+
setSignedCookie,
|
|
37
|
+
} from "./http/cookies.js";
|
|
21
38
|
export { reportUpstreamFailure } from "./server/upstream-tracking.js";
|
|
22
39
|
export { asset, hasAsset, optimizedImage, getSpriteIds } from "./server/assets.js";
|
|
23
40
|
export { headHints } from "./server/head-hints.js";
|
|
@@ -28,8 +45,15 @@ export {
|
|
|
28
45
|
getHtmlCacheSize,
|
|
29
46
|
withHtmlCache,
|
|
30
47
|
} from "./server/html-cache.js";
|
|
48
|
+
export {
|
|
49
|
+
clearDataCache,
|
|
50
|
+
dataCache,
|
|
51
|
+
getDataCacheEntries,
|
|
52
|
+
getDataCacheSize,
|
|
53
|
+
withDataCache,
|
|
54
|
+
} from "./server/data-cache.js";
|
|
31
55
|
export { prewarm, prewarmProgress } from "./server/prewarm.js";
|
|
32
56
|
export { createProxy } from "./server/middleware/upstream-proxy.js";
|
|
33
57
|
export { getConfig, loadConfig } from "./config/index.js";
|
|
34
58
|
export { attrs, cn, cx, esc, jsonScript } from "./views/helpers/html.js";
|
|
35
|
-
export { icon, image, link, preloadImage } from "./views/helpers/tags.js";
|
|
59
|
+
export { csrfField, icon, image, link, preloadImage } from "./views/helpers/tags.js";
|
package/src/init.mjs
CHANGED
|
@@ -12,43 +12,43 @@ import * as log from "./log.mjs";
|
|
|
12
12
|
/** @type {Record<string, string>} */
|
|
13
13
|
const FILES = {
|
|
14
14
|
"jskelet.config.mjs": `/**
|
|
15
|
-
* JSkelet
|
|
16
|
-
*
|
|
15
|
+
* JSkelet configuration. Every field is optional; the app still runs on
|
|
16
|
+
* defaults if you delete this file.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* Details: node_modules/jskelet/docs/en/07-configuration.md
|
|
19
19
|
*/
|
|
20
20
|
export default {
|
|
21
|
-
brand: { lang: "
|
|
21
|
+
brand: { lang: "en" },
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/** Third-party origins; emitted as preconnect in \`<head>\`. */
|
|
24
24
|
preconnect: [],
|
|
25
25
|
|
|
26
26
|
async cache() {
|
|
27
27
|
return {
|
|
28
|
-
/**
|
|
28
|
+
/** How long a page's HTML stays in the cache (seconds). */
|
|
29
29
|
html: { "/": 60 },
|
|
30
30
|
};
|
|
31
31
|
},
|
|
32
32
|
|
|
33
33
|
hooks: {
|
|
34
|
-
/**
|
|
34
|
+
/** Metadata defaults for every page. */
|
|
35
35
|
metadata() {
|
|
36
36
|
return {
|
|
37
37
|
titleTemplate: "%s | JSkelet",
|
|
38
|
-
description: "
|
|
38
|
+
description: "A site built with JSkelet.",
|
|
39
39
|
};
|
|
40
40
|
},
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/** Locals added to the layout on every render. */
|
|
43
43
|
layoutContext() {
|
|
44
44
|
return { bodyClass: "min-h-full" };
|
|
45
45
|
},
|
|
46
46
|
|
|
47
|
-
/** 404
|
|
47
|
+
/** 404 page. */
|
|
48
48
|
notFound() {
|
|
49
49
|
return {
|
|
50
50
|
view: "pages/not-found",
|
|
51
|
-
metadata: { title: "
|
|
51
|
+
metadata: { title: "Page not found", robots: { index: false } },
|
|
52
52
|
};
|
|
53
53
|
},
|
|
54
54
|
},
|
|
@@ -56,11 +56,12 @@ export default {
|
|
|
56
56
|
`,
|
|
57
57
|
|
|
58
58
|
"routes/10-pages.mjs": `/**
|
|
59
|
-
* Route
|
|
60
|
-
* HTML cache
|
|
59
|
+
* Route module. The default export receives \`(app, api)\`; \`api.route()\` wraps
|
|
60
|
+
* the controller with the HTML cache, the notFound/redirect flow and
|
|
61
|
+
* compression.
|
|
61
62
|
*
|
|
62
|
-
*
|
|
63
|
-
* ("/:slug"
|
|
63
|
+
* The numeric prefix in the file name sets load order: catch-all routes
|
|
64
|
+
* (like "/:slug") belong to a higher number.
|
|
64
65
|
*/
|
|
65
66
|
export default function register(app, { route }) {
|
|
66
67
|
app.get(
|
|
@@ -68,8 +69,8 @@ export default function register(app, { route }) {
|
|
|
68
69
|
route(
|
|
69
70
|
async () => ({
|
|
70
71
|
view: "pages/home",
|
|
71
|
-
metadata: { title: "
|
|
72
|
-
data: { message: "JSkelet
|
|
72
|
+
metadata: { title: "Home" },
|
|
73
|
+
data: { message: "JSkelet is running." },
|
|
73
74
|
}),
|
|
74
75
|
{ revalidate: 60 },
|
|
75
76
|
),
|
|
@@ -86,16 +87,16 @@ export default function register(app, { route }) {
|
|
|
86
87
|
|
|
87
88
|
"views/pages/not-found.ejs": `<section class="wrapper">
|
|
88
89
|
<h1>404</h1>
|
|
89
|
-
<p>
|
|
90
|
-
<p><a href="/">
|
|
90
|
+
<p>The page you are looking for was not found.</p>
|
|
91
|
+
<p><a href="/">Back to home</a></p>
|
|
91
92
|
</section>
|
|
92
93
|
`,
|
|
93
94
|
|
|
94
95
|
"views/components/button.js": `import { attrs, esc } from "jskelet/html";
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
|
-
* \`views/components/**\`
|
|
98
|
-
*
|
|
98
|
+
* Every named export under \`views/components/**\` is usable directly in
|
|
99
|
+
* templates: \`<%- button({ text: "Save" }) %>\`. No import needed.
|
|
99
100
|
*
|
|
100
101
|
* @param {{ text: string, href?: string, class?: string }} props
|
|
101
102
|
* @returns {string}
|
|
@@ -109,8 +110,8 @@ export function button({ text, href, class: className }) {
|
|
|
109
110
|
"client/entries/main.js": `import { registerAll, start } from "jskelet/client";
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
|
-
* Island
|
|
113
|
-
*
|
|
113
|
+
* Island registry. Values are dynamic imports: a module is downloaded only if
|
|
114
|
+
* that island is actually on the page and becomes visible.
|
|
114
115
|
*/
|
|
115
116
|
registerAll({
|
|
116
117
|
counter: () => import("../islands/counter.js"),
|
|
@@ -120,8 +121,8 @@ start();
|
|
|
120
121
|
`,
|
|
121
122
|
|
|
122
123
|
"client/islands/counter.js": `/**
|
|
123
|
-
* Island
|
|
124
|
-
*
|
|
124
|
+
* Island contract: a named export called \`mount(element, props)\`.
|
|
125
|
+
* The returned function, if any, is reserved for cleanup.
|
|
125
126
|
*
|
|
126
127
|
* @param {HTMLElement} element
|
|
127
128
|
* @param {{ start?: number }} props
|
|
@@ -133,7 +134,7 @@ export function mount(element, props) {
|
|
|
133
134
|
button.type = "button";
|
|
134
135
|
|
|
135
136
|
const paint = () => {
|
|
136
|
-
button.textContent = \`
|
|
137
|
+
button.textContent = \`Clicks: \${value}\`;
|
|
137
138
|
};
|
|
138
139
|
|
|
139
140
|
button.addEventListener("click", () => {
|
|
@@ -149,9 +150,9 @@ export function mount(element, props) {
|
|
|
149
150
|
"styles/globals.css": `@import "tailwindcss" source(none);
|
|
150
151
|
|
|
151
152
|
/**
|
|
152
|
-
* Tailwind'
|
|
153
|
-
*
|
|
154
|
-
* (data-[active=false]:…
|
|
153
|
+
* Tailwind's class scanning depends on these directives. Automatic detection
|
|
154
|
+
* only scans the directory holding this file; variants used in templates
|
|
155
|
+
* (like data-[active=false]:…) would otherwise be dropped silently.
|
|
155
156
|
*/
|
|
156
157
|
@source "../views";
|
|
157
158
|
@source "../client";
|
|
@@ -213,8 +214,8 @@ export async function init(root) {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
for (const file of created) log.line(`+ ${file}`);
|
|
216
|
-
if (skipped.length) log.warn(`${skipped.length}
|
|
217
|
+
if (skipped.length) log.warn(`${skipped.length} files already existed, skipped`);
|
|
217
218
|
|
|
218
219
|
log.line("");
|
|
219
|
-
log.line("
|
|
220
|
+
log.line("next step: npx jskelet dev");
|
|
220
221
|
}
|
package/src/log.mjs
CHANGED
|
@@ -75,9 +75,15 @@ function clearLine() {
|
|
|
75
75
|
if (isTTY) write("\r\u001b[2K");
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Saat, yerelden bağımsız olarak 24 saatlik biçimde. Dil etiketi vermek
|
|
80
|
+
* sunucunun bulunduğu makinenin diline göre `ÖÖ/ÖS` ya da `AM/PM` basılmasına
|
|
81
|
+
* yol açıyordu; log satırının genişliği sabit kalmalı.
|
|
82
|
+
*
|
|
83
|
+
* @returns {string} `01:49:02`
|
|
84
|
+
*/
|
|
79
85
|
export function clock() {
|
|
80
|
-
return new Date().toLocaleTimeString("
|
|
86
|
+
return new Date().toLocaleTimeString("en-GB", { hour12: false });
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/**
|
package/src/logo.png
CHANGED
|
Binary file
|
|
@@ -56,7 +56,7 @@ function readAliases() {
|
|
|
56
56
|
// Uzun önek önce: `@flags/` `@/`den önce denenmeli, yoksa `@/` yakalar.
|
|
57
57
|
return aliases.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
58
58
|
} catch (error) {
|
|
59
|
-
console.warn(`[alias] ${name}
|
|
59
|
+
console.warn(`[alias] could not read ${name}, aliases disabled`, error);
|
|
60
60
|
return [];
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/server/assets.js
CHANGED
package/src/server/create-app.js
CHANGED
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
* static'e düşer ve middleware anında sıkıştırır (kalite 5).
|
|
14
14
|
* 5. body parser'lar — statikten sonra: görsel isteklerinde gövde ayrıştırma
|
|
15
15
|
* maliyeti ödenmesin.
|
|
16
|
-
* 6.
|
|
17
|
-
*
|
|
16
|
+
* 6. csrf — body parser'lardan sonra olmalı: token form alanından okunuyor.
|
|
17
|
+
* Rewrite'lardan önce, çünkü kontrol istemcinin gördüğü yola bakar.
|
|
18
|
+
* 7. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
|
|
19
|
+
* 8. route'lar → 404 → hata yönetimi.
|
|
18
20
|
*/
|
|
19
21
|
import path from "node:path";
|
|
20
22
|
import process from "node:process";
|
|
21
23
|
import express from "express";
|
|
22
24
|
import { compression } from "./middleware/compression.js";
|
|
23
25
|
import { headersMiddleware } from "./middleware/headers.js";
|
|
26
|
+
import { csrf } from "./middleware/csrf.js";
|
|
24
27
|
import { staticPrecompressed } from "./middleware/static-precompressed.js";
|
|
25
28
|
import { devGate } from "./middleware/dev-gate.js";
|
|
26
29
|
import { redirects } from "./middleware/redirects.js";
|
|
@@ -51,8 +54,11 @@ export async function createApp(options = {}) {
|
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
app.set("etag", "strong");
|
|
54
|
-
// Ters proxy arkasında doğru protokol ve istemci IP'si için.
|
|
55
|
-
|
|
57
|
+
// Ters proxy arkasında doğru protokol ve istemci IP'si için. Doğrudan
|
|
58
|
+
// internete açık bir sunucuda kapatılmalı: açıkken istemci kendi
|
|
59
|
+
// `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit log
|
|
60
|
+
// yanlış IP görür.
|
|
61
|
+
app.set("trust proxy", config.security.trustProxy);
|
|
56
62
|
|
|
57
63
|
app.use(configRewrites("beforeFiles"));
|
|
58
64
|
app.use(compression());
|
|
@@ -87,6 +93,8 @@ export async function createApp(options = {}) {
|
|
|
87
93
|
app.use(express.urlencoded({ extended: false, limit: "64kb" }));
|
|
88
94
|
app.use(express.json({ limit: "256kb" }));
|
|
89
95
|
|
|
96
|
+
app.use(csrf());
|
|
97
|
+
|
|
90
98
|
app.use(configRewrites("afterFiles"));
|
|
91
99
|
|
|
92
100
|
await registerRoutes(app);
|