jskelet 0.1.1 → 0.1.2

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 (64) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +63 -0
  3. package/README.md +21 -7
  4. package/bin/jskelet.mjs +6 -6
  5. package/docs/03-routing.md +48 -9
  6. package/docs/04-render-ve-sablonlar.md +2 -2
  7. package/docs/05-islands.md +59 -6
  8. package/docs/06-cache.md +39 -7
  9. package/docs/07-yapilandirma.md +51 -1
  10. package/docs/08-build.md +4 -4
  11. package/docs/09-dev-araclari.md +5 -0
  12. package/docs/12-panel-ve-oturum.md +384 -0
  13. package/docs/README.md +25 -2
  14. package/docs/en/01-getting-started.md +292 -0
  15. package/docs/en/02-architecture.md +305 -0
  16. package/docs/en/03-routing.md +493 -0
  17. package/docs/en/04-rendering.md +504 -0
  18. package/docs/en/05-islands.md +492 -0
  19. package/docs/en/06-caching.md +454 -0
  20. package/docs/en/07-configuration.md +736 -0
  21. package/docs/en/08-build.md +383 -0
  22. package/docs/en/09-dev-tools.md +314 -0
  23. package/docs/en/10-deployment.md +332 -0
  24. package/docs/en/11-migration.md +360 -0
  25. package/docs/en/12-dashboards-and-sessions.md +392 -0
  26. package/docs/en/README.md +112 -0
  27. package/package.json +4 -2
  28. package/src/build/build.mjs +1 -1
  29. package/src/build/tasks/client.mjs +2 -2
  30. package/src/build/tasks/fonts.mjs +3 -3
  31. package/src/build/tasks/icons.mjs +1 -1
  32. package/src/build/tasks/images.mjs +2 -2
  33. package/src/client/devtools/overlay.js +196 -164
  34. package/src/client/devtools/report.js +96 -96
  35. package/src/client/form.js +192 -0
  36. package/src/client/index.js +10 -1
  37. package/src/client/registry.js +78 -4
  38. package/src/client/swap.js +188 -0
  39. package/src/config/defaults.js +34 -0
  40. package/src/config/index.js +68 -13
  41. package/src/config/pattern.js +1 -1
  42. package/src/dev-server.mjs +1 -1
  43. package/src/http/control-flow.js +16 -1
  44. package/src/http/cookies.js +257 -0
  45. package/src/http/request-context.js +162 -0
  46. package/src/index.js +19 -2
  47. package/src/init.mjs +32 -31
  48. package/src/log.mjs +8 -2
  49. package/src/logo.png +0 -0
  50. package/src/runtime/alias-hooks.mjs +1 -1
  51. package/src/server/assets.js +1 -1
  52. package/src/server/create-app.js +12 -4
  53. package/src/server/dev/devtools.js +6 -2
  54. package/src/server/dev/version-check.mjs +139 -0
  55. package/src/server/head-hints.js +1 -1
  56. package/src/server/html-cache.js +10 -4
  57. package/src/server/middleware/csrf.js +134 -0
  58. package/src/server/prewarm.js +6 -6
  59. package/src/server/render.js +199 -16
  60. package/src/server/router.js +14 -7
  61. package/src/server/status-page.js +1 -1
  62. package/src/version.mjs +9 -4
  63. package/src/views/components/loader.js +1 -1
  64. package/src/views/helpers/tags.js +53 -1
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 yapılandırması. Tüm alanlar opsiyoneldir; bu dosyayı silseniz de
16
- * uygulama varsayılanlarla çalışır.
15
+ * JSkelet configuration. Every field is optional; the app still runs on
16
+ * defaults if you delete this file.
17
17
  *
18
- * Ayrıntılar: node_modules/jskelet/docs/07-yapilandirma.md
18
+ * Details: node_modules/jskelet/docs/en/07-configuration.md
19
19
  */
