jskelet 0.5.3 → 0.5.5

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.
@@ -1,12 +1,17 @@
1
1
  /**
2
- * Phosphor SVG sprite üretimi.
2
+ * SVG ikon sprite üretimi.
3
3
  *
4
- * `@phosphor-icons/core` içindeki tek tek SVG'lerden, **yalnızca kaynakta
5
- * kullanılan** ikonlar için `<symbol>` seti üretir. Tüm seti göndermek 1500+
6
- * ikon, yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak 10-30
7
- * sembolde tutuyor.
4
+ * Kaynak XOR seçilir: uygulama kökünde `icons/` (veya `icons.dir`) dizini
5
+ * varsa yalnızca oradaki düz SVG'ler; yoksa `@phosphor-icons/core`. İkisi
6
+ * birleştirilmez — boş bir `icons/` dizini Phosphor'a düşmez.
7
+ *
8
+ * Yalnızca kaynakta kullanılan ikonlar `<symbol>` olur. Tüm seti göndermek
9
+ * 1500+ ikon, yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak
10
+ * 10-30 sembolde tutuyor. Çıktı `public/assets/` altında hash'li
11
+ * `sprite.svg` olduğu için mevcut precompress kapsamına girer.
8
12
  *
9
13
  * Sembol id'si: `<kebab-name>-<weight>` (örn. `arrow-right-bold`).
14
+ * Yerel dosya adı: `house.svg` → `house:regular`, `house-bold.svg` → `house:bold`.
10
15
  *
11
16
  * Tarama statik metin üzerinden yapıldığı için adı çalışma anında hesaplanan
12
17
  * bir `icon()` çağrısı sprite'a girmez. Bu yüzden ad taşıyan yapılandırma
@@ -41,6 +46,8 @@ const QUOTED_NAME = /["']([A-Z][A-Za-z0-9]*)["']/g;
41
46
 
42
47
  const WEIGHTS = new Set(["thin", "light", "regular", "bold", "fill", "duotone"]);
43
48
 
49
+ const DEFAULT_VIEWBOX = "0 0 256 256";
50
+
44
51
  /**
45
52
  * `ArrowRightIcon` → `arrow-right`
46
53
  * @param {string} name
@@ -54,6 +61,50 @@ function toKebab(name) {
54
61
  .toLowerCase();
55
62
  }
56
63
 
64
+ /**
65
+ * Düz dizin dosya adı → `name` + `weight`.
66
+ * `house.svg` / `house-regular.svg` → regular; `arrow-right-bold.svg` → bold.
67
+ *
68
+ * @param {string} fileName uzantılı veya uzantısız
69
+ * @returns {{ name: string, weight: string } | null}
70
+ */
71
+ export function parseIconFileName(fileName) {
72
+ const base = String(fileName).replace(/\.svg$/i, "");
73
+ if (!base) return null;
74
+
75
+ let name = base;
76
+ let weight = "regular";
77
+
78
+ const dash = base.lastIndexOf("-");
79
+ if (dash > 0) {
80
+ const maybeWeight = base.slice(dash + 1);
81
+ if (WEIGHTS.has(maybeWeight)) {
82
+ name = base.slice(0, dash);
83
+ weight = maybeWeight;
84
+ }
85
+ }
86
+
87
+ // kebab: harf/rakam segmentleri, başta/sonda tire yok
88
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) return null;
89
+
90
+ return { name, weight };
91
+ }
92
+
93
+ /**
94
+ * @param {import('../../config/index.js').ResolvedConfig} config
95
+ * @returns {string | null} mutlak dizin; yoksa veya dizin değilse null
96
+ */
97
+ function resolveLocalIconsDir(config) {
98
+ const relative = config.icons?.dir ?? "icons";
99
+ const dir = path.resolve(config.root, relative);
100
+ try {
101
+ if (fs.statSync(dir).isDirectory()) return dir;
102
+ } catch {
103
+ // yok
104
+ }
105
+ return null;
106
+ }
107
+
57
108
  /**
58
109
  * Paket, framework'ün değil **uygulamanın** node_modules'ünden çözülür:
59
110
  * ikon seti uygulamanın devDependency'si.
@@ -160,21 +211,80 @@ function scanUsedIcons(scanDirs) {
160
211
  return used;
161
212
  }
162
213
 
214
+ /**
215
+ * @param {string} svg
216
+ * @returns {{ body: string, viewBox: string }}
217
+ */
218
+ function parseSvgParts(svg) {
219
+ const open = svg.match(/<svg\b[^>]*>/i)?.[0] ?? "";
220
+ const viewBox =
221
+ open.match(/\bviewBox\s*=\s*["']([^"']+)["']/i)?.[1]?.trim() ||
222
+ DEFAULT_VIEWBOX;
223
+ const body = svg
224
+ .replace(/^[\s\S]*?<svg[^>]*>/i, "")
225
+ .replace(/<\/svg>\s*$/i, "")
226
+ .trim();
227
+ return { body, viewBox };
228
+ }
229
+
230
+ /**
231
+ * Düz `icons/` altındaki `*.svg` dosyalarından `name:weight` → parça haritası.
232
+ *
233
+ * @param {string} dir
234
+ * @returns {Map<string, { body: string, viewBox: string }>}
235
+ */
236
+ function indexLocalIcons(dir) {
237
+ /** @type {Map<string, { body: string, viewBox: string }>} */
238
+ const index = new Map();
239
+
240
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
241
+ if (!entry.isFile() || path.extname(entry.name).toLowerCase() !== ".svg") {
242
+ continue;
243
+ }
244
+
245
+ const parsed = parseIconFileName(entry.name);
246
+ if (!parsed) {
247
+ log.warn(`icon file ignored (bad name) → ${entry.name}`);
248
+ continue;
249
+ }
250
+
251
+ const key = `${parsed.name}:${parsed.weight}`;
252
+ const svg = fs.readFileSync(path.join(dir, entry.name), "utf8");
253
+ index.set(key, parseSvgParts(svg));
254
+ }
255
+
256
+ return index;
257
+ }
258
+
163
259
  /**
164
260
  * @param {string} coreAssets
165
261
  * @param {string} name kebab
166
262
  * @param {string} weight
167
- * @returns {string | null}
263
+ * @returns {{ body: string, viewBox: string } | null}
168
264
  */
