jskelet 0.2.5 → 0.3.0

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.
Files changed (106) hide show
  1. package/AGENTS.md +132 -132
  2. package/CHANGELOG.md +403 -383
  3. package/LICENSE +21 -21
  4. package/bin/jskelet.mjs +103 -103
  5. package/docs/01-baslangic.md +285 -285
  6. package/docs/02-mimari.md +293 -287
  7. package/docs/03-routing.md +486 -480
  8. package/docs/04-render-ve-sablonlar.md +490 -490
  9. package/docs/05-islands.md +482 -482
  10. package/docs/06-cache.md +1231 -1209
  11. package/docs/07-yapilandirma.md +44 -21
  12. package/docs/08-build.md +366 -366
  13. package/docs/09-dev-araclari.md +335 -335
  14. package/docs/10-dagitim.md +329 -329
  15. package/docs/11-tasima.md +1 -0
  16. package/docs/12-panel-ve-oturum.md +384 -384
  17. package/docs/README.md +105 -105
  18. package/docs/en/01-getting-started.md +292 -292
  19. package/docs/en/02-architecture.md +311 -305
  20. package/docs/en/03-routing.md +503 -497
  21. package/docs/en/04-rendering.md +504 -504
  22. package/docs/en/05-islands.md +492 -492
  23. package/docs/en/06-caching.md +1197 -1198
  24. package/docs/en/07-configuration.md +1009 -986
  25. package/docs/en/08-build.md +383 -383
  26. package/docs/en/09-dev-tools.md +342 -342
  27. package/docs/en/10-deployment.md +332 -332
  28. package/docs/en/11-migration.md +360 -359
  29. package/docs/en/12-dashboards-and-sessions.md +392 -392
  30. package/docs/en/README.md +112 -112
  31. package/package.json +102 -102
  32. package/src/build/ensure-build.mjs +15 -15
  33. package/src/build/paths.mjs +143 -143
  34. package/src/build/resolve-peer.mjs +36 -36
  35. package/src/build/tasks/client.mjs +268 -268
  36. package/src/build/tasks/css.mjs +124 -124
  37. package/src/build/tasks/fonts.mjs +146 -146
  38. package/src/build/tasks/icons.mjs +224 -224
  39. package/src/build/tasks/images.mjs +244 -244
  40. package/src/build/tasks/precompress.mjs +78 -78
  41. package/src/client/{cache-panel → admin}/i18n.js +756 -670
  42. package/src/client/{cache-panel → admin}/login.html +74 -74
  43. package/src/client/{cache-panel → admin}/panel.css +804 -756
  44. package/src/client/admin/panel.html +486 -0
  45. package/src/client/{cache-panel → admin}/panel.js +1242 -915
  46. package/src/client/devtools/report.html +185 -185
  47. package/src/client/devtools/report.js +725 -725
  48. package/src/client/dom.js +95 -95
  49. package/src/client/form.js +192 -192
  50. package/src/client/index.js +35 -35
  51. package/src/client/registry.js +297 -297
  52. package/src/client/safe-image.js +91 -91
  53. package/src/client/store.js +36 -36
  54. package/src/client/swap.js +188 -188
  55. package/src/config/defaults.js +16 -7
  56. package/src/config/index.js +38 -22
  57. package/src/config/pattern.js +107 -107
  58. package/src/http/control-flow.js +71 -71
  59. package/src/http/cookies.js +257 -257
  60. package/src/http/request-cache.js +46 -46
  61. package/src/http/request-context.js +162 -162
  62. package/src/index.js +83 -83
  63. package/src/init.mjs +221 -221
  64. package/src/log.mjs +58 -0
  65. package/src/runtime/alias-hooks.mjs +119 -119
  66. package/src/runtime/register.mjs +4 -4
  67. package/src/server/admin/actions.js +229 -0
  68. package/src/server/admin/auth.js +125 -0
  69. package/src/server/admin/event-log.js +151 -0
  70. package/src/server/admin/gate.js +209 -0
  71. package/src/server/admin/inventory.js +188 -0
  72. package/src/server/admin/mount.js +56 -0
  73. package/src/server/admin/router.js +216 -0
  74. package/src/server/admin/snapshot.js +126 -0
  75. package/src/server/assets.js +147 -147
  76. package/src/server/cache-deps.js +42 -42
  77. package/src/server/cloudflare.js +607 -607
  78. package/src/server/create-app.js +295 -291
  79. package/src/server/data-cache.js +462 -462
  80. package/src/server/dev/report.js +369 -369
  81. package/src/server/dev/socket.js +170 -170
  82. package/src/server/dev/version-check.mjs +139 -139
  83. package/src/server/html-cache.js +817 -817
  84. package/src/server/metadata.js +102 -102
  85. package/src/server/middleware/compression.js +205 -205
  86. package/src/server/middleware/csrf.js +134 -134
  87. package/src/server/middleware/dev-gate.js +62 -62
  88. package/src/server/middleware/headers.js +37 -37
  89. package/src/server/middleware/redirects.js +32 -32
  90. package/src/server/middleware/static-precompressed.js +100 -100
  91. package/src/server/middleware/trailing-slash.js +53 -0
  92. package/src/server/middleware/upstream-proxy.js +141 -141
  93. package/src/server/prewarm.js +601 -601
  94. package/src/server/redis.js +569 -569
  95. package/src/server/router.js +128 -128
  96. package/src/server/status-page.js +164 -164
  97. package/src/server/upstream-limiter.js +376 -376
  98. package/src/server/upstream-tracking.js +166 -166
  99. package/src/start.mjs +7 -7
  100. package/src/templates/layout.ejs +44 -44
  101. package/src/version.mjs +31 -31
  102. package/src/views/components/loader.js +85 -85
  103. package/src/views/helpers/html.js +102 -102
  104. package/src/views/helpers/tags.js +245 -245
  105. package/src/client/cache-panel/panel.html +0 -308
  106. package/src/server/cache-panel.js +0 -759