20
20
  export default {
21
- brand: { lang: "tr" },
21
+ brand: { lang: "en" },
22
22
 
23
- /** Üçüncü taraf kaynaklar; \`<head>\`e preconnect olarak basılır. */
23
+ /** Third-party origins; emitted as preconnect in \`<head>\`. */
24
24
  preconnect: [],
25
25
 
26
26
  async cache() {
27
27
  return {
28
- /** Sayfa HTML'inin önbellekte kalma süresi (saniye). */
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
- /** Her sayfanın metadata varsayılanı. */
34
+ /** Metadata defaults for every page. */
35
35
  metadata() {
36
36
  return {
37
37
  titleTemplate: "%s | JSkelet",
38
- description: "JSkelet ile kurulmuş bir site.",
38
+ description: "A site built with JSkelet.",
39
39
  };
40
40
  },
41
41
 
42
- /** Layout'a her render'da eklenen local'ler. */
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 sayfası. */
47
+ /** 404 page. */
48
48
  notFound() {
49
49
  return {
50
50
  view: "pages/not-found",
51
- metadata: { title: "Sayfa bulunamadı", robots: { index: false } },
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 modülü. Default export \`(app, api)\` alır; \`api.route()\` controller'ı
60
- * HTML cache'i, notFound/redirect akışı ve sıkıştırmayla sarar.
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
- * Dosya adındaki sayısal önek yükleme sırasını belirler: yakalayıcı
63
- * ("/:slug" gibi) route'lar daha yüksek numarada olmalı.
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: "Ana sayfa" },
72
- data: { message: "JSkelet çalışıyor." },
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>Aradığınız sayfa bulunamadı.</p>
90
- <p><a href="/">Ana sayfaya dön</a></p>
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/**\` altındaki her named export şablonlarda doğrudan
98
- * kullanılabilir: \`<%- button({ text: "Kaydet" }) %>\`. Import gerekmez.
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 kaydı. Değerler dinamik import: modül yalnızca sayfada o island
113
- * gerçekten varsa ve görünür olduğunda indirilir.
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 sözleşmesi: \`mount(element, props)\` adlı named export.
124
- * Dönen fonksiyon (varsa) temizlik için ayrılmıştır.
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 = \`Tıklama: \${value}\`;
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'in sınıf taraması bu direktiflere bağlıdır. Otomatik tespit
153
- * yalnızca bu dosyanın bulunduğu dizini tarar; şablonlarda geçen varyantlar
154
- * (data-[active=false]:… gibi) aksi hâlde sessizce düşer.
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} dosya zaten vardı, atlandı`);
217
+ if (skipped.length) log.warn(`${skipped.length} files already existed, skipped`);
217
218
 
218
219
  log.line("");
219
- log.line("sıradaki adım: npx jskelet dev");
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
- /** @returns {string} `01:49:02` */
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("tr-TR", { hour12: false });
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} okunamadı, alias'lar devre dışı`, error);
59
+ console.warn(`[alias] could not read ${name}, aliases disabled`, error);
60
60
  return [];
61
61
  }
62
62
  }
@@ -30,7 +30,7 @@ function load() {
30
30
  } catch {
31
31
  if (!warned) {
32
32
  warned = true;
33
- console.warn("[assets] manifest yok — `jskelet build` çalıştırın.");
33
+ console.warn("[assets] no manifest — run `jskelet build`.");
34
34
  }
35
35
  }
36
36
 
@@ -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. rewrites(afterFiles) — statik denendikten sonra, sayfalardan önce.
17
- * 7. route'lar → 404 → hata yönetimi.
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
- app.set("trust proxy", true);
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);
@@ -21,6 +21,7 @@ import {
21
21
  recordPageReport,
22
22
  trackServerFetch,
23
23
  } from "./report.js";
24
+ import { startVersionCheck, versionStatus } from "./version-check.mjs";
24
25
 