169
- function readIconBody(coreAssets, name, weight) {
265
+ function readPhosphorIcon(coreAssets, name, weight) {
170
266
  const fileName = weight === "regular" ? `${name}.svg` : `${name}-${weight}.svg`;
171
267
  const filePath = path.join(coreAssets, weight, fileName);
172
268
 
173
269
  if (!fs.existsSync(filePath)) return null;
174
270
 
175
- const svg = fs.readFileSync(filePath, "utf8");
176
- const inner = svg.replace(/^[\s\S]*?<svg[^>]*>/, "").replace(/<\/svg>\s*$/, "");
177
- return inner.trim();
271
+ return parseSvgParts(fs.readFileSync(filePath, "utf8"));
272
+ }
273
+
274
+ /**
275
+ * @param {Map<string, { body: string, viewBox: string }>} index
276
+ * @param {string} name
277
+ * @param {string} weight
278
+ * @returns {{ body: string, viewBox: string } | null}
279
+ */
280
+ function readLocalIcon(index, name, weight) {
281
+ const exact = index.get(`${name}:${weight}`);
282
+ if (exact) return exact;
283
+
284
+ // `house.svg` regular sayılır; tarama `house:regular` ister.
285
+ if (weight === "regular") return index.get(`${name}:regular`) ?? null;
286
+
287
+ return null;
178
288
  }
179
289
 
180
290
  /**
@@ -182,10 +292,22 @@ function readIconBody(coreAssets, name, weight) {
182
292
  * @returns {Promise<Record<string, string>>}
183
293
  */
184
294
  export async function buildIconSprite(config) {
185
- const coreAssets = resolveIconAssets(config.root);
186
- if (!coreAssets) {
187
- log.detail("@phosphor-icons/core not installed, skipped");
188
- return {};
295
+ const localDir = resolveLocalIconsDir(config);
296
+ /** @type {Map<string, { body: string, viewBox: string }> | null} */
297
+ let localIndex = null;
298
+ /** @type {string | null} */
299
+ let coreAssets = null;
300
+
301
+ if (localDir) {
302
+ localIndex = indexLocalIcons(localDir);
303
+ const rel = path.relative(config.root, localDir) || ".";
304
+ log.detail(`icons from ${rel} (${localIndex.size} files)`);
305
+ } else {
306
+ coreAssets = resolveIconAssets(config.root);
307
+ if (!coreAssets) {
308
+ log.detail("@phosphor-icons/core not installed, skipped");
309
+ return {};
310
+ }
189
311
  }
190
312
 
191
313
  const scanDirs = (
@@ -198,15 +320,17 @@ export async function buildIconSprite(config) {
198
320
 
199
321
  for (const entry of used) {
200
322
  const [name, weight] = entry.split(":");
201
- const body = readIconBody(coreAssets, name, weight);
323
+ const parts = localIndex
324
+ ? readLocalIcon(localIndex, name, weight)
325
+ : readPhosphorIcon(/** @type {string} */ (coreAssets), name, weight);
202
326
 
203
- if (!body) {
327
+ if (!parts?.body) {
204
328
  missing.push(entry);
205
329
  continue;
206
330
  }
207
331
 
208
332
  symbols.push(
209
- `<symbol id="${name}-${weight}" viewBox="0 0 256 256">${body}</symbol>`,
333
+ `<symbol id="${name}-${weight}" viewBox="${parts.viewBox}">${parts.body}</symbol>`,
210
334
  );
211
335
  }
212
336
 
@@ -33,3 +33,13 @@ export {
33
33
  getOverlayRoot,
34
34
  } from "./dom.js";
35
35
  export { startSafeImages } from "./safe-image.js";
36
+ export {
37
+ clearSharedCookie,
38
+ consumeWindowNameHandoff,
39
+ createHandoffUrl,
40
+ handoffViaWindowName,
41
+ readCookie,
42
+ resolveSharedCookieDomain,
43
+ SHARED_COOKIE_WARN_BYTES,
44
+ writeSharedCookie,
45
+ } from "./shared-cookie.js";
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Tarayıcıda paylaşımlı cookie yazma + read-back.
3
+ *
4
+ * `document.cookie` ile Domain=.example.com yazmak bazı tarayıcılarda
5
+ * (Public Suffix, ITP) sessizce başarısız olur. Yazımdan sonra okunamazsa
6
+ * `{ handoff: true }` döner — çağıran `createHandoffUrl` veya
7
+ * `handoffViaWindowName` kullanır.
8
+ *
9
+ * `roots` verilmezse `document.documentElement.dataset.jskeletCookieRoots`
10
+ * (virgülle ayrılmış) okunur; layout'ta
11
+ * `data-jskelet-cookie-roots=".investvio.com,.localhost"` basılabilir.
12
+ */
13
+ import {
14
+ SHARED_COOKIE_WARN_BYTES,
15
+ resolveSharedCookieDomain,
16
+ } from "../shared/cookie-domain.js";
17
+
18
+ export {
19
+ SHARED_COOKIE_WARN_BYTES,
20
+ resolveSharedCookieDomain,
21
+ stripHostPort,
22
+ } from "../shared/cookie-domain.js";
23
+
24
+ /**
25
+ * @returns {string[]}
26
+ */
27
+ function rootsFromDom() {
28
+ if (typeof document === "undefined") return [];
29
+ const raw = document.documentElement?.dataset?.jskeletCookieRoots ?? "";
30
+ return raw
31
+ .split(",")
32
+ .map((entry) => entry.trim())
33
+ .filter(Boolean);
34
+ }
35
+
36
+ /**
37
+ * @param {string} name
38
+ * @returns {string | null}
39
+ */
40
+ export function readCookie(name) {
41
+ if (typeof document === "undefined") return null;
42
+ const prefix = `${encodeURIComponent(name)}=`;
43
+ for (const part of document.cookie.split(";")) {
44
+ const trimmed = part.trim();
45
+ if (!trimmed.startsWith(prefix)) continue;
46
+ try {
47
+ return decodeURIComponent(trimmed.slice(prefix.length));
48
+ } catch {
49
+ return trimmed.slice(prefix.length);
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * @typedef {object} ClientSharedCookieResult
57
+ * @property {boolean} ok
58
+ * @property {string | null} domain
59
+ * @property {boolean} handoff
60
+ * @property {string} [reason]
61
+ */
62
+
63
+ /**
64
+ * @param {string} name
65
+ * @param {string} value
66
+ * @param {{ roots?: string[], maxAge?: number, path?: string, sameSite?: string,
67
+ * secure?: boolean }} [options]
68
+ * @returns {ClientSharedCookieResult}
69
+ */
70
+ export function writeSharedCookie(name, value, options = {}) {
71
+ if (typeof document === "undefined") {
72
+ return { ok: false, domain: null, handoff: true, reason: "no-document" };
73
+ }
74
+
75
+ const roots = options.roots ?? rootsFromDom();
76
+ const domain = resolveSharedCookieDomain(location.hostname, roots);
77
+ if (!domain) {
78
+ return { ok: false, domain: null, handoff: true, reason: "no-matching-root" };
79
+ }
80
+
81
+ const text = String(value ?? "");
82
+ if (new TextEncoder().encode(text).length > SHARED_COOKIE_WARN_BYTES) {
83
+ return { ok: false, domain, handoff: true, reason: "value-too-large" };
84
+ }
85
+
86
+ const secure = options.secure ?? location.protocol === "https:";
87
+ const parts = [
88
+ `${encodeURIComponent(name)}=${encodeURIComponent(text)}`,
89
+ `Path=${options.path ?? "/"}`,
90
+ `Domain=${domain}`,
91
+ `SameSite=${options.sameSite ?? "Lax"}`,
92
+ ];
93
+ if (options.maxAge !== undefined) {
94
+ parts.push(`Max-Age=${Math.floor(Number(options.maxAge))}`);
95
+ }
96
+ if (secure) parts.push("Secure");
97
+
98
+ document.cookie = parts.join("; ");
99
+
100
+ // Read-back: tarayıcı Domain'i reddettiyse cookie görünmez.
101
+ if (readCookie(name) !== text) {
102
+ return { ok: false, domain, handoff: true, reason: "readback-failed" };
103
+ }
104
+
105
+ return { ok: true, domain, handoff: false };
106
+ }
107
+
108
+ /**
109
+ * @param {string} name
110
+ * @param {{ roots?: string[], path?: string, secure?: boolean, sameSite?: string }} [options]
111
+ * @returns {ClientSharedCookieResult}
112
+ */
113
+ export function clearSharedCookie(name, options = {}) {
114
+ if (typeof document === "undefined") {
115
+ return { ok: false, domain: null, handoff: false, reason: "no-document" };
116
+ }
117
+
118
+ const roots = options.roots ?? rootsFromDom();
119
+ const domain = resolveSharedCookieDomain(location.hostname, roots);
120
+ if (!domain) {
121
+ return { ok: false, domain: null, handoff: false, reason: "no-matching-root" };
122
+ }
123
+
124
+ const secure = options.secure ?? location.protocol === "https:";
125
+ const parts = [
126
+ `${encodeURIComponent(name)}=`,
127
+ `Path=${options.path ?? "/"}`,
128
+ `Domain=${domain}`,
129
+ "Max-Age=0",
130
+ `SameSite=${options.sameSite ?? "Lax"}`,
131
+ ];
132
+ if (secure) parts.push("Secure");
133
+ document.cookie = parts.join("; ");
134
+
135
+ return { ok: true, domain, handoff: false };
136
+ }
137
+
138
+ /**
139
+ * Handoff bileti ister; dönen URL'ye gidilir (`?handoff=` taşır).
140
+ *
141
+ * @param {{ name: string, value: string, next: string }} body
142
+ * @param {{ path?: string }} [options]
143
+ * @returns {Promise<string | null>}
144
+ */
145
+ export async function createHandoffUrl(body, options = {}) {
146
+ const path = options.path ?? "/_jskelet/auth/handoff";
147
+ const response = await fetch(path, {
148
+ method: "POST",
149
+ headers: {
150
+ "content-type": "application/json",
151
+ accept: "application/json",
152
+ },
153
+ credentials: "same-origin",
154
+ body: JSON.stringify(body),
155
+ });
156
+
157
+ if (!response.ok) return null;
158
+ /** @type {{ ok?: boolean, url?: string }} */
159
+ const data = await response.json().catch(() => ({}));
160
+ return typeof data.url === "string" ? data.url : null;
161
+ }
162
+
163
+ /**
164
+ * first-party `window.name` köprüsü — cookie Domain reddedilince yedek.
165
+ * Kaynak sayfada hedefe gitmeden önce çağırın; hedefte
166
+ * `consumeWindowNameHandoff` okur.
167
+ *
168
+ * @param {string} targetUrl
169
+ * @param {{ name: string, value: string, maxAge?: number }} cookie
170
+ * @returns {void}
171
+ */
172
+ export function handoffViaWindowName(targetUrl, cookie) {
173
+ if (typeof window === "undefined") return;
174
+ window.name = JSON.stringify({
175
+ jskeletHandoff: 1,
176
+ cookie: {
177
+ name: cookie.name,
178
+ value: cookie.value,
179
+ maxAge: cookie.maxAge,
180
+ },
181
+ });
182
+ location.assign(targetUrl);
183
+ }
184
+
185
+ /**
186
+ * Hedef host'ta `window.name` içindeki handoff'u host-only cookie olarak yazar.
187
+ *
188
+ * @param {{ write?: typeof writeSharedCookie }} [options]
189
+ * Varsayılan: Domain denemeden host-only `document.cookie`.
190
+ * @returns {boolean} Bir şey yazıldı mı.
191
+ */
192
+ export function consumeWindowNameHandoff(options = {}) {
193
+ if (typeof window === "undefined") return false;
194
+
195
+ let payload;
196
+ try {
197
+ payload = JSON.parse(window.name || "");
198
+ } catch {
199
+ return false;
200
+ }
201
+
202
+ if (!payload || payload.jskeletHandoff !== 1 || !payload.cookie?.name) {
203
+ return false;
204
+ }
205
+
206
+ window.name = "";
207
+
208
+ const { name, value, maxAge } = payload.cookie;
209
+ const text = String(value ?? "");
210
+ const secure = location.protocol === "https:";
211
+ const parts = [
212
+ `${encodeURIComponent(name)}=${encodeURIComponent(text)}`,
213
+ "Path=/",
214
+ "SameSite=Lax",
215
+ ];
216
+ if (maxAge !== undefined) parts.push(`Max-Age=${Math.floor(Number(maxAge))}`);
217
+ if (secure) parts.push("Secure");
218
+ document.cookie = parts.join("; ");
219
+
220
+ if (typeof options.write === "function") {
221
+ options.write(name, text, { maxAge });
222
+ }
223
+
224
+ return readCookie(name) === text;
225
+ }
@@ -453,4 +453,23 @@ export const DEFAULT_BRAND = {
453
453
  devBasePath: "/__jskelet/dev",
454
454
  prewarmUserAgent: "jskelet-prewarm",
455
455
  devTokenCookie: "dev_token",
456
+ /**
457
+ * Paylaşımlı cookie Domain kökleri (örn. `.investvio.com`, `.localhost`).
458
+ * `writeSharedCookie` host bunlardan birine uyuyorsa Domain olarak yazar.
459
+ * @type {string[]}
460
+ */
461
+ sharedCookieRoots: [],
462
+ };
463
+
464
+ /**
465
+ * Kimlik / alt alan handoff. Framework oturum vermez; yalnızca çapraz-subdomain
466
+ * cookie köprüsü opsiyoneldir.
467
+ */
468
+ export const DEFAULT_AUTH = {
469
+ /**
470
+ * `true` veya `{ ttlSeconds?, path?, maxValueBytes? }`.
471
+ * Açıkken `POST /_jskelet/auth/handoff` ve `?handoff=` redeem middleware'i.
472
+ * @type {boolean | { enabled?: boolean, ttlSeconds?: number, path?: string, maxValueBytes?: number }}
473
+ */
474
+ crossSubdomainHandoff: false,
456
475
  };
@@ -21,6 +21,7 @@
21
21
  * maxEntries?: number,
22
22
  * data?: {...}, redis?: {...}, prewarm?: {...} }
23
23
  * admin() → { enabled?, basePath?, allowIps?, blockBots?, … }
24
+ * auth → { crossSubdomainHandoff?: boolean | object }
24
25
  * logs → { console?, kinds?, file?, s3? }
25
26
  *
26
27
  * Fonksiyon olmayan bölümler (`brand`, `security`, `static`, `navigation`…)
@@ -33,6 +34,7 @@ import { pathToFileURL } from "node:url";
33
34
  import { compilePattern, matchPattern } from "./pattern.js";
34
35
  import {
35
36
  DEFAULT_ADMIN,
37
+ DEFAULT_AUTH,
36
38
  DEFAULT_BRAND,
37
39
  DEFAULT_CLOUDFLARE,
38
40
  DEFAULT_DATA_CACHE,
@@ -126,6 +128,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
126
128
  * @property {Record<string, unknown>} prewarm
127
129
  * @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
128
130
  * @property {Record<string, unknown>} brand
131
+ * @property {{ crossSubdomainHandoff: boolean | Record<string, unknown> }} auth
129
132
  * @property {Record<string, Function>} hooks
130
133
  * @property {string} layout Layout `.ejs` dosyasının mutlak yolu.
131
134
  * @property {string[] | null} routes Açık route modülü listesi.
@@ -138,7 +141,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
138
141
  * @property {string[]} prewarmSkip
139
142
  * @property {string[]} watch Dev sunucusunun izlediği ek dizinler.
140
143
  * @property {{ family: string, slug?: string, weights: number[] }[]} fonts
141
- * @property {{ scan?: string[] } | false} icons
144
+ * @property {{ scan?: string[], dir: string } | false} icons
142
145
  * @property {ImagesConfig | false} images
143
146
  * @property {string[]} clientEnv Client bundle'a gömülecek env anahtarları.
144
147
  */
@@ -896,6 +899,59 @@ function normalizeNavigation(raw, brand) {
896
899
  * headerName: string }} csrf
897
900
  */
898
901
 
902
+ /**
903
+ * @param {unknown} raw
904
+ * @returns {Record<string, unknown>}
905
+ */
906
+ function normalizeBrand(raw) {
907
+ const source = /** @type {Record<string, unknown>} */ (raw ?? {});
908
+ const roots = asArray(
909
+ source.sharedCookieRoots ?? DEFAULT_BRAND.sharedCookieRoots,
910
+ "brand.sharedCookieRoots",
911
+ )
912
+ .filter((entry) => typeof entry === "string")
913
+ .map((entry) => {
914
+ const trimmed = String(entry).trim().toLowerCase();
915
+ if (!trimmed) return null;
916
+ return trimmed.startsWith(".") ? trimmed : `.${trimmed}`;
917
+ })
918
+ .filter((entry) => entry !== null);
919
+
920
+ return {
921
+ ...DEFAULT_BRAND,
922
+ ...source,
923
+ sharedCookieRoots: roots,
924
+ };
925
+ }
926
+
927
+ /**
928
+ * @param {unknown} raw
929
+ * @returns {{ crossSubdomainHandoff: boolean | Record<string, unknown> }}
930
+ */
931
+ function normalizeAuth(raw) {
932
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
933
+ return { ...DEFAULT_AUTH };
934
+ }
935
+
936
+ const source = /** @type {Record<string, unknown>} */ (raw);
937
+ const handoff = source.crossSubdomainHandoff;
938
+
939
+ if (handoff === true || handoff === false || handoff == null) {
940
+ return {
941
+ crossSubdomainHandoff: handoff === true,
942
+ };
943
+ }
944
+
945
+ if (typeof handoff === "object" && !Array.isArray(handoff)) {
946
+ return { crossSubdomainHandoff: { ...handoff } };
947
+ }
948
+
949
+ console.warn(
950
+ "[config] auth.crossSubdomainHandoff must be boolean or object, ignoring it",
951
+ );
952
+ return { ...DEFAULT_AUTH };
953
+ }
954
+
899
955
  /**
900
956
  * Güvenlik bölümü. `csrf.exclude` desenleri burada derlenir: her istekte
901
957
  * yeniden derlemek gereksiz, ve bozuk bir desen sunucuyu düşürmemeli.
@@ -931,6 +987,34 @@ function normalizeSecurity(raw) {
931
987
  };
932
988
  }
933
989
 
990
+ /**
991
+ * İkon sprite ayarları. `false` → adım atlanır. `dir` varsayılanı `"icons"`:
992
+ * o dizin varsa yalnızca yerel SVG'ler; yoksa Phosphor.
993
+ *
994
+ * @param {unknown} raw
995
+ * @returns {{ scan?: string[], dir: string } | false}
996
+ */
997
+ function normalizeIcons(raw) {
998
+ if (raw === false) return false;
999
+
1000
+ const source = /** @type {Record<string, any>} */ (raw ?? {});
1001
+ const dir =
1002
+ typeof source.dir === "string" && source.dir.trim()
1003
+ ? source.dir.trim()
1004
+ : "icons";
1005
+
1006
+ /** @type {{ scan?: string[], dir: string }} */
1007
+ const icons = { dir };
1008
+
1009
+ if (source.scan != null) {
1010
+ icons.scan = asArray(source.scan, "icons.scan")
1011
+ .filter((entry) => typeof entry === "string" && entry.trim())
1012
+ .map((entry) => String(entry).trim());
1013
+ }
1014
+
1015
+ return icons;
1016
+ }
1017
+
934
1018
  /**
935
1019
  * Build + runtime görsel ayarları. `false` → her iki yüzey de kapalı.
936
1020
  * `remote.allowHosts` boşsa remote kapalı kalır (açık proxy olmasın).
@@ -1132,7 +1216,8 @@ export async function loadConfig(options = {}) {
1132
1216
  prewarmPriority,
1133
1217
  } = normalizeCache(cache);
1134
1218
  const dirs = resolveDirs(root, source.paths);
1135
- const brand = { ...DEFAULT_BRAND, ...(source.brand ?? {}) };
1219
+ const brand = normalizeBrand(source.brand);
1220
+ const auth = normalizeAuth(source.auth);
1136
1221
 
1137
1222
  config = {
1138
1223
  root,
@@ -1157,6 +1242,7 @@ export async function loadConfig(options = {}) {
1157
1242
  prewarm,
1158
1243
  prewarmPriority,
1159
1244
  brand,
1245
+ auth,
1160
1246
  hooks: source.hooks ?? {},
1161
1247
  layout: resolveLayout(dirs, source.layout),
1162
1248
  routes: Array.isArray(source.routes) ? source.routes : null,
@@ -1179,7 +1265,7 @@ export async function loadConfig(options = {}) {
1179
1265
  // Build tarafı ayarları. Sunucu bunları okumaz ama config tek dosya
1180
1266
  // olsun diye aynı yerden geçer.
1181
1267
  fonts: source.fonts ?? [],
1182
- icons: source.icons ?? {},
1268
+ icons: normalizeIcons(source.icons),
1183
1269
  images: normalizeImages(source.images),
1184
1270
  clientEnv: source.clientEnv ?? [],
1185
1271
  };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `jskelet/cookies` paket girişi — imzalı cookie + paylaşımlı Domain yüzeyi.
3
+ */
4
+ export {
5
+ clearCookie,
6
+ getSignedCookie,
7
+ parseCookies,
8
+ randomToken,
9
+ safeEqual,
10
+ serializeCookie,
11
+ setCookie,
12
+ setSignedCookie,
13
+ } from "./cookies.js";
14
+ export {
15
+ clearSharedCookie,
16
+ requestIsHttps,
17
+ resolveSharedCookieDomain,
18
+ SHARED_COOKIE_WARN_BYTES,
19
+ writeSharedCookie,
20
+ } from "./shared-cookie.js";
@@ -144,6 +144,7 @@ export function parseCookies(req) {
144
144
  * @property {Date} [expires]
145
145
  * @property {boolean} [httpOnly] Varsayılan `true`.
146
146
  * @property {boolean} [secure] Varsayılan: development dışında `true`.
147
+ * `writeSharedCookie` protokole bakarak ezer (`https` → Secure).
147
148
  * @property {"Strict" | "Lax" | "None"} [sameSite] Varsayılan `Lax`.
148
149
  */
149
150
 
@@ -255,3 +256,4 @@ export function getSignedCookie(req, name) {
255
256
  export function randomToken(bytes = 32) {
256
257
  return crypto.randomBytes(bytes).toString("base64url");
257
258
  }
259
+