tina4-nodejs 3.13.94 → 3.13.96
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 +158 -30
- package/README.md +1 -1
- package/package.json +3 -1
- package/packages/cli/dist/bin.js +30911 -28444
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +30810 -28261
- package/packages/core/public/css/tina4.min.css +1 -1
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +6 -7
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +294 -106
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +34 -16
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +886 -421
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -147
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/orm/dist/index.js +22717 -20168
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +60 -24
- package/packages/orm/src/autoCrud.ts +12 -10
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +241 -197
- package/packages/orm/src/databaseResult.ts +51 -28
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -6
- package/packages/orm/src/migration.ts +44 -11
- package/packages/orm/src/model.ts +4 -0
- package/packages/orm/src/queryBuilder.ts +47 -6
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +21 -77
- package/packages/swagger/dist/index.js +78 -20
- package/packages/swagger/src/generator.ts +172 -29
- package/types/core/src/ai.d.ts +1 -1
- package/types/core/src/auth.d.ts +28 -5
- package/types/core/src/background.d.ts +3 -3
- package/types/core/src/cache.d.ts +15 -12
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/dotenv.d.ts +38 -16
- package/types/core/src/index.d.ts +6 -9
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +47 -6
- package/types/core/src/metrics.d.ts +25 -61
- package/types/core/src/middleware.d.ts +134 -11
- package/types/core/src/queue.d.ts +54 -5
- package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
- package/types/core/src/router.d.ts +14 -3
- package/types/core/src/server.d.ts +15 -4
- package/types/core/src/session.d.ts +87 -2
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
- package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +28 -5
- package/types/core/src/websocket.d.ts +26 -0
- package/types/orm/src/adapters/firebird.d.ts +55 -10
- package/types/orm/src/adapters/mongodb.d.ts +2 -2
- package/types/orm/src/adapters/mssql.d.ts +18 -11
- package/types/orm/src/adapters/mysql.d.ts +11 -10
- package/types/orm/src/adapters/odbc.d.ts +9 -12
- package/types/orm/src/adapters/postgres.d.ts +11 -10
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +15 -3
- package/types/orm/src/baseModel.d.ts +45 -9
- package/types/orm/src/cachedDatabase.d.ts +18 -5
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +78 -28
- package/types/orm/src/databaseResult.d.ts +29 -15
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +102 -43
- package/types/orm/src/index.d.ts +6 -4
- package/types/orm/src/migration.d.ts +4 -3
- package/types/orm/src/queryBuilder.d.ts +23 -3
- package/types/orm/src/sqlTranslator.d.ts +126 -2
- package/types/orm/src/types.d.ts +21 -38
- package/packages/core/src/scss.ts +0 -623
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/scss.d.ts +0 -19
- package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
|
@@ -24,21 +24,21 @@
|
|
|
24
24
|
* session.get("user"); // { name: "Alice" }
|
|
25
25
|
* session.destroy();
|
|
26
26
|
*/
|
|
27
|
-
import { randomBytes } from "node:crypto";
|
|
27
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
28
28
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from "node:fs";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { Log } from "./logger.js";
|
|
31
31
|
import { isTruthy } from "./dotenv.js";
|
|
32
32
|
import { respCommandSync } from "./sessionHandlers/respClient.js";
|
|
33
|
-
import { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
|
|
34
33
|
import { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
|
|
34
|
+
import { MemcachedSessionHandler } from "./sessionHandlers/memcachedHandler.js";
|
|
35
35
|
import { MongoSessionHandler } from "./sessionHandlers/mongoHandler.js";
|
|
36
36
|
import { DatabaseSessionHandler } from "./sessionHandlers/databaseHandler.js";
|
|
37
37
|
|
|
38
38
|
// ── Types ─────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
40
|
export interface SessionConfig {
|
|
41
|
-
/** Session backend type: "file", "redis", "valkey", "mongo", "database" (or "db") */
|
|
41
|
+
/** Session backend type: "file", "redis", "valkey", "mongo", "memcached", "database" (or "db") */
|
|
42
42
|
backend?: string;
|
|
43
43
|
/** File storage path (default: "data/sessions") */
|
|
44
44
|
path?: string;
|
|
@@ -62,6 +62,104 @@ interface SessionData {
|
|
|
62
62
|
[key: string]: unknown;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ── Session id validation ─────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A session id is OPAQUE — an unguessable lookup token and nothing else. It is
|
|
69
|
+
* never a filename, a path, a SQL fragment or a Redis key fragment, so the only
|
|
70
|
+
* characters it may contain are the ones every backend treats as inert.
|
|
71
|
+
*
|
|
72
|
+
* The alphabet is the RFC 4648 base64url set, which is exactly what all four
|
|
73
|
+
* frameworks already mint: Python `secrets.token_urlsafe(32)`, Ruby
|
|
74
|
+
* `SecureRandom.hex(32)`, PHP/Node `hex(16)`. Validation is therefore
|
|
75
|
+
* non-breaking for every id the family has ever issued, while rejecting the
|
|
76
|
+
* `.` and `/` that turn a cookie into a path traversal.
|
|
77
|
+
*
|
|
78
|
+
* The rule is the ALPHABET, not the length. The vulnerability was `.` and `/`
|
|
79
|
+
* turning a cookie into a path; entropy is not something this check can supply,
|
|
80
|
+
* because unguessability comes from the framework's own minting and an app that
|
|
81
|
+
* calls `start("my-session-id")` is a trusted caller managing its own id, not an
|
|
82
|
+
* attacker. So the floor is 1, and only the 128-character ceiling remains — it
|
|
83
|
+
* bounds what an attacker can push through a backend key.
|
|
84
|
+
*/
|
|
85
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Is `sessionId` a well-formed opaque session identifier?
|
|
89
|
+
*
|
|
90
|
+
* Callers pass UNTRUSTED input here (the session cookie is attacker-chosen), so
|
|
91
|
+
* anything that is not a string of the opaque alphabet is rejected.
|
|
92
|
+
*/
|
|
93
|
+
export function isValidSessionId(sessionId: unknown): boolean {
|
|
94
|
+
return typeof sessionId === "string" && SESSION_ID_PATTERN.test(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Backend name resolution ───────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every accepted backend name, aliases included. Byte-identical membership in
|
|
101
|
+
* all four frameworks. Written once here so the switch below and the error
|
|
102
|
+
* message cannot disagree.
|
|
103
|
+
*/
|
|
104
|
+
export const VALID_SESSION_BACKENDS = [
|
|
105
|
+
"file", "filesystem",
|
|
106
|
+
"redis",
|
|
107
|
+
"valkey",
|
|
108
|
+
"mongodb", "mongo",
|
|
109
|
+
"memcached", "memcache",
|
|
110
|
+
"database", "db",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
/** Canonical name of each backend, for the error message (aliases omitted). */
|
|
114
|
+
export const CANONICAL_SESSION_BACKENDS = [
|
|
115
|
+
"file", "redis", "valkey", "mongodb", "memcached", "database",
|
|
116
|
+
] as const;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Names that USED to work, mapped to the message a caller needs to migrate.
|
|
120
|
+
*
|
|
121
|
+
* A retired name must not fall into the generic "unknown backend" message: the
|
|
122
|
+
* operator had a working config, and telling them what replaced it is the whole
|
|
123
|
+
* point. Checked before the membership test so the specific message wins.
|
|
124
|
+
*/
|
|
125
|
+
const RETIRED_SESSION_BACKENDS: Record<string, string> = {
|
|
126
|
+
"redis-npm":
|
|
127
|
+
'TINA4_SESSION_BACKEND="redis-npm" was removed on 2026-07-31. Use "redis": '
|
|
128
|
+
+ "it is the same Redis backend and reads the same TINA4_SESSION_REDIS_* "
|
|
129
|
+
+ "settings, over a faster persistent connection.",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Normalise a backend name and reject anything unrecognised.
|
|
134
|
+
*
|
|
135
|
+
* An UNKNOWN name used to fall through to `default:` in the switch below, which
|
|
136
|
+
* is the file handler. A typo in TINA4_SESSION_BACKEND ("redsi") - or, before
|
|
137
|
+
* this normalisation existed, merely a capital ("Redis") - produced a running
|
|
138
|
+
* app writing sessions to local disk while the operator believed they were in
|
|
139
|
+
* Redis. Nothing logged, nothing failed, and the symptom surfaced much later as
|
|
140
|
+
* users being logged out whenever a request landed on another instance.
|
|
141
|
+
*
|
|
142
|
+
* A BLANK name still means file. An env var set to "" is a SET variable, so it
|
|
143
|
+
* never reaches the `??` default; rejecting blank would break every deployment
|
|
144
|
+
* that clears the var to take the default.
|
|
145
|
+
*/
|
|
146
|
+
function resolveBackend(name: string): string {
|
|
147
|
+
const normalised = String(name).trim().toLowerCase();
|
|
148
|
+
if (normalised === "") return "file";
|
|
149
|
+
|
|
150
|
+
const retired = RETIRED_SESSION_BACKENDS[normalised];
|
|
151
|
+
if (retired) throw new Error(retired);
|
|
152
|
+
|
|
153
|
+
if (!(VALID_SESSION_BACKENDS as readonly string[]).includes(normalised)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Unknown session backend "${normalised}". `
|
|
156
|
+
+ `Valid backends: ${CANONICAL_SESSION_BACKENDS.join(", ")}. `
|
|
157
|
+
+ "Leave TINA4_SESSION_BACKEND unset for the file default.",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return normalised;
|
|
161
|
+
}
|
|
162
|
+
|
|
65
163
|
// ── Session Handler Interface ─────────────────────────────────────
|
|
66
164
|
|
|
67
165
|
/**
|
|
@@ -93,8 +191,38 @@ export class FileSessionHandler implements SessionHandler {
|
|
|
93
191
|
}
|
|
94
192
|
}
|
|
95
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Derive the file backing a session id. TWO independent guards, both required.
|
|
196
|
+
*
|
|
197
|
+
* This is the one place a session id becomes a filesystem path, and it used to
|
|
198
|
+
* interpolate the id RAW: `join(storagePath, "../../OUTSIDE/appconfig.json")`
|
|
199
|
+
* left the session directory entirely, so a cookie could read an existing
|
|
200
|
+
* .json from anywhere on disk into `session.all()` and then OVERWRITE it on
|
|
201
|
+
* save. Reproduced on Node 24.9.0 / macOS.
|
|
202
|
+
*
|
|
203
|
+
* 1. VALIDATE — a malformed id is refused outright. It throws rather than
|
|
204
|
+
* returning null so a hostile id can never be mistaken for an ordinary
|
|
205
|
+
* cache miss (the Session layer catches it, logs it, and degrades).
|
|
206
|
+
* 2. HASH — the filename is a SHA-256 of the id, matching the Python master
|
|
207
|
+
* (`hashlib.sha256(session_id.encode()).hexdigest()`). A hex digest cannot
|
|
208
|
+
* contain a separator or a dot, so even an id that somehow passed the
|
|
209
|
+
* validator can only ever name a file inside `storagePath`.
|
|
210
|
+
*
|
|
211
|
+
* DEPLOY NOTE: hashing CHANGES the filename for every id, so existing on-disk
|
|
212
|
+
* sessions are orphaned and every logged-in user is logged out ONCE on the
|
|
213
|
+
* deploy that ships this. That is accepted: under strict session mode an old
|
|
214
|
+
* cookie is discarded on a read miss anyway, so the sessions were going to be
|
|
215
|
+
* dropped regardless.
|
|
216
|
+
*/
|
|
96
217
|
private filePath(id: string): string {
|
|
97
|
-
|
|
218
|
+
if (!isValidSessionId(id)) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Invalid session id ${JSON.stringify(id)} — a session id is opaque and must match `
|
|
221
|
+
+ `${SESSION_ID_PATTERN.source}. It is never a path, so it is refused rather than `
|
|
222
|
+
+ "resolved to a file.",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return join(this.storagePath, `${createHash("sha256").update(id).digest("hex")}.json`);
|
|
98
226
|
}
|
|
99
227
|
|
|
100
228
|
read(sessionId: string): SessionData | null {
|
|
@@ -264,12 +392,19 @@ export class Session {
|
|
|
264
392
|
* frameworks. Strict mode is the escape hatch (same as events/seeding).
|
|
265
393
|
*/
|
|
266
394
|
private strict: boolean;
|
|
395
|
+
/**
|
|
396
|
+
* True when the LAST backend read RAISED rather than returning a miss.
|
|
397
|
+
* Lets `start()` tell "no such session" from "the store is unreachable".
|
|
398
|
+
*/
|
|
399
|
+
private lastReadFailed = false;
|
|
267
400
|
|
|
268
401
|
constructor(backend?: string, config?: SessionConfig) {
|
|
269
|
-
const backendType =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
402
|
+
const backendType = resolveBackend(
|
|
403
|
+
backend
|
|
404
|
+
?? config?.backend
|
|
405
|
+
?? process.env.TINA4_SESSION_BACKEND
|
|
406
|
+
?? "file",
|
|
407
|
+
);
|
|
273
408
|
|
|
274
409
|
this.ttl = config?.ttl
|
|
275
410
|
?? (process.env.TINA4_SESSION_TTL ? parseInt(process.env.TINA4_SESSION_TTL, 10) : 3600);
|
|
@@ -281,10 +416,11 @@ export class Session {
|
|
|
281
416
|
case "redis":
|
|
282
417
|
this.handler = new RedisSessionHandler(config);
|
|
283
418
|
break;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
419
|
+
// `redis-npm` was RETIRED here on 2026-07-31 (a Node-only backend NAME for
|
|
420
|
+
// the optional `redis` npm driver, still running execFileSync per command).
|
|
421
|
+
// Its rejection now lives in RETIRED_SESSION_BACKENDS above, so the helpful
|
|
422
|
+
// migration message survives the generic unknown-name check rather than
|
|
423
|
+
// being swallowed by it.
|
|
288
424
|
case "valkey": {
|
|
289
425
|
this.handler = new ValkeySessionHandler(config);
|
|
290
426
|
break;
|
|
@@ -294,15 +430,30 @@ export class Session {
|
|
|
294
430
|
this.handler = new MongoSessionHandler(config);
|
|
295
431
|
break;
|
|
296
432
|
}
|
|
433
|
+
case "memcached":
|
|
434
|
+
case "memcache": {
|
|
435
|
+
this.handler = new MemcachedSessionHandler(config);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
297
438
|
case "database":
|
|
298
439
|
case "db": {
|
|
299
440
|
this.handler = new DatabaseSessionHandler(config);
|
|
300
441
|
break;
|
|
301
442
|
}
|
|
302
443
|
case "file":
|
|
303
|
-
|
|
444
|
+
case "filesystem":
|
|
304
445
|
this.handler = new FileSessionHandler(config?.path);
|
|
305
446
|
break;
|
|
447
|
+
default:
|
|
448
|
+
// Unreachable for a user's typo - resolveBackend already rejected it.
|
|
449
|
+
// Only a name that IS in VALID_SESSION_BACKENDS but has no case above can
|
|
450
|
+
// land here, which is a bug in this switch rather than a configuration
|
|
451
|
+
// error, so it must not be swallowed into a file handler either.
|
|
452
|
+
throw new Error(
|
|
453
|
+
`Session backend "${backendType}" is listed in VALID_SESSION_BACKENDS `
|
|
454
|
+
+ "but has no handler case. This is a framework bug, not a "
|
|
455
|
+
+ "configuration error.",
|
|
456
|
+
);
|
|
306
457
|
}
|
|
307
458
|
}
|
|
308
459
|
|
|
@@ -329,11 +480,23 @@ export class Session {
|
|
|
329
480
|
Log.error(`Session backend ${op} failed (${handlerName}): ${message}`);
|
|
330
481
|
}
|
|
331
482
|
|
|
332
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Read through the backend; on FAILURE log + degrade to empty (or re-throw
|
|
485
|
+
* under strict).
|
|
486
|
+
*
|
|
487
|
+
* Sets {@link lastReadFailed} so `start()` can tell "the store answered, and
|
|
488
|
+
* has no such session" from "the store did not answer at all". Strict mode
|
|
489
|
+
* must discard an id only on the first: treating an outage as an unknown id
|
|
490
|
+
* rotates the session id on EVERY request for the whole outage, logging the
|
|
491
|
+
* entire userbase out over one Redis blip and orphaning their stored
|
|
492
|
+
* sessions. The policy is log-loud + degrade, never rotate.
|
|
493
|
+
*/
|
|
333
494
|
private safeRead(sessionId: string): SessionData | null {
|
|
495
|
+
this.lastReadFailed = false;
|
|
334
496
|
try {
|
|
335
497
|
return this.handler.read(sessionId);
|
|
336
498
|
} catch (err) {
|
|
499
|
+
this.lastReadFailed = true;
|
|
337
500
|
this.logBackendError("read", err);
|
|
338
501
|
if (this.strict) throw err;
|
|
339
502
|
return null;
|
|
@@ -366,27 +529,66 @@ export class Session {
|
|
|
366
529
|
|
|
367
530
|
/**
|
|
368
531
|
* Start or resume a session.
|
|
532
|
+
*
|
|
533
|
+
* `sessionId` is UNTRUSTED — it arrives from the session cookie, which the
|
|
534
|
+
* client fully controls. An id that is not a well-formed opaque identifier is
|
|
535
|
+
* DISCARDED and a fresh one minted, never adopted: adopting it let a cookie
|
|
536
|
+
* steer a filesystem path (a `tina4_session=../../OUTSIDE/appconfig` cookie
|
|
537
|
+
* read an existing .json from outside the session directory into
|
|
538
|
+
* `session.all()`, then OVERWROTE it on save) and let an attacker pre-plant a
|
|
539
|
+
* session id that survived the victim's login (session fixation).
|
|
540
|
+
*
|
|
541
|
+
* The check runs BEFORE the read, so a hostile id never reaches a handler at
|
|
542
|
+
* all. A legitimate id from any of the four frameworks passes unchanged.
|
|
543
|
+
*
|
|
544
|
+
* STRICT SESSION MODE (deliberate, and Node is the family reference for it):
|
|
545
|
+
* an id that is WELL-FORMED but UNKNOWN to the backend is also discarded and
|
|
546
|
+
* a fresh one minted — the `if (loaded)` below only adopts an id the store
|
|
547
|
+
* actually knows. That is OWASP's strict mode and PHP's own
|
|
548
|
+
* `session.use_strict_mode=1` default, and it is what stops an attacker
|
|
549
|
+
* planting a session id that survives the victim's login. The validation
|
|
550
|
+
* above sits IN FRONT of it; neither replaces the other.
|
|
551
|
+
*
|
|
369
552
|
* @param sessionId - Existing session ID to resume (optional)
|
|
370
553
|
* @returns The session ID
|
|
371
554
|
*/
|
|
372
555
|
start(sessionId?: string): string {
|
|
556
|
+
if (sessionId !== undefined && !isValidSessionId(sessionId)) {
|
|
557
|
+
sessionId = undefined;
|
|
558
|
+
}
|
|
373
559
|
if (sessionId) {
|
|
374
560
|
const loaded = this.safeRead(sessionId);
|
|
561
|
+
if (!loaded && this.lastReadFailed) {
|
|
562
|
+
// The store did not ANSWER. That is not evidence the id is unknown, so
|
|
563
|
+
// keep it and degrade to an empty session rather than rotating.
|
|
564
|
+
this.sessionId = sessionId;
|
|
565
|
+
const now = Math.floor(Date.now() / 1000);
|
|
566
|
+
this.data = { _created: now, _accessed: now };
|
|
567
|
+
this.dirty = false;
|
|
568
|
+
return sessionId;
|
|
569
|
+
}
|
|
375
570
|
if (loaded) {
|
|
376
|
-
//
|
|
571
|
+
// Expiry is the HANDLER's job, and it is decided from the record's own
|
|
572
|
+
// absolute deadline. There used to be a SECOND, relative check here —
|
|
573
|
+
// `loaded._accessed && (now - loaded._accessed) > this.ttl` followed by
|
|
574
|
+
// safeDestroy() — running in series with the handler's absolute one.
|
|
575
|
+
//
|
|
576
|
+
// That is the exact shape that made tina4-php's file backend destroy
|
|
577
|
+
// records on read (a missing stamp fed into a subtraction, failure branch
|
|
578
|
+
// deletes). It was defused here only by the leading `loaded._accessed &&`
|
|
579
|
+
// short-circuit, and it was a landmine: two expiry mechanisms on the same
|
|
580
|
+
// data, one of them the broken shape, and this one judged a stored record
|
|
581
|
+
// against whatever ttl the READER happened to carry rather than the
|
|
582
|
+
// deadline the record was written with.
|
|
377
583
|
const now = Math.floor(Date.now() / 1000);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// must not abort the resume — the request still serves.
|
|
387
|
-
this.safeWrite(this.sessionId, this.data, this.ttl);
|
|
388
|
-
return sessionId;
|
|
389
|
-
}
|
|
584
|
+
this.sessionId = sessionId;
|
|
585
|
+
this.data = loaded;
|
|
586
|
+
this.data._accessed = now;
|
|
587
|
+
this.dirty = false;
|
|
588
|
+
// Refresh the accessed timestamp; a write failure here is logged but
|
|
589
|
+
// must not abort the resume — the request still serves.
|
|
590
|
+
this.safeWrite(this.sessionId, this.data, this.ttl);
|
|
591
|
+
return sessionId;
|
|
390
592
|
}
|
|
391
593
|
}
|
|
392
594
|
|
|
@@ -634,6 +836,21 @@ export function sessionCookieName(): string {
|
|
|
634
836
|
return process.env.TINA4_SESSION_NAME ?? "tina4_session";
|
|
635
837
|
}
|
|
636
838
|
|
|
839
|
+
/**
|
|
840
|
+
* Is TINA4_SESSION_STRICT on? The single source of truth for the flag.
|
|
841
|
+
*
|
|
842
|
+
* Module level, not a Session field, because the REQUEST PATH has to be able to
|
|
843
|
+
* consult it when a Session could not be constructed at all - a handler whose
|
|
844
|
+
* constructor raises, or a refused TINA4_SESSION_BACKEND, both fail BEFORE
|
|
845
|
+
* there is an object to ask. That gap is why strict mode used to be inert on
|
|
846
|
+
* the request path. Parity with Python `session.session_strict_mode()`.
|
|
847
|
+
*
|
|
848
|
+
* TINA4_SESSION_STRICT re-throw instead of degrading (default: false)
|
|
849
|
+
*/
|
|
850
|
+
export function sessionStrictMode(): boolean {
|
|
851
|
+
return isTruthy(process.env.TINA4_SESSION_STRICT);
|
|
852
|
+
}
|
|
853
|
+
|
|
637
854
|
/**
|
|
638
855
|
* Build the `Set-Cookie` header value for a Tina4 session. Centralised so
|
|
639
856
|
* the auto-cookie path in server.ts and `Session.cookieHeader()` agree on
|