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.
- package/CHANGELOG.md +292 -377
- package/docs/07-yapilandirma.md +38 -8
- package/docs/08-build.md +26 -10
- package/docs/12-panel-ve-oturum.md +88 -0
- package/docs/en/07-configuration.md +41 -8
- package/docs/en/08-build.md +28 -12
- package/docs/en/12-dashboards-and-sessions.md +91 -0
- package/package.json +2 -2
- package/src/build/tasks/icons.mjs +141 -17
- package/src/client/index.js +10 -0
- package/src/client/shared-cookie.js +225 -0
- package/src/config/defaults.js +19 -0
- package/src/config/index.js +89 -3
- package/src/http/cookies-entry.js +20 -0
- package/src/http/cookies.js +2 -0
- package/src/http/shared-cookie.js +178 -0
- package/src/index.js +7 -0
- package/src/server/auth/handoff.js +226 -0
- package/src/server/create-app.js +14 -0
- package/src/shared/cookie-domain.js +66 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SVG ikon sprite üretimi.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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="
|
|
333
|
+
`<symbol id="${name}-${weight}" viewBox="${parts.viewBox}">${parts.body}</symbol>`,
|
|
210
334
|
);
|
|
211
335
|
}
|
|
212
336
|
|
package/src/client/index.js
CHANGED
|
@@ -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
|
+
}
|
package/src/config/defaults.js
CHANGED
|
@@ -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
|
};
|
package/src/config/index.js
CHANGED
|
@@ -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 =
|
|
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";
|
package/src/http/cookies.js
CHANGED
|
@@ -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
|
+
|