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.
@@ -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: { lang: "tr", poweredBy: "Örnek", cacheHeader: "X-Ornek-Cache" }
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
- Phosphor SVG sprite üretimi.
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) | Sprite üretilir; taranan dizinler `["views", "client", "routes", "lib", "features", "shared"]` |
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
- `@phosphor-icons/core` uygulamanın `node_modules`'ünde yoksa adım sessizce
477
- atlanır. Ayrıntı: [08-build.md](./08-build.md).
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: { scan: ["views", "client", "routes", "lib", "content"] }
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
- `@phosphor-icons/core` içindeki tek tek SVG'lerden, **yalnızca kaynakta
253
- kullanılan** ikonlar için `<symbol>` seti üretir. Tüm seti göndermek 1500+ ikon,
254
- yani birkaç megabayt; kullanım taraması sprite'ı tipik olarak 10-30 sembolde
255
- tutuyor.
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
- - Paket **uygulamanın** `node_modules`'ünden çözülür (ikon seti uygulamanın
259
- devDependency'si); kurulu değilse adım sessizce atlanır.
260
- - Taranan dizinler varsayılan olarak `views`, `client`, `routes`, `lib`;
261
- `icons.scan` ile değiştirilebilir. Taranan uzantılar: `.ejs`, `.js`, `.mjs`.
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
- Phosphor'da bulunamayan adlar build sonunda özet olarak uyarılır:
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: { lang: "tr", poweredBy: "Example", cacheHeader: "X-Example-Cache" }
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
- Phosphor SVG sprite generation.
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) | The sprite is generated; scanned directories are `["views", "client", "routes", "lib"]` |
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
- If `@phosphor-icons/core` is not in the application's `node_modules`, the step is
489
- silently skipped. Details: [08-build.md](./08-build.md).
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: { scan: ["views", "client", "routes", "lib", "content"] }
522
+ icons: {
523
+ dir: "icons",
524
+ scan: ["views", "client", "routes", "lib", "content"],
525
+ }
493
526
  ```
494
527
 
495
528
  ## `images`
@@ -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
- From the individual SVGs inside `@phosphor-icons/core`, it produces a `<symbol>`
263
- set for **only the icons actually used in the source**. Shipping the whole set
264
- means 1500+ icons, i.e. several megabytes; usage scanning typically keeps the
265
- sprite at 10-30 symbols.
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
- - The package is resolved from the **application's** `node_modules` (the icon set
269
- is the application's devDependency); if it is not installed, the step is
270
- silently skipped.
271
- - The scanned directories default to `views`, `client`, `routes`, `lib`; they can
272
- be changed with `icons.scan`. Scanned extensions: `.ejs`, `.js`, `.mjs`.
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 Phosphor are warned about as a summary at the end
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",
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"