tina4-nodejs 3.13.78 → 3.13.81

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
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.78)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.81)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.13.78 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.81 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.78",
3
+ "version": "3.13.81",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -48,7 +48,7 @@ export {
48
48
  ensureDevSecret,
49
49
  Auth,
50
50
  } from "./auth.js";
51
- export { Session, FileSessionHandler, RedisSessionHandler, buildSessionCookie } from "./session.js";
51
+ export { Session, FileSessionHandler, RedisSessionHandler, buildSessionCookie, isSecureScheme, sessionCookieName } from "./session.js";
52
52
  export type { SessionConfig, SessionHandler } from "./session.js";
53
53
  export { I18n } from "./i18n.js";
54
54
  export { FakeData } from "./fakeData.js";
@@ -1102,13 +1102,25 @@ ${reset}
1102
1102
 
1103
1103
  // Auto-start session — read cookie, create session, save + set cookie on response end
1104
1104
  {
1105
- const { Session, buildSessionCookie } = await import("./session.js");
1105
+ const { Session, buildSessionCookie, sessionCookieName } = await import("./session.js");
1106
1106
  const cookieHeader = rawReq.headers.cookie ?? "";
1107
- const cookieName = process.env.TINA4_SESSION_NAME ?? "tina4_session";
1108
- // Build a regex from the (possibly customised) cookie name. Escape regex meta-chars.
1109
- const escapedName = cookieName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1110
- const sidMatch = cookieHeader.match(new RegExp(`${escapedName}=([^;]+)`));
1111
- const existingSid = sidMatch ? sidMatch[1] : undefined;
1107
+ // Read the incoming session cookie by the SAME configured name the write
1108
+ // side emits (TINA4_SESSION_NAME, default tina4_session) via the shared
1109
+ // sessionCookieName() resolver — otherwise a renamed cookie would be
1110
+ // written but never read back and the session would silently never
1111
+ // resume. Match a whole cookie pair by its exact `name=` prefix (split on
1112
+ // ";", trim, startsWith) so `tina4_session` never matches
1113
+ // `tina4_session_foo=` nor a value mid-header. Parity with Python
1114
+ // core/server._init_session.
1115
+ const cookiePrefix = sessionCookieName() + "=";
1116
+ let existingSid: string | undefined;
1117
+ for (const part of cookieHeader.split(";")) {
1118
+ const trimmed = part.trim();
1119
+ if (trimmed.startsWith(cookiePrefix)) {
1120
+ existingSid = trimmed.slice(cookiePrefix.length);
1121
+ break;
1122
+ }
1123
+ }
1112
1124
  const sess = new Session();
1113
1125
  sess.start(existingSid);
1114
1126
  (req as any).session = sess;
@@ -1125,7 +1137,14 @@ ${reset}
1125
1137
  const newSid = (sess as any).sessionId ?? (sess as any).getSessionId?.();
1126
1138
  if (newSid && newSid !== existingSid && !rawRes.headersSent) {
1127
1139
  const ttl = parseInt(process.env.TINA4_SESSION_TTL ?? "3600", 10);
1128
- rawRes.setHeader("Set-Cookie", buildSessionCookie(newSid, ttl));
1140
+ // Thread the client's real scheme in so an HTTPS deploy behind a
1141
+ // TLS-terminating proxy ships the session cookie with `Secure`
1142
+ // (nodejs#34). `x-forwarded-proto` is the same header request.ts
1143
+ // trusts for URL construction; native socket TLS is the fallback.
1144
+ const xfProto = rawReq.headers["x-forwarded-proto"];
1145
+ const forwardedProto = Array.isArray(xfProto) ? xfProto[0] : xfProto;
1146
+ const socketEncrypted = (rawReq.socket as { encrypted?: boolean })?.encrypted === true;
1147
+ rawRes.setHeader("Set-Cookie", buildSessionCookie(newSid, ttl, undefined, forwardedProto, socketEncrypted));
1129
1148
  }
1130
1149
  return origEnd(...args);
1131
1150
  } as typeof rawRes.end;
@@ -589,6 +589,51 @@ export class Session {
589
589
  }
590
590
  }
591
591
 
