tina4-nodejs 3.13.147 → 3.13.148

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/CLAUDE.md CHANGED
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
13
13
 
14
14
  The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
15
15
 
16
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.147)
16
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.148)
17
17
 
18
18
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
19
19
 
20
20
  ## What This Project Is
21
21
 
22
- Tina4 for Node.js/TypeScript v3.13.147 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
22
+ Tina4 for Node.js/TypeScript v3.13.148 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
23
23
 
24
24
  The philosophy: zero ceremony, batteries included, file system as source of truth.
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.147",
3
+ "version": "3.13.148",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
@@ -23878,6 +23878,7 @@ __export(session_exports, {
23878
23878
  isSecureScheme: () => isSecureScheme,
23879
23879
  isValidSessionId: () => isValidSessionId,
23880
23880
  sessionCookieName: () => sessionCookieName,
23881
+ sessionCookieToSend: () => sessionCookieToSend,
23881
23882
  sessionStrictMode: () => sessionStrictMode
23882
23883
  });
23883
23884
  import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
@@ -23938,6 +23939,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
23938
23939
  parts.push(`Max-Age=${ttl}`);
23939
23940
  return parts.join("; ");
23940
23941
  }
23942
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
23943
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
23944
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
23945
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
23946
+ const xfProto = rawReq.headers["x-forwarded-proto"];
23947
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
23948
+ const socketEncrypted = rawReq.socket?.encrypted === true;
23949
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
23950
+ }
23941
23951
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
23942
23952
  var init_session = __esm({
23943
23953
  "../core/src/session.ts"() {
@@ -24300,7 +24310,6 @@ var init_session = __esm({
24300
24310
  const now = Math.floor(Date.now() / 1e3);
24301
24311
  this.data = { _created: now, _accessed: now };
24302
24312
  this.dirty = false;
24303
- this.safeWrite(this.sessionId, this.data, this.ttl);
24304
24313
  this.stored = false;
24305
24314
  this.loaded = /* @__PURE__ */ new Map();
24306
24315
  this.cleared = false;
@@ -24451,6 +24460,19 @@ var init_session = __esm({
24451
24460
  const result = this.flash(key);
24452
24461
  return result !== void 0 ? result : defaultValue;
24453
24462
  }
24463
+ /**
24464
+ * Whether this session is only an id: nothing has stored it and it holds no
24465
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
24466
+ * Such a session has no record and needs no cookie, so save() writes nothing
24467
+ * for it and the server sends no cookie for it. It stops being fresh when
24468
+ * something is stored in it. Calls that mark a session changed without leaving
24469
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
24470
+ * empty session) leave it fresh: a record with no data is not a session.
24471
+ */
24472
+ isFresh() {
24473
+ if (this.stored) return false;
24474
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
24475
+ }
24454
24476
  /**
24455
24477
  * Get the current session ID.
24456
24478
  */
@@ -24513,7 +24535,7 @@ var init_session = __esm({
24513
24535
  */
24514
24536
  save() {
24515
24537
  if (!this.sessionId || !this.data) return true;
24516
- if (!this.dirty && !this.stored) return true;
24538
+ if (this.isFresh()) return true;
24517
24539
  let record = this.data;
24518
24540
  if (this.stored) {
24519
24541
  const current = this.safeRead(this.sessionId);
@@ -24700,7 +24722,7 @@ function errorLabel(err) {
24700
24722
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
24701
24723
  }
24702
24724
  async function sessionAutoStart(rawReq, rawRes, req2) {
24703
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24725
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24704
24726
  const cookieHeader = rawReq.headers.cookie ?? "";
24705
24727
  const cookiePrefix = sessionCookieName2() + "=";
24706
24728
  let existingSid;
@@ -24739,13 +24761,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
24739
24761
  } catch {
24740
24762
  }
24741
24763
  }
24742
- const newSid = sess.sessionId ?? sess.getSessionId?.();
24743
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
24744
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
24745
- const xfProto = rawReq.headers["x-forwarded-proto"];
24746
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
24747
- const socketEncrypted = rawReq.socket?.encrypted === true;
24748
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
24764
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
24765
+ if (sessionCookie !== null) {
24766
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
24749
24767
  }
24750
24768
  return origEnd(...args);
24751
24769
  };
@@ -23857,6 +23857,7 @@ __export(session_exports, {
23857
23857
  isSecureScheme: () => isSecureScheme,
23858
23858
  isValidSessionId: () => isValidSessionId,
23859
23859
  sessionCookieName: () => sessionCookieName,
23860
+ sessionCookieToSend: () => sessionCookieToSend,
23860
23861
  sessionStrictMode: () => sessionStrictMode
23861
23862
  });
23862
23863
  import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
@@ -23917,6 +23918,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
23917
23918
  parts.push(`Max-Age=${ttl}`);
23918
23919
  return parts.join("; ");
23919
23920
  }
23921
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
23922
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
23923
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
23924
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
23925
+ const xfProto = rawReq.headers["x-forwarded-proto"];
23926
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
23927
+ const socketEncrypted = rawReq.socket?.encrypted === true;
23928
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
23929
+ }
23920
23930
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
23921
23931
  var init_session = __esm({
23922
23932
  "src/session.ts"() {
@@ -24279,7 +24289,6 @@ var init_session = __esm({
24279
24289
  const now = Math.floor(Date.now() / 1e3);
24280
24290
  this.data = { _created: now, _accessed: now };
24281
24291
  this.dirty = false;
24282
- this.safeWrite(this.sessionId, this.data, this.ttl);
24283
24292
  this.stored = false;
24284
24293
  this.loaded = /* @__PURE__ */ new Map();
24285
24294
  this.cleared = false;
@@ -24430,6 +24439,19 @@ var init_session = __esm({
24430
24439
  const result = this.flash(key);
24431
24440
  return result !== void 0 ? result : defaultValue;
24432
24441
  }
24442
+ /**
24443
+ * Whether this session is only an id: nothing has stored it and it holds no
24444
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
24445
+ * Such a session has no record and needs no cookie, so save() writes nothing
24446
+ * for it and the server sends no cookie for it. It stops being fresh when
24447
+ * something is stored in it. Calls that mark a session changed without leaving
24448
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
24449
+ * empty session) leave it fresh: a record with no data is not a session.
24450
+ */
24451
+ isFresh() {
24452
+ if (this.stored) return false;
24453
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
24454
+ }
24433
24455
  /**
24434
24456
  * Get the current session ID.
24435
24457
  */
@@ -24492,7 +24514,7 @@ var init_session = __esm({
24492
24514
  */
24493
24515
  save() {
24494
24516
  if (!this.sessionId || !this.data) return true;
24495
- if (!this.dirty && !this.stored) return true;
24517
+ if (this.isFresh()) return true;
24496
24518
  let record = this.data;
24497
24519
  if (this.stored) {
24498
24520
  const current = this.safeRead(this.sessionId);
@@ -24679,7 +24701,7 @@ function errorLabel(err) {
24679
24701
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
24680
24702
  }
24681
24703
  async function sessionAutoStart(rawReq, rawRes, req2) {
24682
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24704
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
24683
24705
  const cookieHeader = rawReq.headers.cookie ?? "";
24684
24706
  const cookiePrefix = sessionCookieName2() + "=";
24685
24707
  let existingSid;
@@ -24718,13 +24740,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
24718
24740
  } catch {
24719
24741
  }
24720
24742
  }
24721
- const newSid = sess.sessionId ?? sess.getSessionId?.();
24722
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
24723
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
24724
- const xfProto = rawReq.headers["x-forwarded-proto"];
24725
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
24726
- const socketEncrypted = rawReq.socket?.encrypted === true;
24727
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
24743
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
24744
+ if (sessionCookie !== null) {
24745
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
24728
24746
  }
24729
24747
  return origEnd(...args);
24730
24748
  };
@@ -367,7 +367,7 @@ export async function sessionAutoStart(
367
367
  rawRes: ServerResponse,
368
368
  req: Tina4Request,
369
369
  ): Promise<void> {
370
- const { Session, buildSessionCookie, sessionCookieName, sessionStrictMode } =
370
+ const { Session, sessionCookieToSend, sessionCookieName, sessionStrictMode } =
371
371
  await import("./session.js");
372
372
  const cookieHeader = rawReq.headers.cookie ?? "";
373
373
 
@@ -433,16 +433,8 @@ export async function sessionAutoStart(
433
433
  try { sess.gc(); } catch { /* GC failure is non-critical */ }
434
434
  }
435
435
 
436
- const newSid = (sess as any).sessionId ?? (sess as any).getSessionId?.();
437
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
438
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
439
- // Thread the client's real scheme in so an HTTPS deploy behind a
440
- // TLS-terminating proxy ships the session cookie with `Secure`
441
- // (nodejs#34). `x-forwarded-proto` is the same header request.ts trusts
442
- // for URL construction; native socket TLS is the fallback.
443
- const xfProto = rawReq.headers["x-forwarded-proto"];
444
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
445
- const socketEncrypted = (rawReq.socket as { encrypted?: boolean })?.encrypted === true;
436
+ const sessionCookie = sessionCookieToSend(sess, existingSid, rawReq, rawRes);
437
+ if (sessionCookie !== null) {
446
438
  // appendHeader, not setHeader (feature 131 fix, found while proving
447
439
  // TC-DEC-02): setHeader REPLACES any existing Set-Cookie value wholesale,
448
440
  // so a route that had already called response.cookie() of its own — on
@@ -452,7 +444,7 @@ export async function sessionAutoStart(
452
444
  // appendHeader adds to whatever is already there (promoting a scalar to
453
445
  // an array, extending an existing array) and behaves exactly like
454
446
  // setHeader when nothing is set yet, so the common case is unchanged.
455
- rawRes.appendHeader("Set-Cookie", buildSessionCookie(newSid, ttl, undefined, forwardedProto, socketEncrypted));
447
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
456
448
  }
457
449
  return origEnd(...args);
458
450
  } as typeof rawRes.end;
@@ -610,12 +610,14 @@ export class Session {
610
610
  }
611
611
  }
612
612
 
613
- // Generate new session
613
+ // Generate new session. Nothing is written for it yet: a session nobody
614
+ // writes to has nothing to keep, and writing it here stored one record for
615
+ // every request that arrived without a cookie (static files, 404s and
616
+ // /health included). Its first save() after a change writes it whole.
614
617
  this.sessionId = randomBytes(16).toString("hex");
615
618
  const now = Math.floor(Date.now() / 1000);
616
619
  this.data = { _created: now, _accessed: now };
617
620
  this.dirty = false;
618
- this.safeWrite(this.sessionId, this.data, this.ttl);
619
621
  this.stored = false;
620
622
  this.loaded = new Map();
621
623
  this.cleared = false;
@@ -785,6 +787,20 @@ export class Session {
785
787
  return result !== undefined ? result : defaultValue;
786
788
  }
787
789
 
790
+ /**
791
+ * Whether this session is only an id: nothing has stored it and it holds no
792
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
793
+ * Such a session has no record and needs no cookie, so save() writes nothing
794
+ * for it and the server sends no cookie for it. It stops being fresh when
795
+ * something is stored in it. Calls that mark a session changed without leaving
796
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
797
+ * empty session) leave it fresh: a record with no data is not a session.
798
+ */
799
+ isFresh(): boolean {
800
+ if (this.stored) return false;
801
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
802
+ }
803
+
788
804
  /**
789
805
  * Get the current session ID.
790
806
  */
@@ -850,7 +866,7 @@ export class Session {
850
866
  */
851
867
  save(): boolean {
852
868
  if (!this.sessionId || !this.data) return true;
853
- if (!this.dirty && !this.stored) return true;
869
+ if (this.isFresh()) return true;
854
870
  let record: SessionData = this.data;
855
871
  if (this.stored) {
856
872
  const current = this.safeRead(this.sessionId);
@@ -1018,3 +1034,29 @@ export function buildSessionCookie(
1018
1034
  parts.push(`Max-Age=${ttl}`);
1019
1035
  return parts.join("; ");
1020
1036
  }
1037
+
1038
+ /**
1039
+ * The Set-Cookie value that hands the client this request's session, or null
1040
+ * when none is owed: the client already holds the id, headers have gone out, or
1041
+ * the session is fresh. A fresh session (only an id: minted for this request
1042
+ * and holding no data) was not stored, so a cookie for it would name nothing:
1043
+ * the next request could not resume it and would be handed another one.
1044
+ */
1045
+ export function sessionCookieToSend(
1046
+ sess: { isFresh(): boolean },
1047
+ existingSid: string | undefined,
1048
+ rawReq: { headers: Record<string, string | string[] | undefined>; socket?: unknown },
1049
+ rawRes: { headersSent: boolean },
1050
+ ): string | null {
1051
+ const newSid = (sess as any).sessionId ?? (sess as any).getSessionId?.();
1052
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
1053
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
1054
+ // Thread the client's real scheme in so an HTTPS deploy behind a
1055
+ // TLS-terminating proxy ships the session cookie with `Secure`
1056
+ // (nodejs#34). `x-forwarded-proto` is the same header request.ts trusts
1057
+ // for URL construction; native socket TLS is the fallback.
1058
+ const xfProto = rawReq.headers["x-forwarded-proto"];
1059
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
1060
+ const socketEncrypted = (rawReq.socket as { encrypted?: boolean } | undefined)?.encrypted === true;
1061
+ return buildSessionCookie(newSid, ttl, undefined, forwardedProto, socketEncrypted);
1062
+ }
@@ -11833,6 +11833,7 @@ __export(session_exports, {
11833
11833
  isSecureScheme: () => isSecureScheme,
11834
11834
  isValidSessionId: () => isValidSessionId,
11835
11835
  sessionCookieName: () => sessionCookieName,
11836
+ sessionCookieToSend: () => sessionCookieToSend,
11836
11837
  sessionStrictMode: () => sessionStrictMode
11837
11838
  });
11838
11839
  import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
@@ -11893,6 +11894,15 @@ function buildSessionCookie(sessionId, ttl, cookieName, forwardedProto, socketEn
11893
11894
  parts.push(`Max-Age=${ttl}`);
11894
11895
  return parts.join("; ");
11895
11896
  }
11897
+ function sessionCookieToSend(sess, existingSid, rawReq, rawRes) {
11898
+ const newSid = sess.sessionId ?? sess.getSessionId?.();
11899
+ if (!newSid || newSid === existingSid || sess.isFresh() || rawRes.headersSent) return null;
11900
+ const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
11901
+ const xfProto = rawReq.headers["x-forwarded-proto"];
11902
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
11903
+ const socketEncrypted = rawReq.socket?.encrypted === true;
11904
+ return buildSessionCookie(newSid, ttl, void 0, forwardedProto, socketEncrypted);
11905
+ }
11896
11906
  var SESSION_ID_PATTERN, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS, RETIRED_SESSION_BACKENDS, FileSessionHandler, RedisSessionHandler, FLASH_PREFIX, Session;
11897
11907
  var init_session = __esm({
11898
11908
  "../core/src/session.ts"() {
@@ -12255,7 +12265,6 @@ var init_session = __esm({
12255
12265
  const now = Math.floor(Date.now() / 1e3);
12256
12266
  this.data = { _created: now, _accessed: now };
12257
12267
  this.dirty = false;
12258
- this.safeWrite(this.sessionId, this.data, this.ttl);
12259
12268
  this.stored = false;
12260
12269
  this.loaded = /* @__PURE__ */ new Map();
12261
12270
  this.cleared = false;
@@ -12406,6 +12415,19 @@ var init_session = __esm({
12406
12415
  const result = this.flash(key);
12407
12416
  return result !== void 0 ? result : defaultValue;
12408
12417
  }
12418
+ /**
12419
+ * Whether this session is only an id: nothing has stored it and it holds no
12420
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
12421
+ * Such a session has no record and needs no cookie, so save() writes nothing
12422
+ * for it and the server sends no cookie for it. It stops being fresh when
12423
+ * something is stored in it. Calls that mark a session changed without leaving
12424
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
12425
+ * empty session) leave it fresh: a record with no data is not a session.
12426
+ */
12427
+ isFresh() {
12428
+ if (this.stored) return false;
12429
+ return !this.data || Object.keys(this.data).every((k) => k === "_created" || k === "_accessed");
12430
+ }
12409
12431
  /**
12410
12432
  * Get the current session ID.
12411
12433
  */
@@ -12468,7 +12490,7 @@ var init_session = __esm({
12468
12490
  */
12469
12491
  save() {
12470
12492
  if (!this.sessionId || !this.data) return true;
12471
- if (!this.dirty && !this.stored) return true;
12493
+ if (this.isFresh()) return true;
12472
12494
  let record = this.data;
12473
12495
  if (this.stored) {
12474
12496
  const current = this.safeRead(this.sessionId);
@@ -12655,7 +12677,7 @@ function errorLabel(err) {
12655
12677
  return err instanceof Error ? `${err.name}: ${err.message}` : String(err);
12656
12678
  }
12657
12679
  async function sessionAutoStart(rawReq, rawRes, req2) {
12658
- const { Session: Session2, buildSessionCookie: buildSessionCookie2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
12680
+ const { Session: Session2, sessionCookieToSend: sessionCookieToSend2, sessionCookieName: sessionCookieName2, sessionStrictMode: sessionStrictMode2 } = await Promise.resolve().then(() => (init_session(), session_exports));
12659
12681
  const cookieHeader = rawReq.headers.cookie ?? "";
12660
12682
  const cookiePrefix = sessionCookieName2() + "=";
12661
12683
  let existingSid;
@@ -12694,13 +12716,9 @@ async function sessionAutoStart(rawReq, rawRes, req2) {
12694
12716
  } catch {
12695
12717
  }
12696
12718
  }
12697
- const newSid = sess.sessionId ?? sess.getSessionId?.();
12698
- if (newSid && newSid !== existingSid && !rawRes.headersSent) {
12699
- const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
12700
- const xfProto = rawReq.headers["x-forwarded-proto"];
12701
- const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
12702
- const socketEncrypted = rawReq.socket?.encrypted === true;
12703
- rawRes.appendHeader("Set-Cookie", buildSessionCookie2(newSid, ttl, void 0, forwardedProto, socketEncrypted));
12719
+ const sessionCookie = sessionCookieToSend2(sess, existingSid, rawReq, rawRes);
12720
+ if (sessionCookie !== null) {
12721
+ rawRes.appendHeader("Set-Cookie", sessionCookie);
12704
12722
  }
12705
12723
  return origEnd(...args);
12706
12724
  };
@@ -258,6 +258,16 @@ export declare class Session {
258
258
  * Get flash data by key (alias for flash(key) without value).
259
259
  */
260
260
  getFlash(key: string, defaultValue?: unknown): unknown;
261
+ /**
262
+ * Whether this session is only an id: nothing has stored it and it holds no
263
+ * data of its own (its `_created` / `_accessed` bookkeeping does not count).
264
+ * Such a session has no record and needs no cookie, so save() writes nothing
265
+ * for it and the server sends no cookie for it. It stops being fresh when
266
+ * something is stored in it. Calls that mark a session changed without leaving
267
+ * anything in it (deleting a key it never had, clear(), regenerate() on an
268
+ * empty session) leave it fresh: a record with no data is not a session.
269
+ */
270
+ isFresh(): boolean;
261
271
  /**
262
272
  * Get the current session ID.
263
273
  */
@@ -388,4 +398,19 @@ export declare function sessionStrictMode(): boolean;
388
398
  * http://localhost dev cookies undeliverable.
389
399
  */
390
400
  export declare function buildSessionCookie(sessionId: string | null, ttl: number, cookieName?: string, forwardedProto?: string, socketEncrypted?: boolean): string;
401
+ /**
402
+ * The Set-Cookie value that hands the client this request's session, or null
403
+ * when none is owed: the client already holds the id, headers have gone out, or
404
+ * the session is fresh. A fresh session (only an id: minted for this request
405
+ * and holding no data) was not stored, so a cookie for it would name nothing:
406
+ * the next request could not resume it and would be handed another one.
407
+ */
408
+ export declare function sessionCookieToSend(sess: {
409
+ isFresh(): boolean;
410
+ }, existingSid: string | undefined, rawReq: {
411
+ headers: Record<string, string | string[] | undefined>;
412
+ socket?: unknown;
413
+ }, rawRes: {
414
+ headersSent: boolean;
415
+ }): string | null;
391
416
  export {};