@@ -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,62 +1,62 @@
1
- /**
2
- * Yayına açılmamış bir ortamı gizler: `DEV_TOKEN` ayarlıyken token taşımayan
3
- * her isteğe 404 döner. 403 değil 404 — 403 ortamın var olduğunu doğrular,
4
- * 404 hiç yokmuş gibi davranır.
5
- *
6
- * Token bir kez `?dev_token=…` ile gelirse çereze yazılır, böylece link
7
- * paylaşımı yeterli olur. `DEV_TOKEN` yoksa middleware tamamen devre dışıdır
8
- * ve üretimde hiçbir maliyeti olmaz.
9
- */
10
- import process from "node:process";
11
- import { getConfig } from "../../config/index.js";
12
-
13
- const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 14;
14
-
15
- /**
16
- * @param {import('http').IncomingMessage} req
17
- * @param {string} name
18
- * @returns {string | undefined}
19
- */
20
- function readCookie(req, name) {
21
- const header = req.headers.cookie;
22
- if (!header) return undefined;
23
-
24
- for (const part of header.split(";")) {
25
- const eq = part.indexOf("=");
26
- if (eq === -1) continue;
27
- if (part.slice(0, eq).trim() !== name) continue;
28
- return decodeURIComponent(part.slice(eq + 1).trim());
29
- }
30
- return undefined;
31
- }
32
-
33
- /** @returns {import('express').RequestHandler} */
34
- export function devGate() {
35
- const { devGateBypass, brand } = getConfig();
36
- const bypass = new Set(devGateBypass);
37
- const cookieName = brand.devTokenCookie;
38
-
39
- return (req, res, next) => {
40
- const devToken = process.env.DEV_TOKEN;
41
- if (!devToken) return next();
42
- if (bypass.has(req.path)) return next();
43
-
44
- const fromQuery = req.query?.[cookieName];
45
- const tokenFromQuery = Array.isArray(fromQuery) ? fromQuery[0] : fromQuery;
46
- const tokenFromCookie = readCookie(req, cookieName);
47
-
48
- if (tokenFromQuery !== devToken && tokenFromCookie !== devToken) {
49
- res.status(404).type("text/plain").send("Not Found");
50
- return;
51
- }
52
-
53
- if (tokenFromQuery === devToken) {
54
- res.setHeader(
55
- "Set-Cookie",
56
- `${cookieName}=${encodeURIComponent(devToken)}; Max-Age=${COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax`,
57
- );
58
- }
59
-
60
- next();
61
- };
62
- }
1
+ /**
2
+ * Yayına açılmamış bir ortamı gizler: `DEV_TOKEN` ayarlıyken token taşımayan
3
+ * her isteğe 404 döner. 403 değil 404 — 403 ortamın var olduğunu doğrular,
4
+ * 404 hiç yokmuş gibi davranır.
5
+ *
6
+ * Token bir kez `?dev_token=…` ile gelirse çereze yazılır, böylece link
7
+ * paylaşımı yeterli olur. `DEV_TOKEN` yoksa middleware tamamen devre dışıdır
8
+ * ve üretimde hiçbir maliyeti olmaz.
9
+ */
10
+ import process from "node:process";
11
+ import { getConfig } from "../../config/index.js";
12
+
13
+ const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 14;
14
+
15
+ /**
16
+ * @param {import('http').IncomingMessage} req
17
+ * @param {string} name
18
+ * @returns {string | undefined}
19
+ */
20
+ function readCookie(req, name) {
21
+ const header = req.headers.cookie;
22
+ if (!header) return undefined;
23
+
24
+ for (const part of header.split(";")) {
25
+ const eq = part.indexOf("=");
26
+ if (eq === -1) continue;
27
+ if (part.slice(0, eq).trim() !== name) continue;
28
+ return decodeURIComponent(part.slice(eq + 1).trim());
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ /** @returns {import('express').RequestHandler} */
34
+ export function devGate() {
35
+ const { devGateBypass, brand } = getConfig();
36
+ const bypass = new Set(devGateBypass);
37
+ const cookieName = brand.devTokenCookie;
38
+
39
+ return (req, res, next) => {
40
+ const devToken = process.env.DEV_TOKEN;
41
+ if (!devToken) return next();
42
+ if (bypass.has(req.path)) return next();
43
+
44
+ const fromQuery = req.query?.[cookieName];
45
+ const tokenFromQuery = Array.isArray(fromQuery) ? fromQuery[0] : fromQuery;
46
+ const tokenFromCookie = readCookie(req, cookieName);
47
+
48
+ if (tokenFromQuery !== devToken && tokenFromCookie !== devToken) {
49
+ res.status(404).type("text/plain").send("Not Found");
50
+ return;
51
+ }
52
+
53
+ if (tokenFromQuery === devToken) {
54
+ res.setHeader(
55
+ "Set-Cookie",
56
+ `${cookieName}=${encodeURIComponent(devToken)}; Max-Age=${COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax`,
57
+ );
58
+ }
59
+
60
+ next();
61
+ };
62
+ }
@@ -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
+ }