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,134 +1,134 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CSRF koruması.
|
|
3
|
-
*
|
|
4
|
-
* Bu yüzey framework'e ait, çünkü gövdeyi ayrıştıran o: `create-app.js`
|
|
5
|
-
* `express.urlencoded` ve `express.json` kuruyor, yani state değiştiren
|
|
6
|
-
* istekleri kabul eden katman framework. Cookie ile kimlik doğrulayan bir
|
|
7
|
-
* uygulama bu koruma olmadan açık.
|
|
8
|
-
*
|
|
9
|
-
* İki katman:
|
|
10
|
-
*
|
|
11
|
-
* 1. **Origin kontrolü** (varsayılan açık). `Origin` ya da
|
|
12
|
-
* `Sec-Fetch-Site` başlığı çapraz site olduğunu gösteriyorsa istek
|
|
13
|
-
* reddedilir. Başlıkların **hiçbiri yoksa geçer**: tarayıcılar çapraz
|
|
14
|
-
* origin bir POST'ta `Origin`'i her zaman gönderir, buna karşılık
|
|
15
|
-
* webhook'lar ve sunucudan sunucuya çağrılar hiç göndermez. Bu ayrım,
|
|
16
|
-
* korumayı açık bırakırken entegrasyonları bozmamayı sağlıyor.
|
|
17
|
-
*
|
|
18
|
-
* 2. **Çift gönderim token'ı** (`security.csrf.token` ile açılır). Cookie'deki
|
|
19
|
-
* imzalı token ile form alanı/başlıktaki değer karşılaştırılır. `Origin`
|
|
20
|
-
* göndermeyen eski tarayıcılar için ikinci katman; formlara `csrfField()`
|
|
21
|
-
* eklenmesini gerektirdiği için varsayılan kapalı.
|
|
22
|
-
*
|
|
23
|
-
* Token'ı bu middleware **üretmez**, yalnızca doğrular. Üretim `csrfField()`
|
|
24
|
-
* içinde, yani gerçekten bir forma basıldığı anda olur. Sebebi somut: token
|
|
25
|
-
* her yanıtta yazılsaydı public ve cache'lenebilir bir sayfa da `Set-Cookie`
|
|
26
|
-
* taşırdı, bir CDN o yanıtı saklardı ve tüm ziyaretçiler aynı token'ı
|
|
27
|
-
* paylaşırdı — çift gönderim kontrolü tam olarak o noktada anlamını yitirir.
|
|
28
|
-
*/
|
|
29
|
-
import { getConfig } from "../../config/index.js";
|
|
30
|
-
import { matchPattern } from "../../config/pattern.js";
|
|
31
|
-
import { getSignedCookie, safeEqual } from "../../http/cookies.js";
|
|
32
|
-
|
|
33
|
-
/** Gövdesi olmayan, yan etkisi beklenmeyen metotlar. */
|
|
34
|
-
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* İsteğin geldiği origin'in kendi origin'imiz olup olmadığını söyler.
|
|
38
|
-
*
|
|
39
|
-
* `trust proxy` kapalıysa `req.protocol` her zaman `http` döner ve HTTPS
|
|
40
|
-
* arkasında her istek çapraz site sanılır; bu yüzden karşılaştırma yalnızca
|
|
41
|
-
* host üzerinden yapılır, protokol değil.
|
|
42
|
-
*
|
|
43
|
-
* @param {import('express').Request} req
|
|
44
|
-
* @param {string[]} allowedOrigins
|
|
45
|
-
* @returns {boolean}
|
|
46
|
-
*/
|
|
47
|
-
function isSameOrigin(req, allowedOrigins) {
|
|
48
|
-
const origin = req.headers.origin;
|
|
49
|
-
if (!origin || origin === "null") return false;
|
|
50
|
-
|
|
51
|
-
if (allowedOrigins.includes(origin)) return true;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const host = req.headers.host;
|
|
55
|
-
return Boolean(host) && new URL(origin).host === host;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @returns {import('express').RequestHandler}
|
|
63
|
-
*/
|
|
64
|
-
export function csrf() {
|
|
65
|
-
const { security } = getConfig();
|
|
66
|
-
const { csrf: options } = security;
|
|
67
|
-
|
|
68
|
-
return (req, res, next) => {
|
|
69
|
-
if (!options.enabled) {
|
|
70
|
-
next();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const pathname = req.path ?? "";
|
|
75
|
-
if (options.exclude.some((pattern) => matchPattern(pattern, pathname))) {
|
|
76
|
-
next();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (SAFE_METHODS.has(req.method)) {
|
|
81
|
-
next();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const site = String(req.headers["sec-fetch-site"] ?? "");
|
|
86
|
-
const hasOrigin = Boolean(req.headers.origin);
|
|
87
|
-
|
|
88
|
-
// Yalnızca çapraz site olduğu **belli** olan istekler reddedilir.
|
|
89
|
-
if (site === "cross-site" || (hasOrigin && !isSameOrigin(req, options.allowedOrigins))) {
|
|
90
|
-
reject(req, res, "origin mismatch");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (options.token) {
|
|
95
|
-
// Cookie yoksa istek zaten token basan bir formdan gelmiyor.
|
|
96
|
-
const expected = getSignedCookie(req, options.cookieName);
|
|
97
|
-
const provided =
|
|
98
|
-
firstString(/** @type {any} */ (req.body)?.[options.fieldName]) ??
|
|
99
|
-
firstString(req.headers[options.headerName]);
|
|
100
|
-
|
|
101
|
-
if (!expected || !provided || !safeEqual(provided, expected)) {
|
|
102
|
-
reject(req, res, "token mismatch");
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
next();
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @param {unknown} value
|
|
113
|
-
* @returns {string | null}
|
|
114
|
-
*/
|
|
115
|
-
function firstString(value) {
|
|
116
|
-
if (typeof value === "string" && value) return value;
|
|
117
|
-
if (Array.isArray(value)) return firstString(value[0]);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Reddedilen istek HTML sayfası değil düz metin döner: bu bir kullanıcı
|
|
123
|
-
* hatası değil, ya bir saldırı ya da bir programlama hatası — ve fragment
|
|
124
|
-
* takasında bir hata sayfasının içine düşmemeli.
|
|
125
|
-
*
|
|
126
|
-
* @param {import('express').Request} req
|
|
127
|
-
* @param {import('express').Response} res
|
|
128
|
-
* @param {string} reason
|
|
129
|
-
*/
|
|
130
|
-
function reject(req, res, reason) {
|
|
131
|
-
console.warn(`[csrf] ${req.method} ${req.originalUrl} rejected — ${reason}`);
|
|
132
|
-
res.status(403).setHeader("Cache-Control", "no-store");
|
|
133
|
-
res.type("text/plain").send("Forbidden");
|
|
134
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* CSRF koruması.
|
|
3
|
+
*
|
|
4
|
+
* Bu yüzey framework'e ait, çünkü gövdeyi ayrıştıran o: `create-app.js`
|
|
5
|
+
* `express.urlencoded` ve `express.json` kuruyor, yani state değiştiren
|
|
6
|
+
* istekleri kabul eden katman framework. Cookie ile kimlik doğrulayan bir
|
|
7
|
+
* uygulama bu koruma olmadan açık.
|
|
8
|
+
*
|
|
9
|
+
* İki katman:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Origin kontrolü** (varsayılan açık). `Origin` ya da
|
|
12
|
+
* `Sec-Fetch-Site` başlığı çapraz site olduğunu gösteriyorsa istek
|
|
13
|
+
* reddedilir. Başlıkların **hiçbiri yoksa geçer**: tarayıcılar çapraz
|
|
14
|
+
* origin bir POST'ta `Origin`'i her zaman gönderir, buna karşılık
|
|
15
|
+
* webhook'lar ve sunucudan sunucuya çağrılar hiç göndermez. Bu ayrım,
|
|
16
|
+
* korumayı açık bırakırken entegrasyonları bozmamayı sağlıyor.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Çift gönderim token'ı** (`security.csrf.token` ile açılır). Cookie'deki
|
|
19
|
+
* imzalı token ile form alanı/başlıktaki değer karşılaştırılır. `Origin`
|
|
20
|
+
* göndermeyen eski tarayıcılar için ikinci katman; formlara `csrfField()`
|
|
21
|
+
* eklenmesini gerektirdiği için varsayılan kapalı.
|
|
22
|
+
*
|
|
23
|
+
* Token'ı bu middleware **üretmez**, yalnızca doğrular. Üretim `csrfField()`
|
|
24
|
+
* içinde, yani gerçekten bir forma basıldığı anda olur. Sebebi somut: token
|
|
25
|
+
* her yanıtta yazılsaydı public ve cache'lenebilir bir sayfa da `Set-Cookie`
|
|
26
|
+
* taşırdı, bir CDN o yanıtı saklardı ve tüm ziyaretçiler aynı token'ı
|
|
27
|
+
* paylaşırdı — çift gönderim kontrolü tam olarak o noktada anlamını yitirir.
|
|
28
|
+
*/
|
|
29
|
+
import { getConfig } from "../../config/index.js";
|
|
30
|
+
import { matchPattern } from "../../config/pattern.js";
|
|
31
|
+
import { getSignedCookie, safeEqual } from "../../http/cookies.js";
|
|
32
|
+
|
|
33
|
+
/** Gövdesi olmayan, yan etkisi beklenmeyen metotlar. */
|
|
34
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* İsteğin geldiği origin'in kendi origin'imiz olup olmadığını söyler.
|
|
38
|
+
*
|
|
39
|
+
* `trust proxy` kapalıysa `req.protocol` her zaman `http` döner ve HTTPS
|
|
40
|
+
* arkasında her istek çapraz site sanılır; bu yüzden karşılaştırma yalnızca
|
|
41
|
+
* host üzerinden yapılır, protokol değil.
|
|
42
|
+
*
|
|
43
|
+
* @param {import('express').Request} req
|
|
44
|
+
* @param {string[]} allowedOrigins
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isSameOrigin(req, allowedOrigins) {
|
|
48
|
+
const origin = req.headers.origin;
|
|
49
|
+
if (!origin || origin === "null") return false;
|
|
50
|
+
|
|
51
|
+
if (allowedOrigins.includes(origin)) return true;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const host = req.headers.host;
|
|
55
|
+
return Boolean(host) && new URL(origin).host === host;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @returns {import('express').RequestHandler}
|
|
63
|
+
*/
|
|
64
|
+
export function csrf() {
|
|
65
|
+
const { security } = getConfig();
|
|
66
|
+
const { csrf: options } = security;
|
|
67
|
+
|
|
68
|
+
return (req, res, next) => {
|
|
69
|
+
if (!options.enabled) {
|
|
70
|
+
next();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pathname = req.path ?? "";
|
|
75
|
+
if (options.exclude.some((pattern) => matchPattern(pattern, pathname))) {
|
|
76
|
+
next();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (SAFE_METHODS.has(req.method)) {
|
|
81
|
+
next();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const site = String(req.headers["sec-fetch-site"] ?? "");
|
|
86
|
+
const hasOrigin = Boolean(req.headers.origin);
|
|
87
|
+
|
|
88
|
+
// Yalnızca çapraz site olduğu **belli** olan istekler reddedilir.
|
|
89
|
+
if (site === "cross-site" || (hasOrigin && !isSameOrigin(req, options.allowedOrigins))) {
|
|
90
|
+
reject(req, res, "origin mismatch");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.token) {
|
|
95
|
+
// Cookie yoksa istek zaten token basan bir formdan gelmiyor.
|
|
96
|
+
const expected = getSignedCookie(req, options.cookieName);
|
|
97
|
+
const provided =
|
|
98
|
+
firstString(/** @type {any} */ (req.body)?.[options.fieldName]) ??
|
|
99
|
+
firstString(req.headers[options.headerName]);
|
|
100
|
+
|
|
101
|
+
if (!expected || !provided || !safeEqual(provided, expected)) {
|
|
102
|
+
reject(req, res, "token mismatch");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
next();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {unknown} value
|
|
113
|
+
* @returns {string | null}
|
|
114
|
+
*/
|
|
115
|
+
function firstString(value) {
|
|
116
|
+
if (typeof value === "string" && value) return value;
|
|
117
|
+
if (Array.isArray(value)) return firstString(value[0]);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Reddedilen istek HTML sayfası değil düz metin döner: bu bir kullanıcı
|
|
123
|
+
* hatası değil, ya bir saldırı ya da bir programlama hatası — ve fragment
|
|
124
|
+
* takasında bir hata sayfasının içine düşmemeli.
|
|
125
|
+
*
|
|
126
|
+
* @param {import('express').Request} req
|
|
127
|
+
* @param {import('express').Response} res
|
|
128
|
+
* @param {string} reason
|
|
129
|
+
*/
|
|
130
|
+
function reject(req, res, reason) {
|
|
131
|
+
console.warn(`[csrf] ${req.method} ${req.originalUrl} rejected — ${reason}`);
|
|
132
|
+
res.status(403).setHeader("Cache-Control", "no-store");
|
|
133
|
+
res.type("text/plain").send("Forbidden");
|
|
134
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Yayına açılmamış bir ortamı gizler. Gate kapalıyken (varsayılan) `DEV_TOKEN`
|
|
3
|
-
* olsa bile istekler geçer: değişkenin task ortamında unutulması production'u
|
|
4
|
-
* kilitlemesin. Açmak için `devGate: true` ya da `DEV_GATE=1` gerekir; o zaman
|
|
5
|
-
* token taşımayan her isteğe 404 döner. 403 değil 404 — 403 ortamın var
|
|
6
|
-
* olduğunu doğrular, 404 hiç yokmuş gibi davranır.
|
|
7
|
-
*
|
|
8
|
-
* Token bir kez `?dev_token=…` ile gelirse çereze yazılır, böylece link
|
|
9
|
-
* paylaşımı yeterli olur. Token boşsa gate açık olsa da kimse kilitlenmez.
|
|
10
|
-
*/
|
|
11
|
-
import process from "node:process";
|
|
12
|
-
import { getConfig } from "../../config/index.js";
|
|
13
|
-
|
|
14
|
-
const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 14;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {import('http').IncomingMessage} req
|
|
18
|
-
* @param {string} name
|
|
19
|
-
* @returns {string | undefined}
|
|
20
|
-
*/
|
|
21
|
-
function readCookie(req, name) {
|
|
22
|
-
const header = req.headers.cookie;
|
|
23
|
-
if (!header) return undefined;
|
|
24
|
-
|
|
25
|
-
for (const part of header.split(";")) {
|
|
26
|
-
const eq = part.indexOf("=");
|
|
27
|
-
if (eq === -1) continue;
|
|
28
|
-
if (part.slice(0, eq).trim() !== name) continue;
|
|
29
|
-
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
30
|
-
}
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** @returns {import('express').RequestHandler} */
|
|
35
|
-
export function devGate() {
|
|
36
|
-
const { devGate: enabled, devGateBypass, brand } = getConfig();
|
|
37
|
-
const bypass = new Set(devGateBypass);
|
|
38
|
-
const cookieName = brand.devTokenCookie;
|
|
39
|
-
const devToken = process.env.DEV_TOKEN;
|
|
40
|
-
|
|
41
|
-
// Unutulmuş bir değişken sessizce herkese 404 kesmesin; operatör görsün.
|
|
42
|
-
if (!enabled && devToken) {
|
|
43
|
-
console.warn(
|
|
44
|
-
"[dev-gate] DEV_TOKEN is set but the gate is off, so the site stays public. " +
|
|
45
|
-
"Set devGate: true or DEV_GATE=1 to require the token.",
|
|
46
|
-
);
|
|
47
|
-
} else if (enabled && !devToken) {
|
|
48
|
-
console.warn(
|
|
49
|
-
"[dev-gate] the gate is on but DEV_TOKEN is empty; requests are not blocked.",
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return (req, res, next) => {
|
|
54
|
-
if (!enabled || !devToken) return next();
|
|
55
|
-
if (bypass.has(req.path)) return next();
|
|
56
|
-
|
|
57
|
-
const fromQuery = req.query?.[cookieName];
|
|
58
|
-
const tokenFromQuery = Array.isArray(fromQuery) ? fromQuery[0] : fromQuery;
|
|
59
|
-
const tokenFromCookie = readCookie(req, cookieName);
|
|
60
|
-
|
|
61
|
-
if (tokenFromQuery !== devToken && tokenFromCookie !== devToken) {
|
|
62
|
-
res.status(404).type("text/plain").send("Not Found");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (tokenFromQuery === devToken) {
|
|
67
|
-
res.setHeader(
|
|
68
|
-
"Set-Cookie",
|
|
69
|
-
`${cookieName}=${encodeURIComponent(devToken)}; Max-Age=${COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
next();
|
|
74
|
-
};
|
|
75
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Yayına açılmamış bir ortamı gizler. Gate kapalıyken (varsayılan) `DEV_TOKEN`
|
|
3
|
+
* olsa bile istekler geçer: değişkenin task ortamında unutulması production'u
|
|
4
|
+
* kilitlemesin. Açmak için `devGate: true` ya da `DEV_GATE=1` gerekir; o zaman
|
|
5
|
+
* token taşımayan her isteğe 404 döner. 403 değil 404 — 403 ortamın var
|
|
6
|
+
* olduğunu doğrular, 404 hiç yokmuş gibi davranır.
|
|
7
|
+
*
|
|
8
|
+
* Token bir kez `?dev_token=…` ile gelirse çereze yazılır, böylece link
|
|
9
|
+
* paylaşımı yeterli olur. Token boşsa gate açık olsa da kimse kilitlenmez.
|
|
10
|
+
*/
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
import { getConfig } from "../../config/index.js";
|
|
13
|
+
|
|
14
|
+
const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 14;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('http').IncomingMessage} req
|
|
18
|
+
* @param {string} name
|
|
19
|
+
* @returns {string | undefined}
|
|
20
|
+
*/
|
|
21
|
+
function readCookie(req, name) {
|
|
22
|
+
const header = req.headers.cookie;
|
|
23
|
+
if (!header) return undefined;
|
|
24
|
+
|
|
25
|
+
for (const part of header.split(";")) {
|
|
26
|
+
const eq = part.indexOf("=");
|
|
27
|
+
if (eq === -1) continue;
|
|
28
|
+
if (part.slice(0, eq).trim() !== name) continue;
|
|
29
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @returns {import('express').RequestHandler} */
|
|
35
|
+
export function devGate() {
|
|
36
|
+
const { devGate: enabled, devGateBypass, brand } = getConfig();
|
|
37
|
+
const bypass = new Set(devGateBypass);
|
|
38
|
+
const cookieName = brand.devTokenCookie;
|
|
39
|
+
const devToken = process.env.DEV_TOKEN;
|
|
40
|
+
|
|
41
|
+
// Unutulmuş bir değişken sessizce herkese 404 kesmesin; operatör görsün.
|
|
42
|
+
if (!enabled && devToken) {
|
|
43
|
+
console.warn(
|
|
44
|
+
"[dev-gate] DEV_TOKEN is set but the gate is off, so the site stays public. " +
|
|
45
|
+
"Set devGate: true or DEV_GATE=1 to require the token.",
|
|
46
|
+
);
|
|
47
|
+
} else if (enabled && !devToken) {
|
|
48
|
+
console.warn(
|
|
49
|
+
"[dev-gate] the gate is on but DEV_TOKEN is empty; requests are not blocked.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (req, res, next) => {
|
|
54
|
+
if (!enabled || !devToken) return next();
|
|
55
|
+
if (bypass.has(req.path)) return next();
|
|
56
|
+
|
|
57
|
+
const fromQuery = req.query?.[cookieName];
|
|
58
|
+
const tokenFromQuery = Array.isArray(fromQuery) ? fromQuery[0] : fromQuery;
|
|
59
|
+
const tokenFromCookie = readCookie(req, cookieName);
|
|
60
|
+
|
|
61
|
+
if (tokenFromQuery !== devToken && tokenFromCookie !== devToken) {
|
|
62
|
+
res.status(404).type("text/plain").send("Not Found");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (tokenFromQuery === devToken) {
|
|
67
|
+
res.setHeader(
|
|
68
|
+
"Set-Cookie",
|
|
69
|
+
`${cookieName}=${encodeURIComponent(devToken)}; Max-Age=${COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
next();
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jskelet.config.mjs` → `headers()` uygulayıcısı.
|
|
3
|
-
*
|
|
4
|
-
* İki katman var. Framework yalnızca statik dosyalara uzun ömürlü cache
|
|
5
|
-
* yazar — bu, hash'li çıktı için her projede doğru olan tek varsayılan.
|
|
6
|
-
* Bunun dışındaki her başlık (CSP, COOP, HSTS, X-Frame-Options…) config'ten
|
|
7
|
-
* gelir ve varsayılanların üstüne biner: aynı başlığı yeniden tanımlarsan
|
|
8
|
-
* config kazanır.
|
|
9
|
-
*/
|
|
10
|
-
import { getConfig } from "../../config/index.js";
|
|
11
|
-
import { matchPattern } from "../../config/pattern.js";
|
|
12
|
-
import { IMMUTABLE_CACHE } from "../../config/defaults.js";
|
|
13
|
-
|
|
14
|
-
/** @returns {import('express').RequestHandler} */
|
|
15
|
-
export function headersMiddleware() {
|
|
16
|
-
const { static: staticRules, headers: rules } = getConfig();
|
|
17
|
-
|
|
18
|
-
return (req, res, next) => {
|
|
19
|
-
const pathname = req.path ?? "";
|
|
20
|
-
const dot = pathname.lastIndexOf(".");
|
|
21
|
-
const ext = dot === -1 ? "" : pathname.slice(dot).toLowerCase();
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
staticRules.extensions.has(ext) ||
|
|
25
|
-
staticRules.prefixes.some((prefix) => pathname.startsWith(prefix))
|
|
26
|
-
) {
|
|
27
|
-
res.setHeader("Cache-Control", IMMUTABLE_CACHE);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (const rule of rules) {
|
|
31
|
-
if (!matchPattern(rule.pattern, pathname)) continue;
|
|
32
|
-
for (const { key, value } of rule.headers) res.setHeader(key, value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
next();
|
|
36
|
-
};
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet.config.mjs` → `headers()` uygulayıcısı.
|
|
3
|
+
*
|
|
4
|
+
* İki katman var. Framework yalnızca statik dosyalara uzun ömürlü cache
|
|
5
|
+
* yazar — bu, hash'li çıktı için her projede doğru olan tek varsayılan.
|
|
6
|
+
* Bunun dışındaki her başlık (CSP, COOP, HSTS, X-Frame-Options…) config'ten
|
|
7
|
+
* gelir ve varsayılanların üstüne biner: aynı başlığı yeniden tanımlarsan
|
|
8
|
+
* config kazanır.
|
|
9
|
+
*/
|
|
10
|
+
import { getConfig } from "../../config/index.js";
|
|
11
|
+
import { matchPattern } from "../../config/pattern.js";
|
|
12
|
+
import { IMMUTABLE_CACHE } from "../../config/defaults.js";
|
|
13
|
+
|
|
14
|
+
/** @returns {import('express').RequestHandler} */
|
|
15
|
+
export function headersMiddleware() {
|
|
16
|
+
const { static: staticRules, headers: rules } = getConfig();
|
|
17
|
+
|
|
18
|
+
return (req, res, next) => {
|
|
19
|
+
const pathname = req.path ?? "";
|
|
20
|
+
const dot = pathname.lastIndexOf(".");
|
|
21
|
+
const ext = dot === -1 ? "" : pathname.slice(dot).toLowerCase();
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
staticRules.extensions.has(ext) ||
|
|
25
|
+
staticRules.prefixes.some((prefix) => pathname.startsWith(prefix))
|
|
26
|
+
) {
|
|
27
|
+
res.setHeader("Cache-Control", IMMUTABLE_CACHE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const rule of rules) {
|
|
31
|
+
if (!matchPattern(rule.pattern, pathname)) continue;
|
|
32
|
+
for (const { key, value } of rule.headers) res.setHeader(key, value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
next();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `jskelet.config.mjs` → `redirects()` uygulayıcısı.
|
|
3
|
-
*
|
|
4
|
-
* İlk eşleşen kural kazanır, sonrası denenmez — sıralama config'teki yazım
|
|
5
|
-
* sırasıdır. Query string korunur: `/eski?utm=x` → `/yeni?utm=x`, çünkü
|
|
6
|
-
* yönlendirme kampanya parametrelerini düşürürse trafik kaynağı kaybolur.
|
|
7
|
-
*/
|
|
8
|
-
import { getConfig } from "../../config/index.js";
|
|
9
|
-
import { fillDestination, matchPattern } from "../../config/pattern.js";
|
|
10
|
-
|
|
11
|
-
/** @returns {import('express').RequestHandler} */
|
|
12
|
-
export function redirects() {
|
|
13
|
-
const rules = getConfig().redirects;
|
|
14
|
-
if (!rules.length) return (req, res, next) => next();
|
|
15
|
-
|
|
16
|
-
return (req, res, next) => {
|
|
17
|
-
const query = req.originalUrl.slice(req.path.length);
|
|
18
|
-
|
|
19
|
-
for (const rule of rules) {
|
|
20
|
-
const params = matchPattern(rule.pattern, req.path);
|
|
21
|
-
if (!params) continue;
|
|
22
|
-
|
|
23
|
-
res.redirect(
|
|
24
|
-
rule.statusCode,
|
|
25
|
-
`${fillDestination(rule.destination, params)}${query}`,
|
|
26
|
-
);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
next();
|
|
31
|
-
};
|
|
32
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `jskelet.config.mjs` → `redirects()` uygulayıcısı.
|
|
3
|
+
*
|
|
4
|
+
* İlk eşleşen kural kazanır, sonrası denenmez — sıralama config'teki yazım
|
|
5
|
+
* sırasıdır. Query string korunur: `/eski?utm=x` → `/yeni?utm=x`, çünkü
|
|
6
|
+
* yönlendirme kampanya parametrelerini düşürürse trafik kaynağı kaybolur.
|
|
7
|
+
*/
|
|
8
|
+
import { getConfig } from "../../config/index.js";
|
|
9
|
+
import { fillDestination, matchPattern } from "../../config/pattern.js";
|
|
10
|
+
|
|
11
|
+
/** @returns {import('express').RequestHandler} */
|
|
12
|
+
export function redirects() {
|
|
13
|
+
const rules = getConfig().redirects;
|
|
14
|
+
if (!rules.length) return (req, res, next) => next();
|
|
15
|
+
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
const query = req.originalUrl.slice(req.path.length);
|
|
18
|
+
|
|
19
|
+
for (const rule of rules) {
|
|
20
|
+
const params = matchPattern(rule.pattern, req.path);
|
|
21
|
+
if (!params) continue;
|
|
22
|
+
|
|
23
|
+
res.redirect(
|
|
24
|
+
rule.statusCode,
|
|
25
|
+
`${fillDestination(rule.destination, params)}${query}`,
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
next();
|
|
31
|
+
};
|
|
32
|
+
}
|