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,257 +1,257 @@
1
- /**
2
- * Cookie okuma/yazma ve HMAC ile imzalama.
3
- *
4
- * Neden framework'te: kişiye özel her sayfa bir oturum cookie'sine dayanıyor
5
- * ve bunu elle yazan her proje aynı üç hatayı tekrar ediyor — `HttpOnly`
6
- * unutmak, imzasız değere güvenmek, karşılaştırmayı `===` ile yapmak.
7
- * Burada varsayılanlar güvenli tarafta ve imza doğrulaması sabit zamanlı.
8
- *
9
- * Framework **kimlik sağlamaz**: oturumun içinde ne olduğu, ne kadar
10
- * yaşadığı ve kimin verdiği uygulamanın kararı. Buradaki yüzey yalnızca
11
- * "bu değeri ben yazdım, kurcalanmamış" garantisini veriyor.
12
- *
13
- * Bağımlılık eklenmez; `node:crypto` yeterli.
14
- */
15
- import crypto from "node:crypto";
16
- import process from "node:process";
17
- import { getConfig } from "../config/index.js";
18
- import { markTainted } from "./request-context.js";
19
-
20
- /** Ayrıştırılmış cookie'ler istek başına bir kez hesaplanır. */
21
- const PARSED = Symbol("jskelet.cookies");
22
-
23
- /**
24
- * @param {string} value
25
- * @returns {string}
26
- */
27
- function base64url(value) {
28
- return Buffer.from(value, "utf8").toString("base64url");
29
- }
30
-
31
- /**
32
- * @param {string} value
33
- * @returns {string | null}
34
- */
35
- function fromBase64url(value) {
36
- try {
37
- return Buffer.from(value, "base64url").toString("utf8");
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * İmza sırrı. `security.cookieSecret` ya da `JSKELET_SECRET`.
45
- *
46
- * Yokluğunda imzasız cookie yazmak en kötü sonuç olurdu: uygulama kendini
47
- * güvende sanar, değer kurcalanabilir. Bu yüzden imzalı API sır olmadan
48
- * hata verir — config hatasının siteyi düşürmemesi kuralı burada geçmez,
49
- * çünkü sessiz alternatif bir güvenlik açığı.
50
- *
51
- * @returns {string}
52
- */
53
- function getSecret() {
54
- /** @type {string | null} */
55
- let configured = null;
56
-
57
- try {
58
- configured = getConfig().security.cookieSecret;
59
- } catch {
60
- // Config yüklenmemiş olabilir (script, test); env yine de geçerli.
61
- configured = null;
62
- }
63
-
64
- const secret = configured ?? process.env.JSKELET_SECRET ?? null;
65
-
66
- if (!secret) {
67
- throw new Error(
68
- "[cookies] a secret is required for signed cookies. Set " +
69
- "`security.cookieSecret` in `jskelet.config.mjs` or the JSKELET_SECRET environment variable.",
70
- );
71
- }
72
-
73
- return secret;
74
- }
75
-
76
- /**
77
- * @param {string} value
78
- * @returns {string}
79
- */
80
- function sign(value) {
81
- return crypto.createHmac("sha256", getSecret()).update(value).digest("base64url");
82
- }
83
-
84
- /**
85
- * Sabit zamanlı karşılaştırma: imza doğrulamasında erken çıkış, saldırganın
86
- * baytları tek tek tahmin etmesine kapı aralar.
87
- *
88
- * @param {string} a
89
- * @param {string} b
90
- * @returns {boolean}
91
- */
92
- export function safeEqual(a, b) {
93
- const left = Buffer.from(String(a));
94
- const right = Buffer.from(String(b));
95
- if (left.length !== right.length) return false;
96
- return crypto.timingSafeEqual(left, right);
97
- }
98
-
99
- /**
100
- * `Cookie` başlığını ayrıştırır.
101
- *
102
- * Okuma çıktının kullanıcıya bağlı olduğunu bildirir: bu sayfa artık public
103
- * HTML cache'ine yazılamaz.
104
- *
105
- * @param {import('http').IncomingMessage} req
106
- * @returns {Record<string, string>}
107
- */
108
- export function parseCookies(req) {
109
- markTainted("parseCookies(req)");
110
-
111
- const cached = /** @type {any} */ (req)[PARSED];
112
- if (cached) return cached;
113
-
114
- /** @type {Record<string, string>} */
115
- const out = {};
116
- const header = req.headers?.cookie;
117
-
118
- if (header) {
119
- for (const part of header.split(";")) {
120
- const index = part.indexOf("=");
121
- if (index === -1) continue;
122
-
123
- const name = part.slice(0, index).trim();
124
- if (!name) continue;
125
-
126
- try {
127
- out[name] = decodeURIComponent(part.slice(index + 1).trim());
128
- } catch {
129
- // Bozuk yüzde kodlaması tüm başlığı çöpe atmamalı.
130
- out[name] = part.slice(index + 1).trim();
131
- }
132
- }
133
- }
134
-
135
- /** @type {any} */ (req)[PARSED] = out;
136
- return out;
137
- }
138
-
139
- /**
140
- * @typedef {object} CookieOptions
141
- * @property {string} [path] Varsayılan `/`.
142
- * @property {string} [domain]
143
- * @property {number} [maxAge] Saniye.
144
- * @property {Date} [expires]
145
- * @property {boolean} [httpOnly] Varsayılan `true`.
146
- * @property {boolean} [secure] Varsayılan: development dışında `true`.
147
- * @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
148
- */
149
-
150
- /**
151
- * Varsayılanlar bilinçli olarak kısıtlayıcı: `HttpOnly` ile JS okuyamaz,
152
- * `SameSite=Lax` ile çapraz site POST'larında gönderilmez (CSRF'nin büyük
153
- * kısmını kapatan tek satır), `Secure` üretimde açık.
154
- *
155
- * @param {string} name
156
- * @param {string} value
157
- * @param {CookieOptions} [options]
158
- * @returns {string}
159
- */
160
- export function serializeCookie(name, value, options = {}) {
161
- const parts = [`${name}=${encodeURIComponent(value)}`];
162
-
163
- parts.push(`Path=${options.path ?? "/"}`);
164
- if (options.domain) parts.push(`Domain=${options.domain}`);
165
- if (options.maxAge !== undefined) parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
166
- if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
167
- if (options.httpOnly !== false) parts.push("HttpOnly");
168
- if (options.secure ?? process.env.NODE_ENV !== "development") parts.push("Secure");
169
- parts.push(`SameSite=${options.sameSite ?? "Lax"}`);
170
-
171
- return parts.join("; ");
172
- }
173
-
174
- /**
175
- * @param {import('http').ServerResponse} res
176
- * @param {string} name
177
- * @param {string} value
178
- * @param {CookieOptions} [options]
179
- */
180
- export function setCookie(res, name, value, options = {}) {
181
- const existing = res.getHeader("Set-Cookie");
182
- const serialized = serializeCookie(name, value, options);
183
-
184
- /** @type {string[]} */
185
- const all = existing
186
- ? Array.isArray(existing)
187
- ? [...existing.map(String)]
188
- : [String(existing)]
189
- : [];
190
-
191
- all.push(serialized);
192
- res.setHeader("Set-Cookie", all);
193
- }
194
-
195
- /**
196
- * @param {import('http').ServerResponse} res
197
- * @param {string} name
198
- * @param {CookieOptions} [options]
199
- */
200
- export function clearCookie(res, name, options = {}) {
201
- setCookie(res, name, "", { ...options, maxAge: 0, expires: new Date(0) });
202
- }
203
-
204
- /**
205
- * İmzalı cookie yazar. Değer okunabilir kalır (şifreleme değil, imza);
206
- * gizli kalması gereken veriyi cookie'ye koymayın, kimliğini koyun.
207
- *
208
- * @param {import('http').ServerResponse} res
209
- * @param {string} name
210
- * @param {string} value
211
- * @param {CookieOptions} [options]
212
- */
213
- export function setSignedCookie(res, name, value, options = {}) {
214
- const encoded = base64url(value);
215
- setCookie(res, name, `${encoded}.${sign(encoded)}`, options);
216
- }
217
-
218
- /**
219
- * İmzalı cookie okur. İmza uymuyorsa `null` — bozuk imza, yok sayılmalı,
220
- * "belki geçerlidir" diye kullanılmamalı.
221
- *
222
- * @param {import('http').IncomingMessage} req
223
- * @param {string} name
224
- * @returns {string | null}
225
- */
226
- export function getSignedCookie(req, name) {
227
- const raw = parseCookies(req)[name];
228
- if (!raw) return null;
229
-
230
- const index = raw.lastIndexOf(".");
231
- if (index <= 0) return null;
232
-
233
- const encoded = raw.slice(0, index);
234
- const signature = raw.slice(index + 1);
235
-
236
- let expected;
237
- try {
238
- expected = sign(encoded);
239
- } catch {
240
- // Sır yoksa imzalı okuma sessizce başarısız olur: sunucu ayakta kalır
241
- // ama hiçbir oturum geçerli sayılmaz.
242
- return null;
243
- }
244
-
245
- if (!safeEqual(signature, expected)) return null;
246
- return fromBase64url(encoded);
247
- }
248
-
249
- /**
250
- * Kriptografik rastgele token. CSRF token'ı ve oturum kimliği için.
251
- *
252
- * @param {number} [bytes]
253
- * @returns {string}
254
- */
255
- export function randomToken(bytes = 32) {
256
- return crypto.randomBytes(bytes).toString("base64url");
257
- }
1
+ /**
2
+ * Cookie okuma/yazma ve HMAC ile imzalama.
3
+ *
4
+ * Neden framework'te: kişiye özel her sayfa bir oturum cookie'sine dayanıyor
5
+ * ve bunu elle yazan her proje aynı üç hatayı tekrar ediyor — `HttpOnly`
6
+ * unutmak, imzasız değere güvenmek, karşılaştırmayı `===` ile yapmak.
7
+ * Burada varsayılanlar güvenli tarafta ve imza doğrulaması sabit zamanlı.
8
+ *
9
+ * Framework **kimlik sağlamaz**: oturumun içinde ne olduğu, ne kadar
10
+ * yaşadığı ve kimin verdiği uygulamanın kararı. Buradaki yüzey yalnızca
11
+ * "bu değeri ben yazdım, kurcalanmamış" garantisini veriyor.
12
+ *
13
+ * Bağımlılık eklenmez; `node:crypto` yeterli.
14
+ */
15
+ import crypto from "node:crypto";
16
+ import process from "node:process";
17
+ import { getConfig } from "../config/index.js";
18
+ import { markTainted } from "./request-context.js";
19
+
20
+ /** Ayrıştırılmış cookie'ler istek başına bir kez hesaplanır. */
21
+ const PARSED = Symbol("jskelet.cookies");
22
+
23
+ /**
24
+ * @param {string} value
25
+ * @returns {string}
26
+ */
27
+ function base64url(value) {
28
+ return Buffer.from(value, "utf8").toString("base64url");
29
+ }
30
+
31
+ /**
32
+ * @param {string} value
33
+ * @returns {string | null}
34
+ */
35
+ function fromBase64url(value) {
36
+ try {
37
+ return Buffer.from(value, "base64url").toString("utf8");
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * İmza sırrı. `security.cookieSecret` ya da `JSKELET_SECRET`.
45
+ *
46
+ * Yokluğunda imzasız cookie yazmak en kötü sonuç olurdu: uygulama kendini
47
+ * güvende sanar, değer kurcalanabilir. Bu yüzden imzalı API sır olmadan
48
+ * hata verir — config hatasının siteyi düşürmemesi kuralı burada geçmez,
49
+ * çünkü sessiz alternatif bir güvenlik açığı.
50
+ *
51
+ * @returns {string}
52
+ */
53
+ function getSecret() {
54
+ /** @type {string | null} */
55
+ let configured = null;
56
+
57
+ try {
58
+ configured = getConfig().security.cookieSecret;
59
+ } catch {
60
+ // Config yüklenmemiş olabilir (script, test); env yine de geçerli.
61
+ configured = null;
62
+ }
63
+
64
+ const secret = configured ?? process.env.JSKELET_SECRET ?? null;
65
+
66
+ if (!secret) {
67
+ throw new Error(
68
+ "[cookies] a secret is required for signed cookies. Set " +
69
+ "`security.cookieSecret` in `jskelet.config.mjs` or the JSKELET_SECRET environment variable.",
70
+ );
71
+ }
72
+
73
+ return secret;
74
+ }
75
+
76
+ /**
77
+ * @param {string} value
78
+ * @returns {string}
79
+ */
80
+ function sign(value) {
81
+ return crypto.createHmac("sha256", getSecret()).update(value).digest("base64url");
82
+ }
83
+
84
+ /**
85
+ * Sabit zamanlı karşılaştırma: imza doğrulamasında erken çıkış, saldırganın
86
+ * baytları tek tek tahmin etmesine kapı aralar.
87
+ *
88
+ * @param {string} a
89
+ * @param {string} b
90
+ * @returns {boolean}
91
+ */
92
+ export function safeEqual(a, b) {
93
+ const left = Buffer.from(String(a));
94
+ const right = Buffer.from(String(b));
95
+ if (left.length !== right.length) return false;
96
+ return crypto.timingSafeEqual(left, right);
97
+ }
98
+
99
+ /**
100
+ * `Cookie` başlığını ayrıştırır.
101
+ *
102
+ * Okuma çıktının kullanıcıya bağlı olduğunu bildirir: bu sayfa artık public
103
+ * HTML cache'ine yazılamaz.
104
+ *
105
+ * @param {import('http').IncomingMessage} req
106
+ * @returns {Record<string, string>}
107
+ */
108
+ export function parseCookies(req) {
109
+ markTainted("parseCookies(req)");
110
+
111
+ const cached = /** @type {any} */ (req)[PARSED];
112
+ if (cached) return cached;
113
+
114
+ /** @type {Record<string, string>} */
115
+ const out = {};
116
+ const header = req.headers?.cookie;
117
+
118
+ if (header) {
119
+ for (const part of header.split(";")) {
120
+ const index = part.indexOf("=");
121
+ if (index === -1) continue;
122
+
123
+ const name = part.slice(0, index).trim();
124
+ if (!name) continue;
125
+
126
+ try {
127
+ out[name] = decodeURIComponent(part.slice(index + 1).trim());
128
+ } catch {
129
+ // Bozuk yüzde kodlaması tüm başlığı çöpe atmamalı.
130
+ out[name] = part.slice(index + 1).trim();
131
+ }
132
+ }
133
+ }
134
+
135
+ /** @type {any} */ (req)[PARSED] = out;
136
+ return out;
137
+ }
138
+
139
+ /**
140
+ * @typedef {object} CookieOptions
141
+ * @property {string} [path] Varsayılan `/`.
142
+ * @property {string} [domain]
143
+ * @property {number} [maxAge] Saniye.
144
+ * @property {Date} [expires]
145
+ * @property {boolean} [httpOnly] Varsayılan `true`.
146
+ * @property {boolean} [secure] Varsayılan: development dışında `true`.
147
+ * @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
148
+ */
149
+
150
+ /**
151
+ * Varsayılanlar bilinçli olarak kısıtlayıcı: `HttpOnly` ile JS okuyamaz,
152
+ * `SameSite=Lax` ile çapraz site POST'larında gönderilmez (CSRF'nin büyük
153
+ * kısmını kapatan tek satır), `Secure` üretimde açık.
154
+ *
155
+ * @param {string} name
156
+ * @param {string} value
157
+ * @param {CookieOptions} [options]
158
+ * @returns {string}
159
+ */
160
+ export function serializeCookie(name, value, options = {}) {
161
+ const parts = [`${name}=${encodeURIComponent(value)}`];
162
+
163
+ parts.push(`Path=${options.path ?? "/"}`);
164
+ if (options.domain) parts.push(`Domain=${options.domain}`);
165
+ if (options.maxAge !== undefined) parts.push(`Max-Age=${Math.floor(options.maxAge)}`);
166
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
167
+ if (options.httpOnly !== false) parts.push("HttpOnly");
168
+ if (options.secure ?? process.env.NODE_ENV !== "development") parts.push("Secure");
169
+ parts.push(`SameSite=${options.sameSite ?? "Lax"}`);
170
+
171
+ return parts.join("; ");
172
+ }
173
+
174
+ /**
175
+ * @param {import('http').ServerResponse} res
176
+ * @param {string} name
177
+ * @param {string} value
178
+ * @param {CookieOptions} [options]
179
+ */
180
+ export function setCookie(res, name, value, options = {}) {
181
+ const existing = res.getHeader("Set-Cookie");
182
+ const serialized = serializeCookie(name, value, options);
183
+
184
+ /** @type {string[]} */
185
+ const all = existing
186
+ ? Array.isArray(existing)
187
+ ? [...existing.map(String)]
188
+ : [String(existing)]
189
+ : [];
190
+
191
+ all.push(serialized);
192
+ res.setHeader("Set-Cookie", all);
193
+ }
194
+
195
+ /**
196
+ * @param {import('http').ServerResponse} res
197
+ * @param {string} name
198
+ * @param {CookieOptions} [options]
199
+ */
200
+ export function clearCookie(res, name, options = {}) {
201
+ setCookie(res, name, "", { ...options, maxAge: 0, expires: new Date(0) });
202
+ }
203
+
204
+ /**
205
+ * İmzalı cookie yazar. Değer okunabilir kalır (şifreleme değil, imza);
206
+ * gizli kalması gereken veriyi cookie'ye koymayın, kimliğini koyun.
207
+ *
208
+ * @param {import('http').ServerResponse} res
209
+ * @param {string} name
210
+ * @param {string} value
211
+ * @param {CookieOptions} [options]
212
+ */
213
+ export function setSignedCookie(res, name, value, options = {}) {
214
+ const encoded = base64url(value);
215
+ setCookie(res, name, `${encoded}.${sign(encoded)}`, options);
216
+ }
217
+
218
+ /**
219
+ * İmzalı cookie okur. İmza uymuyorsa `null` — bozuk imza, yok sayılmalı,
220
+ * "belki geçerlidir" diye kullanılmamalı.
221
+ *
222
+ * @param {import('http').IncomingMessage} req
223
+ * @param {string} name
224
+ * @returns {string | null}
225
+ */
226
+ export function getSignedCookie(req, name) {
227
+ const raw = parseCookies(req)[name];
228
+ if (!raw) return null;
229
+
230
+ const index = raw.lastIndexOf(".");
231
+ if (index <= 0) return null;
232
+
233
+ const encoded = raw.slice(0, index);
234
+ const signature = raw.slice(index + 1);
235
+
236
+ let expected;
237
+ try {
238
+ expected = sign(encoded);
239
+ } catch {
240
+ // Sır yoksa imzalı okuma sessizce başarısız olur: sunucu ayakta kalır
241
+ // ama hiçbir oturum geçerli sayılmaz.
242
+ return null;
243
+ }
244
+
245
+ if (!safeEqual(signature, expected)) return null;
246
+ return fromBase64url(encoded);
247
+ }
248
+
249
+ /**
250
+ * Kriptografik rastgele token. CSRF token'ı ve oturum kimliği için.
251
+ *
252
+ * @param {number} [bytes]
253
+ * @returns {string}
254
+ */
255
+ export function randomToken(bytes = 32) {
256
+ return crypto.randomBytes(bytes).toString("base64url");
257
+ }
@@ -1,46 +1,46 @@
1
- /**
2
- * React `cache()` fonksiyonunun karşılığı: aynı istek içinde aynı argümanlarla
3
- * yapılan çağrılar tek kez çalışır. İstek bağlamı yoksa (script, client)
4
- * memoizasyon devre dışı kalır ve fonksiyon doğrudan çağrılır.
5
- */
6
- import { AsyncLocalStorage } from "node:async_hooks";
7
-
8
- /** @type {AsyncLocalStorage<Map<unknown, Map<string, unknown>>>} */
9
- const storage = new AsyncLocalStorage();
10
-
11
- /**
12
- * İsteği bir memo bağlamı içinde çalıştırır.
13
- * @param {() => T} run
14
- * @returns {T}
15
- * @template T
16
- */
17
- export function withRequestCache(run) {
18
- return storage.run(new Map(), run);
19
- }
20
-
21
- /**
22
- * @param {F} fn
23
- * @returns {F}
24
- * @template {(...args: any[]) => any} F
25
- */
26
- export function cache(fn) {
27
- const wrapped = (...args) => {
28
- const contextStore = storage.getStore();
29
- if (!contextStore) return fn(...args);
30
-
31
- let perFn = contextStore.get(fn);
32
- if (!perFn) {
33
- perFn = new Map();
34
- contextStore.set(fn, perFn);
35
- }
36
-
37
- const key = args.length === 0 ? "" : JSON.stringify(args);
38
- if (perFn.has(key)) return perFn.get(key);
39
-
40
- const result = fn(...args);
41
- perFn.set(key, result);
42
- return result;
43
- };
44
-
45
- return /** @type {F} */ (wrapped);
46
- }
1
+ /**
2
+ * React `cache()` fonksiyonunun karşılığı: aynı istek içinde aynı argümanlarla
3
+ * yapılan çağrılar tek kez çalışır. İstek bağlamı yoksa (script, client)
4
+ * memoizasyon devre dışı kalır ve fonksiyon doğrudan çağrılır.
5
+ */
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+
8
+ /** @type {AsyncLocalStorage<Map<unknown, Map<string, unknown>>>} */
9
+ const storage = new AsyncLocalStorage();
10
+
11
+ /**
12
+ * İsteği bir memo bağlamı içinde çalıştırır.
13
+ * @param {() => T} run
14
+ * @returns {T}
15
+ * @template T
16
+ */
17
+ export function withRequestCache(run) {
18
+ return storage.run(new Map(), run);
19
+ }
20
+
21
+ /**
22
+ * @param {F} fn
23
+ * @returns {F}
24
+ * @template {(...args: any[]) => any} F
25
+ */
26
+ export function cache(fn) {
27
+ const wrapped = (...args) => {
28
+ const contextStore = storage.getStore();
29
+ if (!contextStore) return fn(...args);
30
+
31
+ let perFn = contextStore.get(fn);
32
+ if (!perFn) {
33
+ perFn = new Map();
34
+ contextStore.set(fn, perFn);
35
+ }
36
+
37
+ const key = args.length === 0 ? "" : JSON.stringify(args);
38
+ if (perFn.has(key)) return perFn.get(key);
39
+
40
+ const result = fn(...args);
41
+ perFn.set(key, result);
42
+ return result;
43
+ };
44
+
45
+ return /** @type {F} */ (wrapped);
46
+ }