jskelet 0.6.3 → 0.6.4
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 +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React `cache()` fonksiyonunun karşılığı: aynı istek içinde aynı argümanlarla
|
|
3
|
-
* yapılan çağrılar tek kez çalışır. İstek bağlamı yoksa (script, client)
|
|
4
|
-
* memoizasyon devre dışı kalır ve fonksiyon doğrudan çağrılır.
|
|
5
|
-
*/
|
|
6
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
-
|
|
8
|
-
/** @type {AsyncLocalStorage<Map<unknown, Map<string, unknown>>>} */
|
|
9
|
-
const storage = new AsyncLocalStorage();
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* İsteği bir memo bağlamı içinde çalıştırır.
|
|
13
|
-
* @param {() => T} run
|
|
14
|
-
* @returns {T}
|
|
15
|
-
* @template T
|
|
16
|
-
*/
|
|
17
|
-
export function withRequestCache(run) {
|
|
18
|
-
return storage.run(new Map(), run);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {F} fn
|
|
23
|
-
* @returns {F}
|
|
24
|
-
* @template {(...args: any[]) => any} F
|
|
25
|
-
*/
|
|
26
|
-
export function cache(fn) {
|
|
27
|
-
const wrapped = (...args) => {
|
|
28
|
-
const contextStore = storage.getStore();
|
|
29
|
-
if (!contextStore) return fn(...args);
|
|
30
|
-
|
|
31
|
-
let perFn = contextStore.get(fn);
|
|
32
|
-
if (!perFn) {
|
|
33
|
-
perFn = new Map();
|
|
34
|
-
contextStore.set(fn, perFn);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const key = args.length === 0 ? "" : JSON.stringify(args);
|
|
38
|
-
if (perFn.has(key)) return perFn.get(key);
|
|
39
|
-
|
|
40
|
-
const result = fn(...args);
|
|
41
|
-
perFn.set(key, result);
|
|
42
|
-
return result;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
return /** @type {F} */ (wrapped);
|
|
46
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* React `cache()` fonksiyonunun karşılığı: aynı istek içinde aynı argümanlarla
|
|
3
|
+
* yapılan çağrılar tek kez çalışır. İstek bağlamı yoksa (script, client)
|
|
4
|
+
* memoizasyon devre dışı kalır ve fonksiyon doğrudan çağrılır.
|
|
5
|
+
*/
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
|
|
8
|
+
/** @type {AsyncLocalStorage<Map<unknown, Map<string, unknown>>>} */
|
|
9
|
+
const storage = new AsyncLocalStorage();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* İsteği bir memo bağlamı içinde çalıştırır.
|
|
13
|
+
* @param {() => T} run
|
|
14
|
+
* @returns {T}
|
|
15
|
+
* @template T
|
|
16
|
+
*/
|
|
17
|
+
export function withRequestCache(run) {
|
|
18
|
+
return storage.run(new Map(), run);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {F} fn
|
|
23
|
+
* @returns {F}
|
|
24
|
+
* @template {(...args: any[]) => any} F
|
|
25
|
+
*/
|
|
26
|
+
export function cache(fn) {
|
|
27
|
+
const wrapped = (...args) => {
|
|
28
|
+
const contextStore = storage.getStore();
|
|
29
|
+
if (!contextStore) return fn(...args);
|
|
30
|
+
|
|
31
|
+
let perFn = contextStore.get(fn);
|
|
32
|
+
if (!perFn) {
|
|
33
|
+
perFn = new Map();
|
|
34
|
+
contextStore.set(fn, perFn);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const key = args.length === 0 ? "" : JSON.stringify(args);
|
|
38
|
+
if (perFn.has(key)) return perFn.get(key);
|
|
39
|
+
|
|
40
|
+
const result = fn(...args);
|
|
41
|
+
perFn.set(key, result);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return /** @type {F} */ (wrapped);
|
|
46
|
+
}
|
|
@@ -1,165 +1,165 @@
|
|
|
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
|
-
* @property {string | null} pathname Devtools: bu isteğin sayfa yolu (SSR
|
|
29
|
-
* sırasında upstream hatasına "hangi sayfa" bağlamak için).
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/** @type {AsyncLocalStorage<RequestContext>} */
|
|
33
|
-
const storage = new AsyncLocalStorage();
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @param {{ private?: boolean, res?: import('express').Response, pathname?: string | null }} [initial]
|
|
37
|
-
* @returns {RequestContext}
|
|
38
|
-
*/
|
|
39
|
-
export function createRequestContext(initial = {}) {
|
|
40
|
-
return {
|
|
41
|
-
private: initial.private === true,
|
|
42
|
-
tainted: false,
|
|
43
|
-
taintReasons: [],
|
|
44
|
-
csrfToken: null,
|
|
45
|
-
res: initial.res ?? null,
|
|
46
|
-
pathname: initial.pathname ?? null,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* İsteği verilen bağlam içinde çalıştırır.
|
|
52
|
-
*
|
|
53
|
-
* @template T
|
|
54
|
-
* @param {RequestContext} context
|
|
55
|
-
* @param {() => T} run
|
|
56
|
-
* @returns {T}
|
|
57
|
-
*/
|
|
58
|
-
export function withRequestContext(context, run) {
|
|
59
|
-
return storage.run(context, run);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @returns {RequestContext | undefined}
|
|
64
|
-
*/
|
|
65
|
-
export function getRequestContext() {
|
|
66
|
-
return storage.getStore();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Çıktının kullanıcıya bağlı olduğunu bildirir. Bağlam yoksa (script, build,
|
|
71
|
-
* fragment dışı kullanım) sessizce yok sayılır.
|
|
72
|
-
*
|
|
73
|
-
* @param {string} reason Teşhis mesajında görünecek erişim adı.
|
|
74
|
-
*/
|
|
75
|
-
export function markTainted(reason) {
|
|
76
|
-
const context = storage.getStore();
|
|
77
|
-
if (!context) return;
|
|
78
|
-
|
|
79
|
-
context.tainted = true;
|
|
80
|
-
if (!context.taintReasons.includes(reason)) {
|
|
81
|
-
context.taintReasons.push(reason);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/** Okunması çıktıyı kullanıcıya bağlayan başlıklar. */
|
|
87
|
-
const SENSITIVE_HEADERS = new Set([
|
|
88
|
-
"cookie",
|
|
89
|
-
"authorization",
|
|
90
|
-
"proxy-authorization",
|
|
91
|
-
]);
|
|
92
|
-
|
|
93
|
-
/** Okunması çıktıyı kullanıcıya bağlayan `req` alanları. */
|
|
94
|
-
const SENSITIVE_PROPS = new Set(["cookies", "signedCookies", "session", "user"]);
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @param {Record<string, unknown>} headers
|
|
98
|
-
* @returns {Record<string, unknown>}
|
|
99
|
-
*/
|
|
100
|
-
function guardHeaders(headers) {
|
|
101
|
-
return new Proxy(headers, {
|
|
102
|
-
get(target, prop, receiver) {
|
|
103
|
-
if (typeof prop === "string" && SENSITIVE_HEADERS.has(prop.toLowerCase())) {
|
|
104
|
-
markTainted(`req.headers.${prop}`);
|
|
105
|
-
}
|
|
106
|
-
return Reflect.get(target, prop, receiver);
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Controller'a giden `req`'i kimliğe dokunan erişimleri işaretleyen bir Proxy
|
|
113
|
-
* ile sarar.
|
|
114
|
-
*
|
|
115
|
-
* Neden gerekli: HTML cache anahtarı yalnızca yol + query. Cookie okuyan bir
|
|
116
|
-
* controller cache'lenebilir bir route'a bağlanmışsa bir kullanıcının HTML'i
|
|
117
|
-
* bir başkasına servis edilir ve bu hiçbir yerde hata olarak görünmez.
|
|
118
|
-
* İşaretleme, sessiz sızıntıyı gürültülü bir uyarıya çeviriyor.
|
|
119
|
-
*
|
|
120
|
-
* Kapsam bilinçli olarak dar: yalnızca *okuma* yakalanır, yazma ve metot
|
|
121
|
-
* çağrıları hedefe dokunulmadan geçer. Proxy'nin prototipi korunduğu için
|
|
122
|
-
* `req instanceof IncomingMessage` gibi kontroller etkilenmez.
|
|
123
|
-
*
|
|
124
|
-
* @param {import('express').Request} req
|
|
125
|
-
* @returns {import('express').Request}
|
|
126
|
-
*/
|
|
127
|
-
export function guardRequest(req) {
|
|
128
|
-
/** @type {Record<string, unknown> | null} */
|
|
129
|
-
let headersProxy = null;
|
|
130
|
-
|
|
131
|
-
return new Proxy(req, {
|
|
132
|
-
get(target, prop, receiver) {
|
|
133
|
-
if (prop === "headers" || prop === "headersDistinct") {
|
|
134
|
-
const raw = Reflect.get(target, prop, receiver);
|
|
135
|
-
if (!raw || typeof raw !== "object") return raw;
|
|
136
|
-
if (prop === "headers") {
|
|
137
|
-
headersProxy ??= guardHeaders(/** @type {Record<string, unknown>} */ (raw));
|
|
138
|
-
return headersProxy;
|
|
139
|
-
}
|
|
140
|
-
return guardHeaders(/** @type {Record<string, unknown>} */ (raw));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (prop === "get" || prop === "header") {
|
|
144
|
-
const original = Reflect.get(target, prop, receiver);
|
|
145
|
-
if (typeof original !== "function") return original;
|
|
146
|
-
return function guardedHeaderLookup(/** @type {string} */ name) {
|
|
147
|
-
if (typeof name === "string" && SENSITIVE_HEADERS.has(name.toLowerCase())) {
|
|
148
|
-
markTainted(`req.get("${name}")`);
|
|
149
|
-
}
|
|
150
|
-
return original.call(target, name);
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (typeof prop === "string" && SENSITIVE_PROPS.has(prop)) {
|
|
155
|
-
markTainted(`req.${prop}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const value = Reflect.get(target, prop, receiver);
|
|
159
|
-
// Express metotları kendi iç alanlarına `this` üzerinden erişiyor;
|
|
160
|
-
// proxy'yi receiver olarak bırakmak bu erişimleri de yakalayıp
|
|
161
|
-
// gereksiz işaretlemeye yol açar.
|
|
162
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
}
|
|
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
|
+
* @property {string | null} pathname Devtools: bu isteğin sayfa yolu (SSR
|
|
29
|
+
* sırasında upstream hatasına "hangi sayfa" bağlamak için).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** @type {AsyncLocalStorage<RequestContext>} */
|
|
33
|
+
const storage = new AsyncLocalStorage();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{ private?: boolean, res?: import('express').Response, pathname?: string | null }} [initial]
|
|
37
|
+
* @returns {RequestContext}
|
|
38
|
+
*/
|
|
39
|
+
export function createRequestContext(initial = {}) {
|
|
40
|
+
return {
|
|
41
|
+
private: initial.private === true,
|
|
42
|
+
tainted: false,
|
|
43
|
+
taintReasons: [],
|
|
44
|
+
csrfToken: null,
|
|
45
|
+
res: initial.res ?? null,
|
|
46
|
+
pathname: initial.pathname ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* İsteği verilen bağlam içinde çalıştırır.
|
|
52
|
+
*
|
|
53
|
+
* @template T
|
|
54
|
+
* @param {RequestContext} context
|
|
55
|
+
* @param {() => T} run
|
|
56
|
+
* @returns {T}
|
|
57
|
+
*/
|
|
58
|
+
export function withRequestContext(context, run) {
|
|
59
|
+
return storage.run(context, run);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @returns {RequestContext | undefined}
|
|
64
|
+
*/
|
|
65
|
+
export function getRequestContext() {
|
|
66
|
+
return storage.getStore();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Çıktının kullanıcıya bağlı olduğunu bildirir. Bağlam yoksa (script, build,
|
|
71
|
+
* fragment dışı kullanım) sessizce yok sayılır.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} reason Teşhis mesajında görünecek erişim adı.
|
|
74
|
+
*/
|
|
75
|
+
export function markTainted(reason) {
|
|
76
|
+
const context = storage.getStore();
|
|
77
|
+
if (!context) return;
|
|
78
|
+
|
|
79
|
+
context.tainted = true;
|
|
80
|
+
if (!context.taintReasons.includes(reason)) {
|
|
81
|
+
context.taintReasons.push(reason);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/** Okunması çıktıyı kullanıcıya bağlayan başlıklar. */
|
|
87
|
+
const SENSITIVE_HEADERS = new Set([
|
|
88
|
+
"cookie",
|
|
89
|
+
"authorization",
|
|
90
|
+
"proxy-authorization",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
/** Okunması çıktıyı kullanıcıya bağlayan `req` alanları. */
|
|
94
|
+
const SENSITIVE_PROPS = new Set(["cookies", "signedCookies", "session", "user"]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {Record<string, unknown>} headers
|
|
98
|
+
* @returns {Record<string, unknown>}
|
|
99
|
+
*/
|
|
100
|
+
function guardHeaders(headers) {
|
|
101
|
+
return new Proxy(headers, {
|
|
102
|
+
get(target, prop, receiver) {
|
|
103
|
+
if (typeof prop === "string" && SENSITIVE_HEADERS.has(prop.toLowerCase())) {
|
|
104
|
+
markTainted(`req.headers.${prop}`);
|
|
105
|
+
}
|
|
106
|
+
return Reflect.get(target, prop, receiver);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Controller'a giden `req`'i kimliğe dokunan erişimleri işaretleyen bir Proxy
|
|
113
|
+
* ile sarar.
|
|
114
|
+
*
|
|
115
|
+
* Neden gerekli: HTML cache anahtarı yalnızca yol + query. Cookie okuyan bir
|
|
116
|
+
* controller cache'lenebilir bir route'a bağlanmışsa bir kullanıcının HTML'i
|
|
117
|
+
* bir başkasına servis edilir ve bu hiçbir yerde hata olarak görünmez.
|
|
118
|
+
* İşaretleme, sessiz sızıntıyı gürültülü bir uyarıya çeviriyor.
|
|
119
|
+
*
|
|
120
|
+
* Kapsam bilinçli olarak dar: yalnızca *okuma* yakalanır, yazma ve metot
|
|
121
|
+
* çağrıları hedefe dokunulmadan geçer. Proxy'nin prototipi korunduğu için
|
|
122
|
+
* `req instanceof IncomingMessage` gibi kontroller etkilenmez.
|
|
123
|
+
*
|
|
124
|
+
* @param {import('express').Request} req
|
|
125
|
+
* @returns {import('express').Request}
|
|
126
|
+
*/
|
|
127
|
+
export function guardRequest(req) {
|
|
128
|
+
/** @type {Record<string, unknown> | null} */
|
|
129
|
+
let headersProxy = null;
|
|
130
|
+
|
|
131
|
+
return new Proxy(req, {
|
|
132
|
+
get(target, prop, receiver) {
|
|
133
|
+
if (prop === "headers" || prop === "headersDistinct") {
|
|
134
|
+
const raw = Reflect.get(target, prop, receiver);
|
|
135
|
+
if (!raw || typeof raw !== "object") return raw;
|
|
136
|
+
if (prop === "headers") {
|
|
137
|
+
headersProxy ??= guardHeaders(/** @type {Record<string, unknown>} */ (raw));
|
|
138
|
+
return headersProxy;
|
|
139
|
+
}
|
|
140
|
+
return guardHeaders(/** @type {Record<string, unknown>} */ (raw));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (prop === "get" || prop === "header") {
|
|
144
|
+
const original = Reflect.get(target, prop, receiver);
|
|
145
|
+
if (typeof original !== "function") return original;
|
|
146
|
+
return function guardedHeaderLookup(/** @type {string} */ name) {
|
|
147
|
+
if (typeof name === "string" && SENSITIVE_HEADERS.has(name.toLowerCase())) {
|
|
148
|
+
markTainted(`req.get("${name}")`);
|
|
149
|
+
}
|
|
150
|
+
return original.call(target, name);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof prop === "string" && SENSITIVE_PROPS.has(prop)) {
|
|
155
|
+
markTainted(`req.${prop}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const value = Reflect.get(target, prop, receiver);
|
|
159
|
+
// Express metotları kendi iç alanlarına `this` üzerinden erişiyor;
|
|
160
|
+
// proxy'yi receiver olarak bırakmak bu erişimleri de yakalayıp
|
|
161
|
+
// gereksiz işaretlemeye yol açar.
|
|
162
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|