592
+ /**
593
+ * Is the client's scheme HTTPS? Proxy-aware.
594
+ *
595
+ * TLS is normally terminated at a proxy (nginx, HAProxy, ALB, Cloudflare, most
596
+ * container deploys) which then forwards plain HTTP to Node — so the native
597
+ * socket is NOT encrypted on exactly the deployments that ARE https, and it
598
+ * cannot be the only signal. `x-forwarded-proto` carries the scheme the client
599
+ * actually used; a chain of proxies appends each hop ("https, http") and the
600
+ * FIRST is the client-facing one, which is the scheme the browser used.
601
+ *
602
+ * Parity with PHP `Request::isSecureScheme` (tina4-php#175). Spoofable when the
603
+ * app is directly reachable, but the failure mode is self-limiting: a spoofed
604
+ * `https` only makes the cookie MORE restrictive, and `request.ts` already
605
+ * trusts the same header for URL construction — honouring it here is consistent.
606
+ *
607
+ * @param forwardedProto Raw `x-forwarded-proto` value (or a resolved scheme like
608
+ * "https"/"http"); "" / undefined means "absent".
609
+ * @param socketEncrypted True when Node terminated TLS itself (direct https, no
610
+ * proxy) — the native fallback when no forwarded header.
611
+ */
612
+ export function isSecureScheme(forwardedProto?: string, socketEncrypted?: boolean): boolean {
613
+ const forwarded = (forwardedProto ?? "").trim();
614
+ if (forwarded !== "") {
615
+ return forwarded.split(",")[0].trim().toLowerCase() === "https";
616
+ }
617
+ return socketEncrypted === true;
618
+ }
619
+
620
+ /**
621
+ * Resolve the session cookie name — the single source of truth shared by the
622
+ * WRITE side (`buildSessionCookie` / `Session.cookieHeader`) and the READ side
623
+ * (the auto-session cookie parse in `server.ts`), so a cookie written under a
624
+ * renamed name is read back on the next request.
625
+ *
626
+ * TINA4_SESSION_NAME Cookie name (default: "tina4_session")
627
+ *
628
+ * Keeping this in one place means the default can never drift between the two
629
+ * sides: an operator who sets `TINA4_SESSION_NAME` renames the cookie on both
630
+ * the emit and the parse paths at once. Parity with Python
631
+ * `session.session_cookie_name()`.
632
+ */
633
+ export function sessionCookieName(): string {
634
+ return process.env.TINA4_SESSION_NAME ?? "tina4_session";
635
+ }
636
+
592
637
  /**
593
638
  * Build the `Set-Cookie` header value for a Tina4 session. Centralised so
594
639
  * the auto-cookie path in server.ts and `Session.cookieHeader()` agree on
@@ -598,10 +643,25 @@ export class Session {
598
643
  * TINA4_SESSION_NAME — cookie name (default: "tina4_session")
599
644
  * TINA4_SESSION_SAMESITE — SameSite attribute (default: "Lax")
600
645
  * TINA4_SESSION_HTTPONLY — emit HttpOnly (default: true)
601
- * TINA4_SESSION_SECURE — emit Secure (default: false)
646
+ * TINA4_SESSION_SECURE — emit Secure (default: false; SameSite=None forces it on)
647
+ *
648
+ * `Secure` is emitted when ANY of: TINA4_SESSION_SECURE is truthy; SameSite is
649
+ * `None` (browsers reject a None cookie without Secure); OR the request scheme
650
+ * is https, detected proxy-aware from `forwardedProto` / `socketEncrypted`. The
651
+ * auto-cookie path in server.ts threads the request's scheme in so an HTTPS
652
+ * deploy behind a TLS-terminating proxy ships Secure without the operator
653
+ * having to know about TINA4_SESSION_SECURE (nodejs#34). Plain HTTP with no
654
+ * proxy header and no native TLS stays NOT Secure — an eager Secure would make
655
+ * http://localhost dev cookies undeliverable.
602
656
  */
603
- export function buildSessionCookie(sessionId: string | null, ttl: number, cookieName?: string): string {
604
- const name = cookieName ?? process.env.TINA4_SESSION_NAME ?? "tina4_session";
657
+ export function buildSessionCookie(
658
+ sessionId: string | null,
659
+ ttl: number,
660
+ cookieName?: string,
661
+ forwardedProto?: string,
662
+ socketEncrypted?: boolean,
663
+ ): string {
664
+ const name = cookieName ?? sessionCookieName();
605
665
  const sameSite = process.env.TINA4_SESSION_SAMESITE ?? "Lax";
606
666
 
607
667
  // HttpOnly defaults to TRUE (matches existing behaviour and Python parity).
@@ -611,10 +671,10 @@ export function buildSessionCookie(sessionId: string | null, ttl: number, cookie
611
671
  ? true
612
672
  : !["false", "0", "no", "off"].includes(httpOnlyRaw.trim().toLowerCase());
613
673
 
614
- // Secure defaults to FALSE only emit when the operator opts in (https
615
- // deployments). Setting it eagerly would break http://localhost dev cookies.
616
- const secureRaw = process.env.TINA4_SESSION_SECURE ?? "";
617
- const secure = ["true", "1", "yes", "on"].includes(secureRaw.trim().toLowerCase());
674
+ // Secure defaults to FALSE, then turns ON for any of the three signals above.
675
+ const secure = isTruthy(process.env.TINA4_SESSION_SECURE)
676
+ || sameSite.trim().toLowerCase() === "none"
677
+ || isSecureScheme(forwardedProto, socketEncrypted);
618
678
 
619
679
  const parts = [`${name}=${sessionId ?? ""}`, "Path=/"];
620
680
  if (httpOnly) parts.push("HttpOnly");