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 +2 -2
- package/package.json +1 -1
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/server.ts +26 -7
- package/packages/core/src/session.ts +67 -7
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
1108
|
-
//
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
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(
|
|
604
|
-
|
|
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
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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");
|