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/AGENTS.md CHANGED
@@ -24,6 +24,11 @@ zaten tartışılmış:
24
24
  | `src/config/**` | [07-yapilandirma.md](./docs/07-yapilandirma.md) |
25
25
  | `src/dev-server.mjs`, `src/server/dev/**` | [09-dev-araclari.md](./docs/09-dev-araclari.md) |
26
26
 
27
+ Belgeler iki dilde: Türkçesi `docs/`, İngilizcesi `docs/en/` altında ve
28
+ dosyalar birebir eşlenik. Bir belgeyi değiştirdiysen karşılığını da güncelle;
29
+ pazarlama sitesi (`examples/marketing`) bu dosyaları doğrudan `/docs` altında
30
+ servis ettiği için eksik kalan çeviri kullanıcıya görünür.
31
+
27
32
  ## Doğrulama
28
33
 
29
34
  **Lint yeterli, tam build zorunlu değil.**
package/CHANGELOG.md CHANGED
@@ -10,8 +10,71 @@ one is listed under a **Breaking** heading.
10
10
 
11
11
  ### Added
12
12
 
13
+ - `route(fn, { private: true })` for pages that depend on the visitor. The HTML
14
+ cache is bypassed, `cache.html` patterns can no longer turn caching on for
15
+ that route, and the response is sent with `private, no-store`, `Vary: Cookie`
16
+ and no ETag.
17
+ - A runtime guard against identity leaks: when a cacheable route reads
18
+ `Cookie`, `Authorization` or a session field, the rendered HTML is never
19
+ stored. In development the request fails with an explanation, in production it
20
+ is served with `no-store` and logged.
21
+ - `fragment()` for layout-less partial responses, with `no-store` and cache
22
+ bypass built in.
23
+ - CSRF protection. Cross-site state-changing requests are rejected based on
24
+ `Origin` and `Sec-Fetch-Site`; requests carrying neither header still pass, so
25
+ webhooks keep working. An optional double-submit token layer is enabled with
26
+ `security.csrf.token` and rendered into forms by the new `csrfField()` helper.
27
+ - Signed cookie helpers under `jskelet/cookies`: `parseCookies()`,
28
+ `setCookie()`, `clearCookie()`, `setSignedCookie()`, `getSignedCookie()`,
29
+ `randomToken()` and `safeEqual()`. Defaults are `HttpOnly`, `SameSite=Lax` and
30
+ `Secure` outside development.
31
+ - A `security` configuration section: `trustProxy`, `cookieSecret` and `csrf`.
32
+ - `seeOther()` for the post/redirect/get flow, which needs 303 rather than the
33
+ method-preserving 307 that `redirect()` sends.
34
+ - Island cleanup. A `mount()` function may return a teardown callback; it is now
35
+ stored and called by the new `unmount(root)` export when the subtree leaves
36
+ the DOM.
37
+ - Client helpers for partial updates: `swap()` and `startSwapLinks()` for
38
+ fetching and replacing a region, `enhanceForm()` and `startForms()` for
39
+ submitting forms without a full page load while keeping the no-JavaScript
40
+ path working.
41
+ - A fourth example, `examples/dashboard`: sign-in with a signed cookie session,
42
+ a private page, a paginated table fragment, a CSRF-protected mutation and an
43
+ island with cleanup, covered by its own `smoke.mjs`.
44
+ - An npm version badge in the `README`, linking to the package page.
13
45
  - English `README`, plus `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`,
14
46
  a `LICENSE` file, issue and pull request templates, and a CI workflow.
47
+ - An English edition of the documentation under `docs/en/`, mirroring every
48
+ chapter of the Turkish `docs/`.
49
+ - The dev overlay now compares the installed version against the `latest` tag on
50
+ npm: the Server tab shows the version, marks an `update` chip when a newer
51
+ release exists and offers the upgrade command. The lookup is cached for six
52
+ hours, never blocks the server and can be turned off with
53
+ `JSKELET_VERSION_CHECK=0`.
54
+
55
+ ### Changed
56
+
57
+ - `trust proxy` is now configurable through `security.trustProxy` instead of
58
+ being always on. The default is unchanged, but a server exposed directly to
59
+ the internet should turn it off: while it is on, a client can forge its own
60
+ `X-Forwarded-For` and rate limiting or audit logs see the wrong address.
61
+ - Every message the framework prints is now English: config, router, render,
62
+ cache, prewarm, asset and build warnings, CLI output, the project `jskelet
63
+ init` scaffolds, and the devtools overlay and report interfaces. Visitor-facing
64
+ status pages still follow `brand.lang` and keep their Turkish translations.
65
+ - The dev overlay and report now show the current JSkelet logo, served with a
66
+ cacheable response instead of being re-fetched on every navigation.
67
+
68
+ ### Fixed
69
+
70
+ - A page rendered without `revalidate` used to be sent with no `Cache-Control`
71
+ at all, while still carrying a strong ETag. HTTP treats such a response as
72
+ heuristically cacheable, so an intermediate proxy or the browser's back button
73
+ could store a response meant for a single visitor. Dynamic pages now send
74
+ `private, no-store` and no ETag.
75
+ - A redirect thrown from a route that reads the session is no longer cacheable
76
+ either; a stored "you need to sign in" redirect used to follow the visitor
77
+ even after signing in.
15
78
 