25
26
  /** Overlay dosyaları framework paketinden servis edilir, uygulamadan değil. */
26
27
  const DEVTOOLS_DIR = path.join(FRAMEWORK_ROOT, "src", "client", "devtools");
@@ -267,7 +268,8 @@ function router() {
267
268
 
268
269
  api.get("/logo.png", (req, res) => {
269
270
  res.type("image/png");
270
- res.setHeader("Cache-Control", "no-store");
271
+ // Logo geliştirme sırasında değişmiyor; her gezinmede yeniden indirmesin.
272
+ res.setHeader("Cache-Control", "public, max-age=86400");
271
273
  fs.createReadStream(LOGO_FILE).pipe(res);
272
274
  });
273
275
 
@@ -293,6 +295,7 @@ function router() {
293
295
  boot: BOOT_ID,
294
296
  uptime: process.uptime(),
295
297
  node: process.version,
298
+ version: versionStatus(),
296
299
  memory: { rss: usage.rss, heapUsed: usage.heapUsed },
297
300
  prewarm: { ...prewarmProgress },
298
301
  requests: requests.slice(-25).reverse(),
@@ -351,7 +354,7 @@ function router() {
351
354
  origin: `${req.protocol}://${req.get("host")}`,
352
355
  paths: paths.length ? paths : undefined,
353
356
  }).catch((error) => {
354
- console.error("[prewarm] elle tetikleme başarısız", error);
357
+ console.error("[prewarm] manual trigger failed", error);
355
358
  });
356
359
 
357
360
  res.json({ ok: true, scope: paths.length || "all" });
@@ -378,6 +381,7 @@ export function mountDevtools(app) {
378
381
  patchConsole();
379
382
  trackServerFetch();
380
383
  watchManifest();
384
+ startVersionCheck();
381
385
  app.use(timing());
382
386
  app.use(brand.devBasePath, router());
383
387
  }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * npm kayıt defterindeki son sürümü yoklar ve dev overlay'e "outdated" bilgisi
3
+ * verir. Yalnızca geliştirme sırasında çalışır.
4
+ *
5
+ * Sonuç geçici dizinde saklanıyor: `node --watch` sunucuyu sık yeniden
6
+ * başlatıyor ve her açılışta ağa çıkmak hem yavaş hem gereksiz. Kayıt defteri
7
+ * ulaşılamazsa sessizce eski/boş sonuç kullanılır — sürüm kontrolü hiçbir
8
+ * koşulda dev akışını bekletmez ya da hata basmaz.
9
+ */
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { FRAMEWORK_PACKAGE, FRAMEWORK_VERSION } from "../../version.mjs";
14
+
15
+ const CACHE_FILE = path.join(os.tmpdir(), `jskelet-version-${FRAMEWORK_PACKAGE}.json`);
16
+
17
+ /** Aynı sürümü saatte birden fazla sormanın anlamı yok. */
18
+ const TTL = 6 * 60 * 60 * 1000;
19
+
20
+ const TIMEOUT = 3000;
21
+
22
+ /** @type {{ current: string, latest: string | null, outdated: boolean, checkedAt: number | null }} */
23
+ let state = {
24
+ current: FRAMEWORK_VERSION,
25
+ latest: null,
26
+ outdated: false,
27
+ checkedAt: null,
28
+ };
29
+
30
+ /**
31
+ * Ön sürüm etiketleri (`1.2.0-beta.1`) karşılaştırmada yok sayılır: kayıt
32
+ * defterinden yalnızca `latest` etiketi okunduğu için pratikte gelmiyor.
33
+ * @param {string} a
34
+ * @param {string} b
35
+ * @returns {number} a > b ise pozitif
36
+ */
37
+ function compare(a, b) {
38
+ const parse = (value) =>
39
+ String(value)
40
+ .split("-")[0]
41
+ .split(".")
42
+ .map((part) => Number.parseInt(part, 10) || 0);
43
+
44
+ const left = parse(a);
45
+ const right = parse(b);
46
+
47
+ for (let index = 0; index < 3; index += 1) {
48
+ const diff = (left[index] ?? 0) - (right[index] ?? 0);
49
+ if (diff) return diff;
50
+ }
51
+
52
+ return 0;
53
+ }
54
+
55
+ /**
56
+ * @param {string | null} latest
57
+ */
58
+ function apply(latest) {
59
+ state = {
60
+ current: FRAMEWORK_VERSION,
61
+ latest,
62
+ outdated: Boolean(latest) && compare(latest, FRAMEWORK_VERSION) > 0,
63
+ checkedAt: Date.now(),
64
+ };
65
+ }
66
+
67
+ /** @returns {{ latest: string, checkedAt: number } | null} */
68
+ function readCache() {
69
+ try {
70
+ const saved = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
71
+ if (typeof saved.latest !== "string" || typeof saved.checkedAt !== "number") return null;
72
+ return saved;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ async function fetchLatest() {
79
+ const controller = new AbortController();
80
+ const timer = setTimeout(() => controller.abort(), TIMEOUT);
81
+
82
+ try {
83
+ const response = await fetch(
84
+ `https://registry.npmjs.org/${encodeURIComponent(FRAMEWORK_PACKAGE)}/latest`,
85
+ {
86
+ signal: controller.signal,
87
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
88
+ },
89
+ );
90
+
91
+ if (!response.ok) return;
92
+
93
+ const body = await response.json();
94
+ if (typeof body?.version !== "string") return;
95
+
96
+ apply(body.version);
97
+ try {
98
+ fs.writeFileSync(
99
+ CACHE_FILE,
100
+ JSON.stringify({ latest: body.version, checkedAt: state.checkedAt }),
101
+ );
102
+ } catch {
103
+ // Önbellek yazılamazsa yalnızca her açılışta yeniden sorulur.
104
+ }
105
+ } catch {
106
+ // Ağ yok, kayıt defteri kapalı ya da zaman aşımı: sürüm bilgisi boş kalır.
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Kontrolü başlatır. Beklenmez; sonuç geldiğinde `versionStatus()` günceldir.
114
+ * `JSKELET_VERSION_CHECK=0` ile tamamen kapatılabilir (çevrimdışı çalışma,
115
+ * kurumsal ağlar).
116
+ */
117
+ export function startVersionCheck() {
118
+ if (process.env.JSKELET_VERSION_CHECK === "0") return;
119
+
120
+ const cached = readCache();
121
+ if (cached) {
122
+ apply(cached.latest);
123
+ state.checkedAt = cached.checkedAt;
124
+ if (Date.now() - cached.checkedAt < TTL) return;
125
+ }
126
+
127
+ // Açılışta ağ isteği ilk isteğin önüne geçmesin.
128
+ const timer = setTimeout(() => {
129
+ fetchLatest();
130
+ }, 1500);
131
+ timer.unref?.();
132
+ }
133
+
134
+ /**
135
+ * @returns {{ current: string, latest: string | null, outdated: boolean, checkedAt: number | null }}
136
+ */
137
+ export function versionStatus() {
138
+ return state;
139
+ }
@@ -29,7 +29,7 @@ export function preconnectHints() {
29
29
  try {
30
30
  return `<link rel="preconnect" href="${esc(new URL(origin).origin)}">`;
31
31
  } catch {
32
- console.warn(`[head] preconnect geçersiz URL, atlandı: ${origin}`);
32
+ console.warn(`[head] invalid preconnect URL, skipped: ${origin}`);
33
33
  return "";
34
34
  }
35
35
  })
@@ -83,8 +83,10 @@ function write(key, value, ttlSeconds) {
83
83
  /**
84
84
  * @param {string} key
85
85
  * @param {number} ttlSeconds
86
- * @param {() => Promise<{ html: string, status: number, degraded?: boolean }>} producer
87
- * @returns {Promise<{ html: string, status: number, degraded?: boolean }>}
86
+ * @param {() => Promise<{ html: string, status: number, degraded?: boolean,
87
+ * storable?: boolean }>} producer
88
+ * @returns {Promise<{ html: string, status: number, degraded?: boolean,
89
+ * storable?: boolean }>}
88
90
  */
89
91
  function refresh(key, ttlSeconds, producer) {
90
92
  const pending = inflight.get(key);
@@ -94,7 +96,11 @@ function refresh(key, ttlSeconds, producer) {
94
96
  .then((value) => {
95
97
  // `degraded`: upstream düştüğü için eksik veriyle üretilmiş HTML.
96
98
  // Saklanırsa eksik içerik tüm TTL boyunca servis edilir.
97
- if (value.status === 200 && !value.degraded) {
99
+ //
100
+ // `storable: false`: çıktı kullanıcıya bağlı (cookie/Authorization
101
+ // okundu). Anahtar yalnızca yol + query olduğu için saklamak, bir
102
+ // kullanıcının HTML'ini bir başkasına servis etmek olur.
103
+ if (value.status === 200 && !value.degraded && value.storable !== false) {
98
104
  write(key, value, ttlSeconds);
99
105
  }
100
106
  return value;
@@ -127,7 +133,7 @@ export async function withHtmlCache(key, ttlSeconds, producer) {
127
133
  // isteği etkilemez (eski HTML stale penceresi boyunca geçerli kalır).
128
134
  if (hit.stale) {
129
135
  void refresh(key, ttlSeconds, producer).catch((error) => {
130
- console.error(`[html-cache] arka plan tazelemesi başarısız: ${key}`, error);
136
+ console.error(`[html-cache] background refresh failed: ${key}`, error);
131
137
  });
132
138
  }
133
139
  return { ...hit, cached: true };
@@ -0,0 +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
+ }
@@ -64,7 +64,7 @@ async function collectPaths() {
64
64
  const paths = await hook("prewarmPaths", []);
65
65
 
66
66
  if (!Array.isArray(paths)) {
67
- console.warn("[prewarm] hooks.prewarmPaths() dizi döndürmeli, yok sayıldı");
67
+ console.warn("[prewarm] hooks.prewarmPaths() must return an array, ignoring it");
68
68
  return [];
69
69
  }
70
70
 
@@ -237,10 +237,10 @@ export async function prewarm({ origin, quiet = false, paths: only }) {
237
237
  if (!quiet && paths.length) {
238
238
  const skipped = all.length - paths.length;
239
239
  console.log(
240
- `[prewarm] ${ok}/${paths.length} sayfa ısıtıldı` +
241
- `${failed ? `, ${failed} hata` : ""}` +
242
- `${recovered ? `, ${recovered} sayfa tekrar turunda kurtarıldı` : ""}` +
243
- `${skipped > 0 ? `, ${skipped} sayfa limit dışı` : ""}` +
240
+ `[prewarm] warmed ${ok}/${paths.length} pages` +
241
+ `${failed ? `, ${failed} failed` : ""}` +
242
+ `${recovered ? `, ${recovered} recovered on the retry pass` : ""}` +
243
+ `${skipped > 0 ? `, ${skipped} over the limit` : ""}` +
244
244
  ` (${(elapsed / 1000).toFixed(1)}s)`,
245
245
  );
246
246
  }
@@ -266,7 +266,7 @@ export function startPrewarm({ port }) {
266
266
  const origin = `http://127.0.0.1:${port}`;
267
267
  const run = () =>
268
268
  prewarm({ origin }).catch((error) => {
269
- console.error("[prewarm] başarısız", error);
269
+ console.error("[prewarm] failed", error);
270
270
  });
271
271
 
272
272
  // Isıtma ilk isteklerle yarışmasın diye gecikmeyle başlar. Dev'de gecikme