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
package/docs/07-yapilandirma.md
CHANGED
|
@@ -88,7 +88,7 @@ export default {
|
|
|
88
88
|
watch: ["data"],
|
|
89
89
|
|
|
90
90
|
fonts: [{ family: "Inter", weights: [400, 600, 700] }],
|
|
91
|
-
icons: { scan: ["views", "client", "routes", "lib"] },
|
|
91
|
+
icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
|
|
92
92
|
images: { widths: [400, 800, 1200], quality: 78, skip: ["indirmeler"] },
|
|
93
93
|
clientEnv: ["PUBLIC_WS_URL"],
|
|
94
94
|
|
|
@@ -186,14 +186,38 @@ birleştirilir.
|
|
|
186
186
|
| `prewarmUserAgent` | `string` | `"jskelet-prewarm"` | Isıtma isteklerinin UA'sı; dev paneli bunu filtreler |
|
|
187
187
|
| `devTokenCookie` | `string` | `"dev_token"` | Dev gate'in çerez ve query parametresi adı |
|
|
188
188
|
| `lang` | `string` | — | `<html lang>` varsayılanı. Verilmezse layout `"en"` kullanır. |
|
|
189
|
+
| `sharedCookieRoots` | `string[]` | `[]` | Paylaşımlı cookie Domain kökleri (örn. `.investvio.com`, `.localhost`). [12](./12-panel-ve-oturum.md) |
|
|
189
190
|
|
|
190
191
|
`lang` için öncelik sırası: `hooks.layoutContext()` → `lang` **>** `brand.lang`
|
|
191
192
|
**>** `"en"`.
|
|
192
193
|
|
|
193
194
|
```js
|
|
194
|
-
brand: {
|
|
195
|
+
brand: {
|
|
196
|
+
lang: "tr",
|
|
197
|
+
poweredBy: "Örnek",
|
|
198
|
+
sharedCookieRoots: [".investvio.com", ".localhost"],
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## `auth`
|
|
203
|
+
|
|
204
|
+
**Tip:** `object` — **Varsayılan:** `{ crossSubdomainHandoff: false }`
|
|
205
|
+
|
|
206
|
+
Kimlik framework'te yok; bu bölüm yalnızca alt alan adları arasında kısa
|
|
207
|
+
session id taşımak için handoff köprüsünü açar.
|
|
208
|
+
|
|
209
|
+
| Alan | Tip | Varsayılan | Anlamı |
|
|
210
|
+
| --- | --- | --- | --- |
|
|
211
|
+
| `crossSubdomainHandoff` | `boolean \| object` | `false` | `true` veya `{ ttlSeconds?, path?, maxValueBytes? }` → `POST /_jskelet/auth/handoff` + `?handoff=` redeem |
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
auth: {
|
|
215
|
+
crossSubdomainHandoff: { ttlSeconds: 60 },
|
|
216
|
+
},
|
|
195
217
|
```
|
|
196
218
|
|
|
219
|
+
Ayrıntı ve `window.name` yedeği: [12-panel-ve-oturum.md](./12-panel-ve-oturum.md).
|
|
220
|
+
|
|
197
221
|
## `layout`
|
|
198
222
|
|
|
199
223
|
**Tip:** `string` — **Varsayılan:** yok (otomatik çözüm)
|
|
@@ -463,21 +487,27 @@ fonts: [
|
|
|
463
487
|
|
|
464
488
|
## `icons`
|
|
465
489
|
|
|
466
|
-
**Tip:** `{ scan?: string[] } | false` — **Varsayılan:** `{}`
|
|
490
|
+
**Tip:** `{ scan?: string[], dir?: string } | false` — **Varsayılan:** `{ dir: "icons" }`
|
|
467
491
|
|
|
468
|
-
|
|
492
|
+
SVG ikon sprite üretimi. Kaynak **XOR** seçilir: `icons.dir` dizini varsa
|
|
493
|
+
yalnızca oradaki düz SVG'ler; yoksa `@phosphor-icons/core` (kuruluysa).
|
|
469
494
|
|
|
470
495
|
| Değer | Sonuç |
|
|
471
496
|
| --- | --- |
|
|
472
|
-
| `{}` (varsayılan) |
|
|
497
|
+
| `{}` (varsayılan) | `dir: "icons"`; taranan dizinler `["views", "client", "routes", "lib", "features", "shared"]` |
|
|
498
|
+
| `{ dir: "assets/icons" }` | Yerel SVG kökü değiştirilir |
|
|
473
499
|
| `{ scan: [...] }` | Taranan dizinler değiştirilir |
|
|
474
500
|
| `false` | Sprite adımı tamamen atlanır |
|
|
475
501
|
|
|
476
|
-
|
|
477
|
-
|
|
502
|
+
Yerel dizin (varsa) düz dosya adları kullanır: `house.svg` → `house:regular`,
|
|
503
|
+
`house-bold.svg` → `house:bold`. Boş bir `icons/` dizini Phosphor'a düşmez —
|
|
504
|
+
dizini silmek fallback'i açar. Ayrıntı: [08-build.md](./08-build.md).
|
|
478
505
|
|
|
479
506
|
```js
|
|
480
|
-
icons: {
|
|
507
|
+
icons: {
|
|
508
|
+
dir: "icons",
|
|
509
|
+
scan: ["views", "client", "routes", "lib", "content"],
|
|
510
|
+
}
|
|
481
511
|
```
|
|
482
512
|
|
|
483
513
|
## `images`
|
package/docs/08-build.md
CHANGED
|
@@ -249,16 +249,32 @@ bu dosyalara otomatik olarak `immutable` cache yazılır.
|
|
|
249
249
|
|
|
250
250
|
## İkon sprite
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
**Yalnızca kaynakta kullanılan** ikonlar için bir `<symbol>` seti üretir. Tüm
|
|
253
|
+
seti göndermek 1500+ ikon, yani birkaç megabayt; kullanım taraması sprite'ı
|
|
254
|
+
tipik olarak 10-30 sembolde tutuyor. Çıktı hash'li `sprite.svg` olarak
|
|
255
|
+
`public/assets/` altına yazılır ve precompress kapsamına girer.
|
|
256
|
+
|
|
257
|
+
Kaynak **XOR** seçilir — ikisi birleştirilmez:
|
|
258
|
+
|
|
259
|
+
1. `icons.dir` (varsayılan `icons/`) **dizin olarak varsa** yalnızca oradaki
|
|
260
|
+
düz SVG'ler. Boş dizin Phosphor'a düşmez; fallback için dizini silin.
|
|
261
|
+
2. Aksi hâlde `@phosphor-icons/core` (uygulamanın `node_modules`'ünden). Kurulu
|
|
262
|
+
değilse adım sessizce atlanır.
|
|
263
|
+
|
|
264
|
+
Yerel dosya adları:
|
|
265
|
+
|
|
266
|
+
| Dosya | Sprite anahtarı |
|
|
267
|
+
| --- | --- |
|
|
268
|
+
| `icons/house.svg` | `house:regular` |
|
|
269
|
+
| `icons/house-regular.svg` | `house:regular` |
|
|
270
|
+
| `icons/arrow-right-bold.svg` | `arrow-right:bold` |
|
|
256
271
|
|
|
257
272
|
- Sembol id'si: `<kebab-ad>-<weight>`, örn. `arrow-right-bold`.
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
- Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib
|
|
261
|
-
`icons.scan` ile değiştirilebilir. Taranan uzantılar:
|
|
273
|
+
- `viewBox` kaynak SVG'den `<symbol>`'e taşınır; yoksa `0 0 256 256`
|
|
274
|
+
(Phosphor ve `icon()` ile uyum için önerilen kutu).
|
|
275
|
+
- Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib`,
|
|
276
|
+
`features`, `shared`; `icons.scan` ile değiştirilebilir. Taranan uzantılar:
|
|
277
|
+
`.ejs`, `.jsk`, `.js`, `.mjs`.
|
|
262
278
|
- Ağırlıklar: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. Tanınmayan
|
|
263
279
|
bir ağırlık `regular` sayılır.
|
|
264
280
|
|
|
@@ -285,7 +301,7 @@ Bu uyarıyı görürseniz ya adı sabit yazın, ya `icons.scan` listesine ilgili
|
|
|
285
301
|
dizini ekleyin, ya da adı bir yapılandırma alanında `icon: "XLogo"` biçiminde
|
|
286
302
|
tutun.
|
|
287
303
|
|
|
288
|
-
|
|
304
|
+
Kaynakta bulunamayan adlar build sonunda özet olarak uyarılır:
|
|
289
305
|
`N icons missing → …`
|
|
290
306
|
|
|
291
307
|
## Görsel optimizasyonu
|
|
@@ -355,7 +371,7 @@ Bu dosyaları `staticPrecompressed` middleware'i servis eder; kopya yoksa istek
|
|
|
355
371
|
| `tailwindcss` | CSS (peer) | Tailwind direktifleri çözülemez |
|
|
356
372
|
| `lightningcss` | CSS minifikasyonu | Tailwind çıktısı kullanılır, birkaç kB daha büyük |
|
|
357
373
|
| `sharp` | Görsel optimizasyonu | Adım atlanır; `image()` orijinali kullanır |
|
|
358
|
-
| `@phosphor-icons/core` | İkon sprite | Adım atlanır; `icon()` boş `<use>` üretir |
|
|
374
|
+
| `@phosphor-icons/core` | İkon sprite (yerel `icons/` yoksa) | Adım atlanır; `icon()` boş `<use>` üretir |
|
|
359
375
|
|
|
360
376
|
CSS kullanmayacaksanız `paths.styles` dosyasını hiç oluşturmayın: adım uyarıyla
|
|
361
377
|
atlanır ve postcss'e ihtiyaç kalmaz.
|
|
@@ -129,6 +129,94 @@ kapatıyor — cookie çapraz site POST'larında hiç gönderilmiyor.
|
|
|
129
129
|
Cookie **şifrelenmiyor**, imzalanıyor. Değer okunabilir; gizli kalması gereken
|
|
130
130
|
veriyi değil, onun kimliğini koyun.
|
|
131
131
|
|
|
132
|
+
## Alt alan adları: paylaşımlı cookie
|
|
133
|
+
|
|
134
|
+
Host tabanlı i18n (`tr.example.com` / `en.example.com`) oturumu alt alanlar
|
|
135
|
+
arasında paylaşmak ister. Çözüm **kısa session id** + isteğe bağlı
|
|
136
|
+
`Domain=.example.com` — JWT veya büyük access token paylaşımlı cookie'ye
|
|
137
|
+
konmaz (`large token ≠ shared cookie`).
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
// jskelet.config.mjs
|
|
141
|
+
export default {
|
|
142
|
+
brand: {
|
|
143
|
+
sharedCookieRoots: [".investvio.com", ".localhost"],
|
|
144
|
+
},
|
|
145
|
+
auth: {
|
|
146
|
+
crossSubdomainHandoff: true, // POST /_jskelet/auth/handoff
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Sunucu
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import { writeSharedCookie } from "jskelet/cookies";
|
|
155
|
+
|
|
156
|
+
export function startSession(res, req, sessionId) {
|
|
157
|
+
const result = writeSharedCookie(res, "sid", sessionId, {
|
|
158
|
+
req,
|
|
159
|
+
maxAge: 60 * 60 * 8,
|
|
160
|
+
});
|
|
161
|
+
// result.ok === false → result.handoff; istemci handoff denemeli
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`Secure` **protokole** bakılır (`https` / `x-forwarded-proto`), `NODE_ENV`'e
|
|
167
|
+
değil. Domain, isteğin Host'u `sharedCookieRoots` ile eşleşince yazılır.
|
|
168
|
+
Değer ~512 baytı aşarsa yazım reddedilir ve `handoff: true` döner.
|
|
169
|
+
|
|
170
|
+
### İstemci
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import {
|
|
174
|
+
writeSharedCookie,
|
|
175
|
+
createHandoffUrl,
|
|
176
|
+
handoffViaWindowName,
|
|
177
|
+
consumeWindowNameHandoff,
|
|
178
|
+
} from "jskelet/client";
|
|
179
|
+
|
|
180
|
+
const result = writeSharedCookie("sid", sessionId, {
|
|
181
|
+
roots: [".investvio.com", ".localhost"],
|
|
182
|
+
maxAge: 60 * 60 * 8,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!result.ok && result.handoff) {
|
|
186
|
+
const url = await createHandoffUrl({
|
|
187
|
+
name: "sid",
|
|
188
|
+
value: sessionId,
|
|
189
|
+
next: "https://tr.investvio.com/panel",
|
|
190
|
+
});
|
|
191
|
+
if (url) location.assign(url);
|
|
192
|
+
else handoffViaWindowName("https://tr.investvio.com/panel", {
|
|
193
|
+
name: "sid",
|
|
194
|
+
value: sessionId,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Hedef host'ta (layout / island bootstrap):
|
|
199
|
+
consumeWindowNameHandoff();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`roots` verilmezse `<html data-jskelet-cookie-roots=".investvio.com,.localhost">`
|
|
203
|
+
okunur. Yazımdan sonra **read-back** yapılır; tarayıcı Domain'i reddettiyse
|
|
204
|
+
`handoff: true`.
|
|
205
|
+
|
|
206
|
+
### Handoff bileti
|
|
207
|
+
|
|
208
|
+
`auth.crossSubdomainHandoff` açıkken:
|
|
209
|
+
|
|
210
|
+
1. `POST /_jskelet/auth/handoff` `{ name, value, next }` → `{ url }` (`?handoff=` ekli)
|
|
211
|
+
2. Hedef host'ta GET middleware bileti tek kullanımlık tüketir, cookie yazar
|
|
212
|
+
(önce shared Domain, olmazsa host-only), `handoff` query'siz 303
|
|
213
|
+
|
|
214
|
+
`next` yalnızca aynı `sharedCookieRoots` altındaki host'lara izinli. Bilet
|
|
215
|
+
~60 sn, süreç belleğinde. JWT URL'ye konmaz.
|
|
216
|
+
|
|
217
|
+
`window.name` köprüsü cookie'siz yedek: kaynakta `handoffViaWindowName`,
|
|
218
|
+
hedeefte `consumeWindowNameHandoff`.
|
|
219
|
+
|
|
132
220
|
## CSRF
|
|
133
221
|
|
|
134
222
|
Gövdeyi framework ayrıştırıyor (`express.urlencoded` + `express.json`), yani
|
|
@@ -92,7 +92,7 @@ export default {
|
|
|
92
92
|
watch: ["data"],
|
|
93
93
|
|
|
94
94
|
fonts: [{ family: "Inter", weights: [400, 600, 700] }],
|
|
95
|
-
icons: { scan: ["views", "client", "routes", "lib"] },
|
|
95
|
+
icons: { dir: "icons", scan: ["views", "client", "routes", "lib"] },
|
|
96
96
|
images: { widths: [400, 800, 1200], quality: 78, skip: ["downloads"] },
|
|
97
97
|
clientEnv: ["PUBLIC_WS_URL"],
|
|
98
98
|
|
|
@@ -191,14 +191,39 @@ shallow-merged with the defaults.
|
|
|
191
191
|
| `prewarmUserAgent` | `string` | `"jskelet-prewarm"` | UA of prewarm requests; the dev panel filters on it |
|
|
192
192
|
| `devTokenCookie` | `string` | `"dev_token"` | Name of the dev gate's cookie and query parameter |
|
|
193
193
|
| `lang` | `string` | — | Default for `<html lang>`. If not given, the layout uses `"en"`. |
|
|
194
|
+
| `sharedCookieRoots` | `string[]` | `[]` | Shared cookie Domain roots (e.g. `.investvio.com`, `.localhost`). [12](./12-dashboards-and-sessions.md) |
|
|
194
195
|
|
|
195
196
|
Precedence for `lang`: `hooks.layoutContext()` → `lang` **>** `brand.lang`
|
|
196
197
|
**>** `"en"`.
|
|
197
198
|
|
|
198
199
|
```js
|
|
199
|
-
brand: {
|
|
200
|
+
brand: {
|
|
201
|
+
lang: "tr",
|
|
202
|
+
poweredBy: "Example",
|
|
203
|
+
sharedCookieRoots: [".investvio.com", ".localhost"],
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## `auth`
|
|
208
|
+
|
|
209
|
+
**Type:** `object` — **Default:** `{ crossSubdomainHandoff: false }`
|
|
210
|
+
|
|
211
|
+
The framework does not provide identity; this section only opens the
|
|
212
|
+
cross-subdomain handoff bridge for a short session id.
|
|
213
|
+
|
|
214
|
+
| Field | Type | Default | Meaning |
|
|
215
|
+
| --- | --- | --- | --- |
|
|
216
|
+
| `crossSubdomainHandoff` | `boolean \| object` | `false` | `true` or `{ ttlSeconds?, path?, maxValueBytes? }` → `POST /_jskelet/auth/handoff` + `?handoff=` redeem |
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
auth: {
|
|
220
|
+
crossSubdomainHandoff: { ttlSeconds: 60 },
|
|
221
|
+
},
|
|
200
222
|
```
|
|
201
223
|
|
|
224
|
+
Details and the `window.name` fallback:
|
|
225
|
+
[12-dashboards-and-sessions.md](./12-dashboards-and-sessions.md).
|
|
226
|
+
|
|
202
227
|
## `layout`
|
|
203
228
|
|
|
204
229
|
**Type:** `string` — **Default:** none (automatic resolution)
|
|
@@ -475,21 +500,29 @@ fonts: [
|
|
|
475
500
|
|
|
476
501
|
## `icons`
|
|
477
502
|
|
|
478
|
-
**Type:** `{ scan?: string[] } | false` — **Default:** `{}`
|
|
503
|
+
**Type:** `{ scan?: string[], dir?: string } | false` — **Default:** `{ dir: "icons" }`
|
|
479
504
|
|
|
480
|
-
|
|
505
|
+
SVG icon sprite generation. The source is chosen **XOR**: if the `icons.dir`
|
|
506
|
+
directory exists, only the flat SVGs there are used; otherwise
|
|
507
|
+
`@phosphor-icons/core` (when installed).
|
|
481
508
|
|
|
482
509
|
| Value | Result |
|
|
483
510
|
| --- | --- |
|
|
484
|
-
| `{}` (default) |
|
|
511
|
+
| `{}` (default) | `dir: "icons"`; scanned directories are `["views", "client", "routes", "lib", "features", "shared"]` |
|
|
512
|
+
| `{ dir: "assets/icons" }` | Changes the local SVG root |
|
|
485
513
|
| `{ scan: [...] }` | Changes the scanned directories |
|
|
486
514
|
| `false` | The sprite step is skipped entirely |
|
|
487
515
|
|
|
488
|
-
|
|
489
|
-
|
|
516
|
+
A local directory (when present) uses flat file names: `house.svg` →
|
|
517
|
+
`house:regular`, `house-bold.svg` → `house:bold`. An empty `icons/` directory
|
|
518
|
+
does not fall back to Phosphor — delete the directory to open the fallback.
|
|
519
|
+
Details: [08-build.md](./08-build.md).
|
|
490
520
|
|
|
491
521
|
```js
|
|
492
|
-
icons: {
|
|
522
|
+
icons: {
|
|
523
|
+
dir: "icons",
|
|
524
|
+
scan: ["views", "client", "routes", "lib", "content"],
|
|
525
|
+
}
|
|
493
526
|
```
|
|
494
527
|
|
|
495
528
|
## `images`
|
package/docs/en/08-build.md
CHANGED
|
@@ -259,17 +259,33 @@ Because the `.woff2` extension and the `/fonts/` prefix are in the default
|
|
|
259
259
|
|
|
260
260
|
## Icon sprite
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
Produces a `<symbol>` set for **only the icons actually used in the source**.
|
|
263
|
+
Shipping the whole set means 1500+ icons, i.e. several megabytes; usage scanning
|
|
264
|
+
typically keeps the sprite at 10-30 symbols. The hashed `sprite.svg` is written
|
|
265
|
+
under `public/assets/` and is covered by precompress.
|
|
266
|
+
|
|
267
|
+
The source is chosen **XOR** — the two are never merged:
|
|
268
|
+
|
|
269
|
+
1. If `icons.dir` (default `icons/`) **exists as a directory**, only the flat
|
|
270
|
+
SVGs there. An empty directory does not fall back to Phosphor; delete the
|
|
271
|
+
directory to open the fallback.
|
|
272
|
+
2. Otherwise `@phosphor-icons/core` (from the application's `node_modules`). If
|
|
273
|
+
it is not installed, the step is silently skipped.
|
|
274
|
+
|
|
275
|
+
Local file names:
|
|
276
|
+
|
|
277
|
+
| File | Sprite key |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| `icons/house.svg` | `house:regular` |
|
|
280
|
+
| `icons/house-regular.svg` | `house:regular` |
|
|
281
|
+
| `icons/arrow-right-bold.svg` | `arrow-right:bold` |
|
|
266
282
|
|
|
267
283
|
- Symbol id: `<kebab-name>-<weight>`, e.g. `arrow-right-bold`.
|
|
268
|
-
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
284
|
+
- `viewBox` is copied from the source SVG onto the `<symbol>`; if missing,
|
|
285
|
+
`0 0 256 256` (recommended for Phosphor / `icon()` compatibility).
|
|
286
|
+
- The scanned directories default to `views`, `client`, `routes`, `lib`,
|
|
287
|
+
`features`, `shared`; they can be changed with `icons.scan`. Scanned
|
|
288
|
+
extensions: `.ejs`, `.jsk`, `.js`, `.mjs`.
|
|
273
289
|
- Weights: `thin`, `light`, `regular`, `bold`, `fill`, `duotone`. An
|
|
274
290
|
unrecognised weight counts as `regular`.
|
|
275
291
|
|
|
@@ -296,8 +312,8 @@ If you see this warning, either write the name as a constant, or add the relevan
|
|
|
296
312
|
directory to the `icons.scan` list, or keep the name in a configuration field in
|
|
297
313
|
the form `icon: "XLogo"`.
|
|
298
314
|
|
|
299
|
-
Names that cannot be found in
|
|
300
|
-
of the build: `N icons missing → …`
|
|
315
|
+
Names that cannot be found in the chosen source are warned about as a summary at
|
|
316
|
+
the end of the build: `N icons missing → …`
|
|
301
317
|
|
|
302
318
|
## Image optimisation
|
|
303
319
|
|
|
@@ -373,7 +389,7 @@ copy, the request is handed over to `express.static`
|
|
|
373
389
|
| `tailwindcss` | CSS (peer) | Tailwind directives cannot be resolved |
|
|
374
390
|
| `lightningcss` | CSS minification | Tailwind's output is used, a few kB bigger |
|
|
375
391
|
| `sharp` | Image optimisation | The step is skipped; `image()` uses the original |
|
|
376
|
-
| `@phosphor-icons/core` | Icon sprite | The step is skipped; `icon()` produces an empty `<use>` |
|
|
392
|
+
| `@phosphor-icons/core` | Icon sprite (when no local `icons/` dir) | The step is skipped; `icon()` produces an empty `<use>` |
|
|
377
393
|
|
|
378
394
|
If you are not going to use CSS, simply never create the `paths.styles` file: the
|
|
379
395
|
step is skipped with a warning and postcss is not needed.
|
|
@@ -131,6 +131,97 @@ cookie is simply not sent on cross-site POSTs.
|
|
|
131
131
|
Cookies are **signed, not encrypted**. The value is readable, so store the
|
|
132
132
|
identifier of a secret rather than the secret itself.
|
|
133
133
|
|
|
134
|
+
## Cross-subdomain: shared cookies
|
|
135
|
+
|
|
136
|
+
Host-based i18n (`tr.example.com` / `en.example.com`) often needs a session on
|
|
137
|
+
every locale host. The supported pattern is a **short session id** plus an
|
|
138
|
+
optional `Domain=.example.com` — do not put a JWT or a large access token in a
|
|
139
|
+
shared cookie (`large token ≠ shared cookie`).
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
// jskelet.config.mjs
|
|
143
|
+
export default {
|
|
144
|
+
brand: {
|
|
145
|
+
sharedCookieRoots: [".investvio.com", ".localhost"],
|
|
146
|
+
},
|
|
147
|
+
auth: {
|
|
148
|
+
crossSubdomainHandoff: true, // POST /_jskelet/auth/handoff
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Server
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { writeSharedCookie } from "jskelet/cookies";
|
|
157
|
+
|
|
158
|
+
export function startSession(res, req, sessionId) {
|
|
159
|
+
const result = writeSharedCookie(res, "sid", sessionId, {
|
|
160
|
+
req,
|
|
161
|
+
maxAge: 60 * 60 * 8,
|
|
162
|
+
});
|
|
163
|
+
// result.ok === false → result.handoff; the client should use handoff
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`Secure` follows the **protocol** (`https` / `x-forwarded-proto`), not
|
|
169
|
+
`NODE_ENV`. The Domain is set when the request Host matches
|
|
170
|
+
`sharedCookieRoots`. Values larger than ~512 bytes are refused with
|
|
171
|
+
`handoff: true`.
|
|
172
|
+
|
|
173
|
+
### Client
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import {
|
|
177
|
+
writeSharedCookie,
|
|
178
|
+
createHandoffUrl,
|
|
179
|
+
handoffViaWindowName,
|
|
180
|
+
consumeWindowNameHandoff,
|
|
181
|
+
} from "jskelet/client";
|
|
182
|
+
|
|
183
|
+
const result = writeSharedCookie("sid", sessionId, {
|
|
184
|
+
roots: [".investvio.com", ".localhost"],
|
|
185
|
+
maxAge: 60 * 60 * 8,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!result.ok && result.handoff) {
|
|
189
|
+
const url = await createHandoffUrl({
|
|
190
|
+
name: "sid",
|
|
191
|
+
value: sessionId,
|
|
192
|
+
next: "https://tr.investvio.com/panel",
|
|
193
|
+
});
|
|
194
|
+
if (url) location.assign(url);
|
|
195
|
+
else handoffViaWindowName("https://tr.investvio.com/panel", {
|
|
196
|
+
name: "sid",
|
|
197
|
+
value: sessionId,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// On the target host (layout / island bootstrap):
|
|
202
|
+
consumeWindowNameHandoff();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If `roots` is omitted, the client reads
|
|
206
|
+
`<html data-jskelet-cookie-roots=".investvio.com,.localhost">`. After writing,
|
|
207
|
+
a **read-back** runs; if the browser rejected the Domain, `handoff: true`.
|
|
208
|
+
|
|
209
|
+
### Handoff ticket
|
|
210
|
+
|
|
211
|
+
With `auth.crossSubdomainHandoff` on:
|
|
212
|
+
|
|
213
|
+
1. `POST /_jskelet/auth/handoff` `{ name, value, next }` → `{ url }` (with `?handoff=`)
|
|
214
|
+
2. On the target host a GET middleware redeems the one-time ticket, sets the
|
|
215
|
+
cookie (shared Domain first, else host-only), and 303-redirects without
|
|
216
|
+
`handoff`
|
|
217
|
+
|
|
218
|
+
`next` must be under the same `sharedCookieRoots`. Tickets live ~60s in process
|
|
219
|
+
memory. Do not put a JWT in the URL.
|
|
220
|
+
|
|
221
|
+
The `window.name` bridge is the cookie-less fallback:
|
|
222
|
+
`handoffViaWindowName` on the source page, `consumeWindowNameHandoff` on the
|
|
223
|
+
target.
|
|
224
|
+
|
|
134
225
|
## CSRF
|
|
135
226
|
|
|
136
227
|
The framework parses the request body (`express.urlencoded` + `express.json`),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + build-time .jsk (or EJS) SSR, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"./client": "./src/client/index.js",
|
|
34
34
|
"./html": "./src/views/helpers/html.js",
|
|
35
35
|
"./tags": "./src/views/helpers/tags.js",
|
|
36
|
-
"./cookies": "./src/http/cookies.js",
|
|
36
|
+
"./cookies": "./src/http/cookies-entry.js",
|
|
37
37
|
"./log": "./src/log.mjs",
|
|
38
38
|
"./register": "./src/runtime/register.mjs",
|
|
39
39
|
"./layout": "./src/templates/layout.ejs"
|