16
79
  ## [0.1.0]
17
80
 
package/README.md CHANGED
@@ -9,6 +9,7 @@ Tailwind v4 stylesheet**, and instead of ISR keeps an in-process **HTML TTL
9
9
  cache** with stale-while-revalidate. No React, no TypeScript — plain JavaScript
10
10
  with JSDoc.
11
11
 
12
+ [![npm version](https://img.shields.io/npm/v/jskelet)](https://www.npmjs.com/package/jskelet)
12
13
  [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
13
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
14
15
 
@@ -171,10 +172,15 @@ client.
171
172
  everything".
172
173
  - **No global state management** beyond the small island store.
173
174
 
174
- If you are building an app-shaped interface behind a login — a dashboard, an
175
- editor, an admin panel — page HTML cannot be cached and this framework is the
176
- wrong tool. The reasoning and a feature-by-feature comparison with Next.js live
177
- in [docs/11-tasima.md](./docs/11-tasima.md).
175
+ An app-shaped interface behind a login — a dashboard, an editor, an admin panel
176
+ — cannot benefit from the HTML cache, which is the main reason to pick this
177
+ framework. It is supported rather than recommended: `route(fn, { private: true })`
178
+ keeps per-visitor pages out of the cache, and signed cookies, CSRF, fragment
179
+ endpoints and region swapping cover the rest
180
+ ([docs/12-panel-ve-oturum.md](./docs/12-panel-ve-oturum.md)). Live data
181
+ transport is deliberately left to you; pick SSE, WebSocket or polling yourself.
182
+ A feature-by-feature comparison with Next.js is in
183
+ [docs/11-tasima.md](./docs/11-tasima.md).
178
184
 
179
185
  ## Project layout
180
186
 
@@ -260,10 +266,11 @@ Only the specifiers in the `exports` map are supported:
260
266
 
261
267
  | Specifier | Contents |
262
268
  | --- | --- |
263
- | `jskelet` | `route`, `createApp`, `startServer`, `notFound`, `redirect`, `cache`, `asset`, `getConfig`, HTML cache and prewarm helpers |
264
- | `jskelet/client` | `register`, `registerAll`, `hydrate`, `start`, `createStore`, DOM helpers |
269
+ | `jskelet` | `route`, `fragment`, `createApp`, `startServer`, `notFound`, `redirect`, `seeOther`, `cache`, `asset`, `getConfig`, cookie helpers, HTML cache and prewarm helpers |
270
+ | `jskelet/client` | `register`, `registerAll`, `hydrate`, `unmount`, `start`, `swap`, `startForms`, `createStore`, DOM helpers |
265
271
  | `jskelet/html` | `attrs`, `cn`, `cx`, `esc`, `jsonScript` |
266
- | `jskelet/tags` | `icon`, `image`, `link`, `preloadImage` |
272
+ | `jskelet/tags` | `icon`, `image`, `link`, `preloadImage`, `csrfField` |
273
+ | `jskelet/cookies` | `parseCookies`, `setCookie`, `clearCookie`, `setSignedCookie`, `getSignedCookie`, `randomToken`, `safeEqual` |
267
274
 
268
275
  Anything reachable by a deeper path is internal and may change without notice.
269
276
 
@@ -295,6 +302,7 @@ written in Turkish; translations are a welcome contribution.
295
302
  | [09-dev-araclari](./docs/09-dev-araclari.md) | Dev workflow, overlay, report page, dev gate |
296
303
  | [10-dagitim](./docs/10-dagitim.md) | Production, Docker, reverse proxy, health checks |
297
304
  | [11-tasima](./docs/11-tasima.md) | Migrating from Next.js: mapping table and plan |
305
+ | [12-panel-ve-oturum](./docs/12-panel-ve-oturum.md) | Per-visitor pages: `private: true`, sessions, CSRF, fragments, swapping |
298
306
 
299
307
  If you work with AI agents, [AGENTS.md](./AGENTS.md) summarizes the rules that
300
308
  apply to this repository.
@@ -305,6 +313,7 @@ apply to this repository.
305
313
  npm --prefix examples/minimal install && npm --prefix examples/minimal run dev
306
314
  npm --prefix examples/blog install && npm --prefix examples/blog run dev
307
315
  npm --prefix examples/marketing install && npm --prefix examples/marketing run dev
316
+ npm --prefix examples/dashboard install && npm --prefix examples/dashboard run dev
308
317
  ```
309
318
 
310
319
  - **`examples/minimal`** — two routes, one component, one island. The smallest
@@ -319,6 +328,11 @@ npm --prefix examples/marketing install && npm --prefix examples/marketing run d
319
328
  are measured in the browser; there are no invented benchmarks. It is also
320
329
  bilingual — English at the root, Turkish under `/tr` — which shows how to build
321
330
  a multi-language site on a framework that ships no i18n of its own.
331
+ - **`examples/dashboard`** — the opposite axis: per-visitor pages. A signed
332
+ cookie session, a `private: true` page that never enters the HTML cache, a
333
+ paginated table fragment, a CSRF-protected form that still works without
334
+ JavaScript, and an island with cleanup. A public landing page sits next to it,
335
+ so a cached response and a `no-store` one are visible side by side.
322
336
 
323
337
  With a server running, `node smoke.mjs` inside an example verifies that its
324
338
  endpoints respond as expected.
package/bin/jskelet.mjs CHANGED
@@ -91,13 +91,13 @@ switch (command) {
91
91
  }
92
92
 
93
93
  default: {
94
- const known = command ? `bilinmeyen komut: ${command}\n\n` : "";
94
+ const known = command ? `unknown command: ${command}\n\n` : "";
95
95
  process.stderr.write(
96
- `${known}kullanım: jskelet <dev|build|start|init>\n\n` +
97
- " dev build watch + sunucu (canlı yenileme, dev overlay)\n" +
98
- " build prod build\n" +
99
- " start prod sunucu\n" +
100
- " init bulunduğun dizine minimal iskelet kurar\n",
96
+ `${known}usage: jskelet <dev|build|start|init>\n\n` +
97
+ " dev build watch + server (live reload, dev overlay)\n" +
98
+ " build production build\n" +
99
+ " start production server\n" +
100
+ " init scaffold a minimal skeleton in the current directory\n",
101
101
  );
102
102
  process.exit(command ? 1 : 0);
103
103
  }
@@ -28,11 +28,13 @@ gerekmez:
28
28
  | Alan | Karşılığı |
29
29
  | --- | --- |
30
30
  | `route` | `jskelet` → `route` |
31
+ | `fragment` | `jskelet` → `fragment` |
31
32
  | `renderView` | `jskelet` → `renderView` |
32
33
  | `renderPage` | `jskelet` → `renderPage` |
33
34
  | `notFound` | `jskelet` → `notFound` |
34
35
  | `redirect` | `jskelet` → `redirect` |
35
36
  | `permanentRedirect` | `jskelet` → `permanentRedirect` |
37
+ | `seeOther` | `jskelet` → `seeOther` |
36
38
 
37
39
  İstersen doğrudan import da edebilirsin; `api` yalnızca kolaylık:
38
40
 
@@ -45,7 +47,7 @@ export function register(app) {
45
47
  ```
46
48
 
47
49
  Modül geçerli bir fonksiyon açmazsa uyarı basılır ve atlanır:
48
- `[router] <dosya> default ya da 'register' fonksiyonu dışa açmıyor, atlandı`.
50
+ `[router] <file> exports neither a default nor a 'register' function, skipped`.
49
51
 
50
52
  ## Yükleme sırası
51
53
 
@@ -97,8 +99,8 @@ Hiç route modülü bulunamazsa uyarı basılır ve sunucu yalnızca statik dosy
97
99
  - `ctx` nesnesini kurar ve controller'ı çağırır.
98
100
  - HTML TTL cache'ini uygular (`revalidate` varsa ve metot `GET` ise).
99
101
  - `notFound()` / `redirect()` kontrol akışını yakalar.
100
- - Yanıt başlıklarını yazar: `Content-Type`, cache'lenebilir yanıtlarda
101
- `Cache-Control`, ve her zaman `X-JSkelet-Cache`.
102
+ - Yanıt başlıklarını yazar: `Content-Type` ve cache durumuna göre
103
+ `Cache-Control` (+ önbelleklenebilir yanıtlarda `X-JSkelet-Cache`).
102
104
  - Önbellekte saklanan sıkıştırılmış gövdeyi kullanarak yanıtı gönderir.
103
105
 
104
106
  ```js
@@ -114,11 +116,42 @@ app.get(
114
116
  );
115
117
  ```
116
118
 
117
- `options` tek bir alan kabul eder:
119
+ `options` iki alan kabul eder:
118
120
 
119
121
  | Alan | Tip | Anlamı |
120
122
  | --- | --- | --- |
121
123
  | `revalidate` | `number` (saniye) | HTML önbellek TTL'i. Verilmezse ya da 0 ise bu route önbelleklenmez. `jskelet.config.mjs` → `cache().html` içindeki eşleşen bir kural bu değeri **ezer**. |
124
+ | `private` | `boolean` | Sayfa ziyaretçiye bağlı. Önbellek devre dışı kalır, `cache().html` deseni bunu **ezemez**, yanıt `private, no-store` ve `Vary: Cookie` ile ETag'siz gider. |
125
+
126
+ Oturuma bağlı her sayfa `private: true` almalı; önbellek anahtarında kimlik
127
+ olmadığı için bayrak olmadan bir kullanıcının HTML'i bir başkasına servis
128
+ edilir. Framework bu hatayı çalışma zamanında da yakalıyor (controller cookie
129
+ okuduğunda render önbelleğe yazılmaz), ama doğru yer bayrak. Ayrıntılar
130
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
131
+
132
+ ## `fragment()` — layout'suz parça
133
+
134
+ Bir bölgeyi tazeleyen uçlar için. Layout basılmaz, yanıt `private, no-store` ve
135
+ ETag'siz gider, HTML önbelleğine hiç uğramaz.
136
+
137
+ ```js
138
+ app.get(
139
+ "/_fragment/satirlar",
140
+ fragment(async ({ query }) => ({
141
+ view: "partials/rows",
142
+ data: { rows: getRows(Number(query.sayfa ?? 1)) },
143
+ })),
144
+ );
145
+ ```
146
+
147
+ Controller `{ view, data?, status? }` ya da doğrudan bir HTML string döner.
148
+ Hata durumunda tüm sayfa yerine küçük bir uyarı parçası döner
149
+ (`<div role="alert" data-fragment-error>`), çünkü takas edilen bölge bir hata
150
+ sayfasının tamamını içine almamalı.
151
+
152
+ `fragment()` POST için de kullanılabilir: form gönderiminin cevabı olarak
153
+ güncellenmiş parçayı döndürmenin yolu bu, ve şablonda `csrfField()`
154
+ çalışabilmesi için gereken istek bağlamını da kuruyor.
122
155
 
123
156
  ## `ctx` — controller bağlamı
124
157
 
@@ -200,18 +233,24 @@ fonksiyon `throw` eder, framework yakalar. Böylece veri katmanındaki bir
200
233
  fonksiyon, controller'a dönüş değeri taşımak zorunda kalmadan 404 üretebilir.
201
234
 
202
235
  ```js
203
- import { notFound, redirect, permanentRedirect } from "jskelet";
236
+ import { notFound, redirect, permanentRedirect, seeOther } from "jskelet";
204
237
 
205
238
  notFound(); // 404 → hooks.notFound() sayfası
206
- redirect("/yeni-adres"); // 307 (geçici)
207
- permanentRedirect("/yeni"); // 308 (kalıcı)
239
+ redirect("/yeni-adres"); // 307 (geçici, metodu korur)
240
+ permanentRedirect("/yeni"); // 308 (kalıcı, metodu korur)
241
+ seeOther("/panel"); // 303 (POST sonrası)
208
242
  ```
209
243
 
210
- Üçü de `never` döner (her zaman fırlatır). Ayrıntı:
244
+ Dördü de `never` döner (her zaman fırlatır). Ayrıntı:
211
245
 
212
246
  - `notFound()` → `NotFoundError` (`statusCode: 404`)
213
247
  - `redirect(location)` → `RedirectError` (`statusCode: 307`)
214
248
  - `permanentRedirect(location)` → `RedirectError` (`statusCode: 308`)
249
+ - `seeOther(location)` → `RedirectError` (`statusCode: 303`)
250
+
251
+ Bir POST handler'ında `redirect()` değil `seeOther()` kullanılır: 307 metodu
252
+ koruyor, yani tarayıcı hedefe yeniden POST ediyor. "Post/redirect/get" akışı —
253
+ geri tuşunun formu yeniden göndermediği akış — 303 gerektiriyor.
215
254
 
216
255
  Özel bir durum kodu gerekiyorsa sınıfı doğrudan kullanabilirsin:
217
256
 
@@ -424,7 +463,7 @@ Yakalanan değerler `destination` içindeki aynı adlı `:param`'lara yazılır.
424
463
  Parametre adı `[A-Za-z_][A-Za-z0-9_]*` kalıbına uymalıdır.
425
464
 
426
465
  `source` mutlaka `/` ile başlamalı; başlamazsa kural yok sayılır ve uyarı
427
- basılır (`[config] geçersiz source (\`/\` ile başlamalı): …`). Tanınmayan bir
466
+ basılır (``[config] invalid source (must start with `/`): …``). Tanınmayan bir
428
467
  sözdizimi sessizce literal kabul edilmez.
429
468
 
430
469
  Tam desen listesi ve config referansı: [07-yapilandirma.md](./07-yapilandirma.md).
@@ -194,8 +194,8 @@ Kurallar:
194
194
  önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
195
195
  bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
196
196
  - Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
197
- kazanır**: `[components] 'card' iki kez tanımlı: a.js ve b.js — ikincisi
198
- kazanıyor.`
197
+ kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
198
+ one wins.`
199
199
  - `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
200
200
  bir proje de çalışır.
201
201
 
@@ -205,14 +205,67 @@ start();
205
205
  - Bir element aynı island adıyla **iki kez bağlanmaz**; kayıt element bazında
206
206
  `WeakMap` içinde tutulur.
207
207
  - Kayıtlı olmayan bir ad için konsola uyarı basılır:
208
- `[island] kayıtlı değil: <ad>`.
208
+ `[island] not registered: <name>`.
209
209
  - Modül import'u ya da `mount()` hata verirse konsola hata basılır
210
- (`[island] <ad> yüklenemedi`) ve **sayfanın kalanı etkilenmez**.
210
+ (`[island] <name> failed to load`) ve **sayfanın kalanı etkilenmez**.
211
211
  - `mount()` başarıyla dönerse elemente `data-island-ready="true"` yazılır.
212
- - `mount()` bir fonksiyon döndürebilir; sözleşmede temizlik için ayrılmıştır.
213
- Framework bu fonksiyonu şu anda kendiliğinden çağırmaz — island'ın ömrünü
214
- kendisi yönetiyorsa (ör. bir fragment'ı değiştirirken) dönen fonksiyonu
215
- saklayıp kendisi çağırmalıdır.
212
+ - `mount()` bir temizlik fonksiyonu döndürebilir; framework onu saklar ve
213
+ `unmount()` çağrıldığında işletir (aşağıya bakın).
214
+
215
+ ### `unmount(root?)`
216
+
217
+ `root` altındaki island'ları söker: saklanan temizlik fonksiyonlarını çağırır,
218
+ `data-island-ready` işaretini kaldırır ve kaydı siler, böylece aynı düğüm
219
+ tekrar DOM'a girerse yeniden bağlanabilir. `root`'un kendisi de island olabilir.
220
+
221
+ DOM'un bir bölgesini değiştirirken çağrılması **zorunlu**:
222
+
223
+ ```js
224
+ import { hydrate, unmount } from "jskelet/client";
225
+
226
+ unmount(container);
227
+ container.innerHTML = html;
228
+ hydrate(container);
229
+ ```
230
+
231
+ Atlanması en kolay gözden kaçan sızıntı biçimini üretiyor. `innerHTML` ile
232
+ değiştirilen bir bölgenin island'ları DOM'dan çıkar, ama `document`/`window`
233
+ üzerine kurdukları dinleyiciler ve `setInterval`'ları yaşamaya devam eder;
234
+ birkaç takastan sonra aynı iş onlarca kez çalışır.
235
+
236
+ ```js
237
+ export function mount(element) {
238
+ const timer = setInterval(() => tick(element), 1000);
239
+ const onResize = () => layout(element);
240
+ window.addEventListener("resize", onResize);
241
+
242
+ return () => {
243
+ clearInterval(timer);
244
+ window.removeEventListener("resize", onResize);
245
+ };
246
+ }
247
+ ```
248
+
249
+ `swap()` ve form yardımcıları `unmount()`u kendileri çağırıyor; elle DOM
250
+ değiştirdiğiniz yerlerde siz çağırıyorsunuz.
251
+
252
+ ### `swap(target, url, options?)` ve `startSwapLinks(root?)`
253
+
254
+ Bir bölgeyi sunucudan gelen parçayla değiştirir: eski alt ağacı söker, içeriği
255
+ yazar, yeniden hidre eder ve odağı kaybolmuşsa geri getirir.
256
+
257
+ ```html
258
+ <a href="/_fragment/satirlar?sayfa=2" data-swap="#satirlar">Sonraki</a>
259
+ ```
260
+
261
+ Sunucu tarafı ve tüm seçenekler
262
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
263
+
264
+ ### `enhanceForm(form)` ve `startForms(root?)`
265
+
266
+ `data-enhance` taşıyan formları sayfa yenilemeden gönderir; JS kapalıyken
267
+ normal POST + yönlendirme akışı çalışmaya devam eder. Sözleşmenin tamamı
268
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
216
269
 
217
270
  ## Durum paylaşımı: `createStore`
218
271
 
package/docs/06-cache.md CHANGED
@@ -23,6 +23,29 @@ Sıra önemli: **istek içi cache en içte** olmalı ki aynı render'daki iki ç
23
23
  tek upstream isteğine düşsün; **upstream takibi HTML cache'in içinde** olmalı ki
24
24
  eksik veriyle üretilen çıktı önbelleğe yazılmasın.
25
25
 
26
+ ## Public ve kişiye özel ayrımı
27
+
28
+ Bu belgedeki her şey **herkese aynı gidebilen** HTML için geçerli. Cache
29
+ anahtarında kimlik yok (yalnızca yol + query), yani önbellekteki bir sayfa onu
30
+ ilk isteyen kişinin değil, o yolun cevabıdır.
31
+
32
+ Kullanıcıya bağlı bir sayfa bu yüzden ayrı bir yoldan geçer:
33
+
34
+ ```js
35
+ app.get("/panel", route(async ({ req }) => { … }, { private: true }));
36
+ ```
37
+
38
+ `private: true` üç şeyi birden yapar: önbellek devre dışı kalır, config'in
39
+ `cache.html` deseni bu kararı **ezemez** ve yanıt `private, no-store`,
40
+ `Vary: Cookie` ile, ETag'siz gider. Ayrıntılar ve oturum/CSRF tarafı
41
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de.
42
+
43
+ Bayrağı unutursanız framework sessiz kalmaz: controller `Cookie`,
44
+ `Authorization` ya da `req.session`/`req.user` okuduğu anda render işaretlenir
45
+ ve önbelleğe **yazılmaz**. Dev'de istek bir hatayla düşer, üretimde `no-store`
46
+ ile servis edilip loglanır. Koruma bir mazeret değil son savunma — doğru yer
47
+ `private: true`.
48
+
26
49
  ## `revalidate` — TTL nereden gelir
27
50
 
28
51
  Bir route'un TTL'i iki kaynaktan gelebilir ve **config kazanır**:
@@ -31,6 +54,9 @@ Bir route'un TTL'i iki kaynaktan gelebilir ve **config kazanır**:
31
54
  2. `jskelet.config.mjs` → `cache().html` içindeki eşleşen desen. Varsa
32
55
  route'unkini ezer.
33
56
 
57
+ Tek istisna `private: true`: desen eşleşse bile yok sayılır. Kilit tek yönlü,
58
+ çünkü ters yönde bir hata sessiz veri sızıntısı anlamına geliyor.
59
+
34
60
  ```js
35
61
  // jskelet.config.mjs
36
62
  export default {
@@ -53,8 +79,14 @@ mümkün kılar; route dosyalarını dolaşmak gerekmez.
53
79
  yapılmaz. Hiç `cache().html` kuralı yoksa doğrudan route'un değeri kullanılır.
54
80
 
55
81
  `revalidate` verilmemişse ya da 0 ise sayfa **hiç önbelleklenmez**: her istek
56
- render edilir ve yanıta `Cache-Control` yazılmaz (yalnızca
57
- `X-JSkelet-Cache: MISS`).
82
+ render edilir ve yanıt `Cache-Control: private, no-store` ile, ETag'siz gider.
83
+ `X-JSkelet-Cache` başlığı da yazılmaz — önbellek yolu hiç çalışmadı, `MISS`
84
+ demek yanıltıcı olurdu.
85
+
86
+ Dinamik bir sayfaya `no-store` yazılması bilinçli. Hiç direktif taşımayan bir
87
+ yanıtı HTTP "sezgisel olarak önbelleklenebilir" sayıyor; araya giren bir proxy
88
+ ya da tarayıcının geri tuşu, tek bir ziyaretçi için üretilmiş HTML'i
89
+ saklayabiliyordu.
58
90
 
59
91
  Önbellek ayrıca yalnızca `GET` istekleri için devreye girer.
60
92
 
@@ -92,7 +124,7 @@ Okuma davranışı:
92
124
 
93
125
  Stale penceresinde tazelemenin hatası isteği etkilemez: eski HTML pencere
94
126
  boyunca geçerli kalır ve hata yalnızca loglanır
95
- (`[html-cache] arka plan tazelemesi başarısız: …`).
127
+ (`[html-cache] background refresh failed: …`).
96
128
 
97
129
  Aynı anahtar için eşzamanlı tazelemeler tek bir çalışmada birleştirilir
98
130
  (`inflight` haritası): yüz eşzamanlı istek tek render'a düşer.
@@ -220,8 +252,8 @@ export async function apiGet(path) {
220
252
 
221
253
  | Durum | Sayılır | Sonuç |
222
254
  | --- | --- | --- |
223
- | `0` (ağ hatası), `408`, `425`, `429`, `>= 500` | **Geçici** | Sayfa önbelleğe yazılmaz, uyarı: `[render] <yol> eksik veriyle üretildi, önbelleğe alınmıyor (…)` |
224
- | Diğerleri (`400`, `403`, `404`, …) | **Kalıcı** | Yalnızca uyarı: `[render] <yol> eksik veriyle üretildi, upstream kalıcı hata veriyor (…)`. Önbellek engellenmez. |
255
+ | `0` (ağ hatası), `408`, `425`, `429`, `>= 500` | **Geçici** | Sayfa önbelleğe yazılmaz, uyarı: `[render] <path> was produced with missing data, not caching it (…)` |
256
+ | Diğerleri (`400`, `403`, `404`, …) | **Kalıcı** | Yalnızca uyarı: `[render] <path> was produced with missing data, upstream is failing permanently (…)`. Önbellek engellenmez. |
225
257
 
226
258
  Kalıcı hataların önbelleği engellememesi bilinçli: deterministik cevaplar tekrar
227
259
  denemekle düzelmez. Onlar yüzünden önbelleği kapatmak sayfayı her ziyarette
@@ -319,7 +351,7 @@ Kurallar:
319
351
  anda çekerken API'yi zorluyor. Tekrar turu bu sayfaların önbelleğe girmesini
320
352
  sağlıyor; aksi hâlde ziyaretçi soğuk render'ı öder.
321
353
  4. Özet loglanır:
322
- `[prewarm] 128/130 sayfa ısıtıldı, 2 hata, 5 sayfa tekrar turunda kurtarıldı (12.4s)`
354
+ `[prewarm] warmed 128/130 pages, 2 failed, 5 recovered on the retry pass (12.4s)`
323
355
 
324
356
  İstekler `user-agent: jskelet-prewarm` (`brand.prewarmUserAgent`) ve
325
357
  `accept-encoding: br, gzip` başlıklarıyla gider; ikincisi sıkıştırılmış gövdenin
@@ -392,7 +424,7 @@ turunun gerçekten `MISS` → önbellek doldurup doldurmadığını buradan gör
392
424
  `cache().html` içindeki desen 0 saniye veriyor. Veya sayfa `status: 200`
393
425
  dışında bir kod dönüyor.
394
426
  - **Sayfa `MISS` dönüyor ama upstream sağlam.** Geçici bir upstream hatası
395
- bildirilmiş olabilir; logda `eksik veriyle üretildi, önbelleğe alınmıyor`
427
+ bildirilmiş olabilir; logda `was produced with missing data, not caching it`
396
428
  satırını arayın.
397
429
  - **Sürekli eski veri.** `revalidate` çok yüksek; unutmayın ki gerçek gecikme en
398
430
  fazla `revalidate` + bir tazeleme turudur.
@@ -25,7 +25,7 @@ düz değer** olabilir; fonksiyon olmaları hâlinde `async` olabilirler ve `thi
25
25
  config nesnesine bağlıdır. Bir bölüm hata verirse yalnızca o bölüm yok sayılır.
26
26
 
27
27
  Config başarıyla yüklendiğinde bir özet basılır:
28
- `[config] jskelet.config.mjs yüklendi — 3 header, 2 redirect, 1 cache kuralı`
28
+ `[config] jskelet.config.mjs loaded — 3 headers, 2 redirects, 1 cache rule`
29
29
 
30
30
  ## Tam örnek
31
31
 
@@ -62,6 +62,20 @@ export default {
62
62
  devGateBypass: ["/api/healthcheck", "/robots.txt"],
63
63
  preconnect: ["https://cdn.ornek.com"],
64
64
 
65
+ security: {
66
+ trustProxy: true,
67
+ cookieSecret: process.env.JSKELET_SECRET,
68
+ csrf: {
69
+ enabled: true,
70
+ token: false,
71
+ allowedOrigins: [],
72
+ exclude: ["/webhook/:path*"],
73
+ cookieName: "csrf_token",
74
+ fieldName: "_csrf",
75
+ headerName: "x-csrf-token",
76
+ },
77
+ },
78
+
65
79
  navigation: {
66
80
  prefetch: "moderate",
67
81
  prerender: "conservative",
@@ -239,6 +253,37 @@ bir yapılandırmadır.
239
253
  preconnect: ["https://cdn.ornek.com", "https://api.ornek.com"]
240
254
  ```
241
255
 
256
+ ## `security`
257
+
258
+ **Tip:** `object` — **Varsayılan:**
259
+ `{ trustProxy: true, cookieSecret: null, csrf: { enabled: true, token: false, … } }`
260
+
261
+ Kişiye özel sayfaların tamamı ve gerekçeleri
262
+ [12-panel-ve-oturum.md](./12-panel-ve-oturum.md)'de; burada alanların referansı
263
+ var.
264
+
265
+ | Alan | Tip | Varsayılan | Anlamı |
266
+ | --- | --- | --- | --- |
267
+ | `trustProxy` | `boolean` | `true` | Express'in `trust proxy` ayarı. Ters proxy arkasında doğru protokol ve istemci IP'si için gerekli. |
268
+ | `cookieSecret` | `string \| null` | `null` | İmzalı cookie sırrı. Verilmezse `JSKELET_SECRET` okunur. |
269
+ | `csrf.enabled` | `boolean` | `true` | Origin/`Sec-Fetch-Site` kontrolü. |
270
+ | `csrf.token` | `boolean` | `false` | Çift gönderim token'ı katmanı. |
271
+ | `csrf.allowedOrigins` | `string[]` | `[]` | Kendi host'umuzun yanında kabul edilen origin'ler. |
272
+ | `csrf.exclude` | `string[]` | `[]` | Kontrolden muaf yollar; `source` desen sözdizimi. |
273
+ | `csrf.cookieName` | `string` | `"csrf_token"` | Token cookie'sinin adı. |
274
+ | `csrf.fieldName` | `string` | `"_csrf"` | `csrfField()`in bastığı alan adı. |
275
+ | `csrf.headerName` | `string` | `"x-csrf-token"` | Token'ın kabul edildiği başlık. |
276
+
277
+ `trustProxy` doğrudan internete açık bir sunucuda **kapatılmalı**: açıkken
278
+ istemci kendi `X-Forwarded-For` başlığını uydurabilir ve rate limit ile audit
279
+ log yanlış adresi görür.
280
+
281
+ CSRF kontrolü yalnızca çapraz site olduğu **belli** olan istekleri reddeder —
282
+ `Origin` uyuşmuyorsa ya da `Sec-Fetch-Site: cross-site` geldiyse. İkisi de yoksa
283
+ istek geçer, çünkü tarayıcılar çapraz origin bir POST'ta `Origin`'i her zaman
284
+ gönderirken webhook'lar hiç göndermez. Yine de tarayıcıdan gelmeyen uçları
285
+ `csrf.exclude` listesine yazmak niyeti okunur kılıyor.
286
+
242
287
  ## `navigation`
243
288
 
244
289
  **Tip:** `object` — **Varsayılan:**
@@ -521,6 +566,10 @@ html: {
521
566
  }
522
567
  ```
523
568
 
569
+ Tek istisna `route(fn, { private: true })`: bu route'ta desen eşleşse bile yok
570
+ sayılır. Kilidin tek yönlü olması bilinçli — ters yönde bir hata, bir
571
+ kullanıcının HTML'inin bir başkasına servis edilmesi anlamına geliyor.
572
+
524
573
  ### `cache().prewarm`
525
574
 
526
575
  | Alan | Tip | Varsayılan | Anlamı |
@@ -622,6 +671,7 @@ basılmaz.
622
671
  | `NODE_ENV` | her yer | `production` (start/build), `development` (dev) | Dev overlay, EJS cache, manifest yeniden okuma, route hata davranışı ve prewarm varsayılanlarını belirler. `jskelet dev` bunu kendisi ayarlar — `cross-env` gerekmez. |
623
672
  | `PORT` | `startServer` | `3000` | Dinlenecek port |
624
673
  | `HOST` | `startServer` | `0.0.0.0` | Bağlanılacak arayüz |
674
+ | `JSKELET_SECRET` | `jskelet/cookies` | — | İmzalı cookie sırrı. `security.cookieSecret` verilmediğinde buradan okunur; ikisi de yoksa imzalı cookie API'si hata verir. [12](./12-panel-ve-oturum.md) |
625
675
  | `DEV_TOKEN` | `devGate`, `prewarm` | — | Ayarlıysa token taşımayan her isteğe 404 döner. Isıtma token'ı çerez olarak taşır. [09](./09-dev-araclari.md) |
626
676
  | `PREWARM` | `startPrewarm` | — | `0` ısıtmayı kapatır; `1` config'teki `enabled: false`'u ezip açar |
627
677
  | `PREWARM_MAX` | `prewarm` | `400` | En fazla kaç yol ısıtılır |
package/docs/08-build.md CHANGED
@@ -69,7 +69,7 @@ dosya adlarını okunur tutuyor. Hash'li olmaları sayesinde bu dosyalara
69
69
  Build çalışmadıysa uygulama yine ayağa kalkar: `hasAsset()` false olur, layout
70
70
  stylesheet ve script etiketlerini hiç basmaz. `jskelet build` unutulduğunda hata
71
71
  yerine stilsiz ama çalışan bir sayfa görürsünüz. Manifest hiç yoksa bir kez uyarı
72
- basılır: `[assets] manifest yok — 'jskelet build' çalıştırın.`
72
+ basılır: ``[assets] no manifest — run `jskelet build`.``
73
73
 
74
74
  Manifest **dev'de her istekte** yeniden okunur (watch build hash'leri
75
75
  değiştirir), prod'da bir kez.
@@ -244,8 +244,8 @@ Son satır için iki güvenlik ağı var: ad taşıyan yapılandırma alanları
244
244
  sembolleri okuyup eksik olan için tek seferlik uyarı basar:
245
245
 
246
246
  ```
247
- [icon] sprite'ta yok: x-logo-regular — adı sabit yazın ya da
248
- build/tasks/icons.mjs taramasına ekleyin.
247
+ [icon] missing from sprite: x-logo-regular — write the name as a literal or add
248
+ it to the build/tasks/icons.mjs scan.
249
249
  ```
250
250
 
251
251
  Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
@@ -352,7 +352,7 @@ karşılaşmaması.
352
352
  kontrol edin.
353
353
  - **Bazı Tailwind sınıfları çalışmıyor.** `@source` direktifi eksik olan bir
354
354
  dizinde yazılmışlar.
355
- - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] sprite'ta yok`
355
+ - **İkon boş görünüyor.** Sprite'ta o sembol yok; dev'de `[icon] missing from sprite`
356
356
  uyarısına bakın.
357
357
  - **Island'lar hiç açılmıyor.** `main.js` manifest'te yok (entry dizini boş ya
358
358
  da build atlanmış) veya bir build hatası var.
@@ -161,6 +161,11 @@ Gösterdikleri:
161
161
  - **Prewarm:** ısıtma turunun ilerlemesi; panelden elle tetiklenebilir, tek tek
162
162
  yollar tekrar denenebilir.
163
163
  - **Süreç:** pid, Node sürümü, uptime, RSS ve heap kullanımı.
164
+ - **Sürüm:** kurulu JSkelet sürümü ve npm'deki `latest` ile karşılaştırması.
165
+ Yeni bir sürüm varsa **Server** sekmesinde `update` rozeti ve yükseltme
166
+ komutunu kopyalayan bir satır çıkar. Yoklama açılıştan 1,5 saniye sonra bir
167
+ kez yapılır, sonucu 6 saat boyunca `os.tmpdir()` içinde saklanır ve ağ yoksa
168
+ sessizce atlanır. `JSKELET_VERSION_CHECK=0` ile tamamen kapatılır.
164
169
 
165
170
  Isıtma istekleri (`user-agent: jskelet-prewarm`) hem terminalden hem istek
166
171
  listesinden filtrelenir: yüzlerce istek görünümü doldurmasın. İlerleme baloncuğun