tina4-nodejs 3.13.92 → 3.13.95

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.
Files changed (193) hide show
  1. package/CLAUDE.md +170 -28
  2. package/README.md +2 -2
  3. package/package.json +13 -9
  4. package/packages/cli/dist/bin.js +33126 -30055
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +33062 -29908
  8. package/packages/core/src/ai.ts +7 -1
  9. package/packages/core/src/auth.ts +191 -39
  10. package/packages/core/src/background.ts +19 -19
  11. package/packages/core/src/cache.ts +492 -49
  12. package/packages/core/src/devAdmin.ts +79 -32
  13. package/packages/core/src/devMailbox.ts +20 -44
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +7 -6
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +81 -13
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +6 -9
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +751 -414
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  34. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  35. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  36. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
  37. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  38. package/packages/core/src/sessionHandlers/respClient.ts +16 -143
  39. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  40. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  41. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  42. package/packages/core/src/testClient.ts +18 -5
  43. package/packages/core/src/trustedProxy.ts +249 -0
  44. package/packages/core/src/types.ts +29 -5
  45. package/packages/core/src/websocket.ts +66 -0
  46. package/packages/frond/dist/index.js +74 -31
  47. package/packages/frond/src/engine.ts +99 -33
  48. package/packages/orm/dist/index.js +26554 -23400
  49. package/packages/orm/src/adapters/firebird.ts +183 -56
  50. package/packages/orm/src/adapters/mongodb.ts +25 -4
  51. package/packages/orm/src/adapters/mssql.ts +114 -29
  52. package/packages/orm/src/adapters/mysql.ts +103 -40
  53. package/packages/orm/src/adapters/odbc.ts +44 -21
  54. package/packages/orm/src/adapters/postgres.ts +118 -26
  55. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  56. package/packages/orm/src/adapters/sqlite.ts +64 -25
  57. package/packages/orm/src/baseModel.ts +135 -40
  58. package/packages/orm/src/cachedDatabase.ts +43 -19
  59. package/packages/orm/src/connectTimeout.ts +265 -0
  60. package/packages/orm/src/database.ts +338 -198
  61. package/packages/orm/src/databaseResult.ts +65 -13
  62. package/packages/orm/src/databaseUrl.ts +484 -0
  63. package/packages/orm/src/docstore.ts +386 -145
  64. package/packages/orm/src/index.ts +13 -3
  65. package/packages/orm/src/migration.ts +18 -3
  66. package/packages/orm/src/queryBuilder.ts +38 -4
  67. package/packages/orm/src/sqlTranslator.ts +310 -4
  68. package/packages/orm/src/types.ts +15 -4
  69. package/types/cli/src/bin.d.ts +92 -0
  70. package/types/cli/src/commands/build.d.ts +2 -0
  71. package/types/cli/src/commands/generate.d.ts +47 -0
  72. package/types/cli/src/commands/init.d.ts +1 -0
  73. package/types/cli/src/commands/metrics.d.ts +6 -0
  74. package/types/cli/src/commands/migrate.d.ts +1 -0
  75. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  76. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  77. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  78. package/types/cli/src/commands/queue.d.ts +20 -0
  79. package/types/cli/src/commands/routes.d.ts +1 -0
  80. package/types/cli/src/commands/seed.d.ts +1 -0
  81. package/types/cli/src/commands/serve.d.ts +6 -0
  82. package/types/cli/src/commands/test.d.ts +1 -0
  83. package/types/core/src/ai.d.ts +64 -0
  84. package/types/core/src/api.d.ts +262 -0
  85. package/types/core/src/auth.d.ts +177 -0
  86. package/types/core/src/authGate.d.ts +20 -0
  87. package/types/core/src/background.d.ts +34 -0
  88. package/types/core/src/cache.d.ts +163 -0
  89. package/types/core/src/constants.d.ts +38 -0
  90. package/types/core/src/container.d.ts +44 -0
  91. package/types/core/src/context/chunker.d.ts +31 -0
  92. package/types/core/src/context/index.d.ts +93 -0
  93. package/types/core/src/devAdmin.d.ts +179 -0
  94. package/types/core/src/devMailbox.d.ts +54 -0
  95. package/types/core/src/dispatchPipeline.d.ts +117 -0
  96. package/types/core/src/docs.d.ts +141 -0
  97. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  98. package/types/core/src/dotenv.d.ts +87 -0
  99. package/types/core/src/env.d.ts +28 -0
  100. package/types/core/src/errorOverlay.d.ts +36 -0
  101. package/types/core/src/events.d.ts +75 -0
  102. package/types/core/src/fakeData.d.ts +55 -0
  103. package/types/core/src/feedback.d.ts +90 -0
  104. package/types/core/src/graphql.d.ts +207 -0
  105. package/types/core/src/health.d.ts +22 -0
  106. package/types/core/src/htmlElement.d.ts +75 -0
  107. package/types/core/src/i18n.d.ts +37 -0
  108. package/types/core/src/index.d.ts +92 -0
  109. package/types/core/src/job.d.ts +39 -0
  110. package/types/core/src/logger.d.ts +200 -0
  111. package/types/core/src/mcp.d.ts +248 -0
  112. package/types/core/src/messenger.d.ts +191 -0
  113. package/types/core/src/metrics.d.ts +41 -0
  114. package/types/core/src/middleware.d.ts +330 -0
  115. package/types/core/src/mqtt.d.ts +257 -0
  116. package/types/core/src/mqttMessage.d.ts +67 -0
  117. package/types/core/src/plan.d.ts +96 -0
  118. package/types/core/src/projectIndex.d.ts +56 -0
  119. package/types/core/src/queue.d.ts +268 -0
  120. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  121. package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
  122. package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
  123. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  124. package/types/core/src/rateLimiter.d.ts +49 -0
  125. package/types/core/src/request.d.ts +25 -0
  126. package/types/core/src/response.d.ts +28 -0
  127. package/types/core/src/routeDiscovery.d.ts +12 -0
  128. package/types/core/src/router.d.ts +366 -0
  129. package/types/core/src/scss.d.ts +19 -0
  130. package/types/core/src/server.d.ts +146 -0
  131. package/types/core/src/service.d.ts +115 -0
  132. package/types/core/src/session.d.ts +341 -0
  133. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  134. package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
  135. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  136. package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
  137. package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
  138. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  139. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  140. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  141. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  142. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  143. package/types/core/src/static.d.ts +2 -0
  144. package/types/core/src/test.d.ts +94 -0
  145. package/types/core/src/testClient.d.ts +36 -0
  146. package/types/core/src/testing.d.ts +58 -0
  147. package/types/core/src/trustedProxy.d.ts +44 -0
  148. package/types/core/src/types.d.ts +242 -0
  149. package/types/core/src/validator.d.ts +52 -0
  150. package/types/core/src/websocket.d.ts +402 -0
  151. package/types/core/src/websocketBackplane.d.ts +166 -0
  152. package/types/core/src/websocketConnection.d.ts +54 -0
  153. package/types/core/src/wsdl.d.ts +101 -0
  154. package/types/frond/src/engine.d.ts +263 -0
  155. package/types/frond/src/index.d.ts +2 -0
  156. package/types/orm/src/adapters/firebird.d.ts +183 -0
  157. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  158. package/types/orm/src/adapters/mssql.d.ts +77 -0
  159. package/types/orm/src/adapters/mysql.d.ts +67 -0
  160. package/types/orm/src/adapters/odbc.d.ts +94 -0
  161. package/types/orm/src/adapters/postgres.d.ts +86 -0
  162. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  163. package/types/orm/src/adapters/sqlite.d.ts +68 -0
  164. package/types/orm/src/autoCrud.d.ts +73 -0
  165. package/types/orm/src/baseModel.d.ts +427 -0
  166. package/types/orm/src/cachedDatabase.d.ts +190 -0
  167. package/types/orm/src/connectTimeout.d.ts +100 -0
  168. package/types/orm/src/database.d.ts +655 -0
  169. package/types/orm/src/databaseResult.d.ts +109 -0
  170. package/types/orm/src/databaseUrl.d.ts +125 -0
  171. package/types/orm/src/docstore.d.ts +241 -0
  172. package/types/orm/src/fakeData.d.ts +22 -0
  173. package/types/orm/src/index.d.ts +43 -0
  174. package/types/orm/src/migration.d.ts +275 -0
  175. package/types/orm/src/model.d.ts +7 -0
  176. package/types/orm/src/query.d.ts +14 -0
  177. package/types/orm/src/queryBuilder.d.ts +193 -0
  178. package/types/orm/src/realtime/index.d.ts +7 -0
  179. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  180. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  181. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  182. package/types/orm/src/realtime/models/message.d.ts +36 -0
  183. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  184. package/types/orm/src/realtime/realtime.d.ts +24 -0
  185. package/types/orm/src/realtime/storage.d.ts +61 -0
  186. package/types/orm/src/seeder.d.ts +118 -0
  187. package/types/orm/src/sqlTranslator.d.ts +258 -0
  188. package/types/orm/src/types.d.ts +148 -0
  189. package/types/orm/src/validation.d.ts +6 -0
  190. package/types/swagger/src/generator.d.ts +46 -0
  191. package/types/swagger/src/index.d.ts +2 -0
  192. package/types/swagger/src/ui.d.ts +11 -0
  193. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
@@ -0,0 +1,97 @@
1
+ import type { SessionHandler } from "../session.js";
2
+ interface SessionData {
3
+ _created: number;
4
+ _accessed: number;
5
+ [key: string]: unknown;
6
+ }
7
+ export interface DatabaseSessionConfig {
8
+ /** SQLite database file path. Explicit config wins over TINA4_DATABASE_URL. */
9
+ dbPath?: string;
10
+ backend?: string;
11
+ path?: string;
12
+ ttl?: number;
13
+ redisHost?: string;
14
+ redisPort?: number;
15
+ redisPassword?: string;
16
+ redisPrefix?: string;
17
+ redisDb?: number;
18
+ }
19
+ /**
20
+ * Database session handler.
21
+ *
22
+ * Stores session data as JSON in a `tina4_session` table.
23
+ * Expiry is checked on read; expired rows are cleaned up lazily.
24
+ */
25
+ export declare class DatabaseSessionHandler implements SessionHandler {
26
+ private sqliteHandle;
27
+ /** Set for SQLite. Null when this handler talks to a networked engine. */
28
+ private dbPath;
29
+ /** Set for a networked engine. Null for SQLite. */
30
+ private target;
31
+ private initialized;
32
+ /**
33
+ * NO I/O IN A CONSTRUCTOR (ADR-0021).
34
+ *
35
+ * This used to run `new DatabaseSync(dbPath)` and a `PRAGMA journal_mode =
36
+ * WAL` right here. Both are real work against real storage: opening the
37
+ * database CREATES the file, and switching to WAL creates its `-wal` and
38
+ * `-shm` siblings. Measured from a clean temp cwd, merely constructing this
39
+ * handler left three files on disk before a single session was ever read or
40
+ * written.
41
+ *
42
+ * A constructor sits OUTSIDE the log-loud-and-degrade policy, so nothing it
43
+ * does can be logged, degraded, or re-raised by TINA4_SESSION_STRICT - the one
44
+ * place the policy cannot protect is the first thing that runs.
45
+ *
46
+ * Everything below is pure string work. Resolving the target parses a URL;
47
+ * refusing an unsupported engine is a CONFIGURATION error that must still be
48
+ * loud at construction. The database - file or socket - is opened on first
49
+ * use. Going multi-engine is the change most likely to reintroduce
50
+ * constructor-time I/O, which is why test/sessionHandlerConstruction.test.ts
51
+ * measures a real filesystem and a real listening socket rather than trusting
52
+ * this comment.
53
+ */
54
+ constructor(config?: DatabaseSessionConfig);
55
+ /** Open the SQLite database on FIRST USE, not at construction. */
56
+ private get sqlite();
57
+ private get engine();
58
+ /**
59
+ * Decide, from TINA4_DATABASE_URL, which engine this handler talks to.
60
+ *
61
+ * A NON-SQLITE URL NOW WORKS. It used to throw, because the handler drove
62
+ * `node:sqlite` directly and had no way to reach anything else; the async
63
+ * drivers now ride the sync bridge, so the reason for the refusal is gone.
64
+ *
65
+ * AN UNSUPPORTED ENGINE STILL REFUSES, and that half is not negotiable. The
66
+ * original defect was worse than a refusal: an unrecognised URL fell through
67
+ * to the literal default `"data/tina4_sessions.db"`, so
68
+ * `TINA4_DATABASE_URL=postgres://...` with `TINA4_SESSION_BACKEND=database`
69
+ * round-tripped happily while writing SQLite files into the process working
70
+ * directory. Measured from a clean temp cwd: round-trip true, and `data/`
71
+ * contained `tina4_sessions.db`, `-shm` and `-wal`. Every horizontally-scaled
72
+ * instance therefore had its own private session store and a user was logged
73
+ * out on every request that landed elsewhere.
74
+ *
75
+ * This is the same rule `resolveBackend()` applies one layer up, where an
76
+ * unknown backend name raises rather than falling through to disk.
77
+ *
78
+ * @throws Error naming the offending scheme and the engines this backend
79
+ * speaks. The URL itself is NEVER in the message - it may carry a
80
+ * password.
81
+ */
82
+ private resolveTarget;
83
+ private unsupportedEngine;
84
+ /** Run a statement that returns rows. */
85
+ private query;
86
+ /** Run a statement that returns nothing. */
87
+ private exec;
88
+ /**
89
+ * Ensure the session table exists (called once on first use).
90
+ */
91
+ private ensureTable;
92
+ read(sessionId: string): SessionData | null;
93
+ write(sessionId: string, data: SessionData, ttl: number): void;
94
+ destroy(sessionId: string): void;
95
+ gc(_maxLifetime: number): void;
96
+ }
97
+ export {};
@@ -0,0 +1,60 @@
1
+ import type { SessionHandler } from "../session.js";
2
+ interface SessionData {
3
+ _created: number;
4
+ _accessed: number;
5
+ [key: string]: unknown;
6
+ }
7
+ export interface MemcachedSessionConfig {
8
+ host?: string;
9
+ port?: number;
10
+ prefix?: string;
11
+ ttl?: number;
12
+ backend?: string;
13
+ path?: string;
14
+ }
15
+ export declare class MemcachedSessionHandler implements SessionHandler {
16
+ private host;
17
+ private port;
18
+ private prefix;
19
+ private ttl;
20
+ constructor(config?: MemcachedSessionConfig);
21
+ private key;
22
+ /**
23
+ * Run one memcached command synchronously and return the raw reply.
24
+ *
25
+ * Delegates to the shared persistent-connection transport (syncSocket) rather
26
+ * than spawning a child per command: that cost a process spawn plus a fresh
27
+ * TCP connection every time (p50 41ms, p99 487ms) and its tail tripped the
28
+ * deadline under load — the same defect that made the Valkey session tests
29
+ * flaky, which this handler inherited on the day it was written.
30
+ *
31
+ * @throws Error on any transport failure — never swallowed to an empty
32
+ * result, because for a session an outage must be distinguishable
33
+ * from "no session yet".
34
+ */
35
+ private command;
36
+ read(sessionId: string): SessionData | null;
37
+ /**
38
+ * Convert a ttl in SECONDS to memcached's dual-meaning exptime field.
39
+ *
40
+ * memcached documents exptime as RELATIVE seconds up to 2592000 (30 days),
41
+ * and as an ABSOLUTE UNIX TIMESTAMP for anything larger. Sending a raw ttl of
42
+ * 2592001 therefore does not mean "30 days and one second" - it means
43
+ * 1970-01-31, which is already past, so the item expires the instant it is
44
+ * stored. memcached still replies STORED, so the write looks successful and
45
+ * the very next read is a miss: a silent logout on every request.
46
+ *
47
+ * Measured against real memcached 1.6.45: ttl=2592000 survives, ttl=2592001
48
+ * vanishes instantly.
49
+ *
50
+ * We CONVERT rather than CLAMP. Clamping a 60-day session down to 30 days
51
+ * would silently shorten a lifetime the operator explicitly asked to be
52
+ * longer, which is the same class of lie in the other direction.
53
+ */
54
+ private expTime;
55
+ write(sessionId: string, data: SessionData, ttl: number): void;
56
+ destroy(sessionId: string): void;
57
+ /** No-op — memcached expires its own keys via the TTL set on write. */
58
+ gc(_maxLifetime: number): void;
59
+ }
60
+ export {};
@@ -0,0 +1,35 @@
1
+ export interface MongoTarget {
2
+ host: string;
3
+ port: number;
4
+ database: string;
5
+ collection: string;
6
+ }
7
+ /**
8
+ * Command args: `filter` (always), plus `data`/`expires_at`/`last_accessed` for
9
+ * an update.
10
+ *
11
+ * `expires_at` is an ABSOLUTE epoch-seconds deadline computed at write time. It
12
+ * is what makes the session actually expire: this handler previously stored no
13
+ * expiry at all, created no TTL index, and never consulted anything on read, so
14
+ * MongoDB sessions lived forever.
15
+ */
16
+ export interface MongoCommandArgs {
17
+ filter: Record<string, unknown>;
18
+ data?: unknown;
19
+ expires_at?: number;
20
+ last_accessed?: number;
21
+ }
22
+ /**
23
+ * Run a session Mongo command synchronously.
24
+ *
25
+ * - command "find" -> the matched document as a JSON string, or "__EMPTY__".
26
+ * - command "update" -> "__OK__" (upsert of `{_id, data, last_accessed}`).
27
+ * - command "delete" -> "__OK__".
28
+ *
29
+ * THROWS `<label> command failed: ...` on a transport failure (server
30
+ * unreachable, timeout) OR a Mongo command error (`ok != 1`), so the Session
31
+ * boundary can log-loud + degrade (or re-throw under strict mode). A genuine
32
+ * miss is NOT a failure and comes back as "__EMPTY__" — collapsing the two is
33
+ * how a dead backend silently logs every user out.
34
+ */
35
+ export declare function mongoCommandSync(target: MongoTarget, command: "find" | "update" | "delete", args: MongoCommandArgs, label?: string): string;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Tina4 MongoDB Session Handler — MongoDB wire protocol via raw TCP, zero dependencies.
3
+ *
4
+ * Stores session data in MongoDB using the MongoDB wire protocol directly.
5
+ * No `mongodb` or `mongoose` npm package required.
6
+ *
7
+ * Configure via environment variables:
8
+ * TINA4_SESSION_MONGO_HOST (default: "127.0.0.1")
9
+ * TINA4_SESSION_MONGO_PORT (default: 27017)
10
+ * TINA4_SESSION_MONGO_URI (overrides host/port if set)
11
+ * TINA4_SESSION_MONGO_USERNAME (optional)
12
+ * TINA4_SESSION_MONGO_PASSWORD (optional)
13
+ * TINA4_SESSION_MONGO_DB (default: "tina4")
14
+ * TINA4_SESSION_MONGO_COLLECTION (default: "sessions")
15
+ */
16
+ import type { SessionHandler } from "../session.js";
17
+ interface SessionData {
18
+ _created: number;
19
+ _accessed: number;
20
+ [key: string]: unknown;
21
+ }
22
+ export interface MongoSessionConfig {
23
+ host?: string;
24
+ port?: number;
25
+ uri?: string;
26
+ username?: string;
27
+ password?: string;
28
+ database?: string;
29
+ collection?: string;
30
+ backend?: string;
31
+ path?: string;
32
+ ttl?: number;
33
+ redisHost?: string;
34
+ redisPort?: number;
35
+ redisPassword?: string;
36
+ redisPrefix?: string;
37
+ redisDb?: number;
38
+ }
39
+ /**
40
+ * MongoDB session handler using raw TCP (MongoDB wire protocol).
41
+ *
42
+ * Uses synchronous socket communication via child process — no external
43
+ * MongoDB client library required. Stores session data as BSON documents
44
+ * with TTL index support.
45
+ */
46
+ export declare class MongoSessionHandler implements SessionHandler {
47
+ private host;
48
+ private port;
49
+ private uri;
50
+ private username;
51
+ private password;
52
+ private database;
53
+ private collection;
54
+ private hostExplicit;
55
+ private portExplicit;
56
+ private uriExplicit;
57
+ constructor(config?: MongoSessionConfig);
58
+ /**
59
+ * Resolve the effective host/port (honours a configured mongodb:// URI).
60
+ *
61
+ * PRECEDENCE, and it runs the way every other resolver in Tina4 runs -
62
+ * EXPLICIT CONFIGURATION BEATS THE ENVIRONMENT:
63
+ *
64
+ * 1. an explicitly passed `uri` - the caller named a complete address
65
+ * 2. an explicitly passed host/port - per field, and an ENV uri may not touch them
66
+ * 3. TINA4_SESSION_MONGO_URI / _URL - the ambient address
67
+ * 4. TINA4_SESSION_MONGO_HOST/_PORT - ambient parts
68
+ * 5. 127.0.0.1:27017
69
+ *
70
+ * THE DEFECT THIS FIXES, measured 2026-08-05 on the lab host: the URI won
71
+ * UNCONDITIONALLY, including over an argument the caller had just passed by
72
+ * hand. With TINA4_SESSION_MONGO_URI=mongodb://127.0.0.1:27017/tina4_node
73
+ * exported - an entirely ordinary deployment setting -
74
+ *
75
+ * new MongoSessionHandler({ host: "127.0.0.1", port: 59999 })
76
+ *
77
+ * resolved to 127.0.0.1:27017. The handler dialled a DIFFERENT SERVER from the
78
+ * one it was told to use, said nothing, and a read against it came back null:
79
+ * indistinguishable from a genuine miss. So an app that points a handler at one
80
+ * Mongo while the environment names another writes its sessions to the wrong
81
+ * server, and the backend-failure policy cannot fire because nothing failed.
82
+ *
83
+ * It also made two suites report a framework contract as broken - the
84
+ * unreachable-server-must-throw cases in sessionHandlers and
85
+ * sessionMongoRawProtocol never reached the dead port at all, so they measured
86
+ * a live server and got a miss. Those cases were RIGHT; this was the bug they
87
+ * were catching.
88
+ *
89
+ * Same class as the TINA4_QUEUE_URL precedence inversion fixed in PHP's
90
+ * Queue::resolveMongoConfig earlier the same day: environment quietly beating
91
+ * an explicit argument.
92
+ */
93
+ private target;
94
+ read(sessionId: string): SessionData | null;
95
+ /**
96
+ * Write session data.
97
+ *
98
+ * The ttl is consumed HERE, at write time, and baked into an absolute deadline,
99
+ * so nothing at read time needs to know what the ttl was. The parameter used to
100
+ * be named `_ttl` and thrown away.
101
+ *
102
+ * @param sessionId - the session id
103
+ * @param data - the payload to store
104
+ * @param ttl - lifetime in seconds; 0 or less means never expires
105
+ */
106
+ write(sessionId: string, data: SessionData, ttl?: number): void;
107
+ destroy(sessionId: string): void;
108
+ }
109
+ export {};
@@ -0,0 +1,22 @@
1
+ export interface RespTarget {
2
+ host: string;
3
+ port: number;
4
+ /** AUTH password (empty/undefined = no AUTH sent). */
5
+ password?: string;
6
+ /** SELECT db index (0/undefined = no SELECT sent). */
7
+ db?: number;
8
+ }
9
+ /**
10
+ * Run a single RESP command synchronously against host:port and return the reply.
11
+ *
12
+ * - A genuine nil / key-miss returns `""` (callers treat "" as "no session yet").
13
+ * - A transport FAILURE (server unreachable, timeout, connection closed before a
14
+ * reply) THROWS `<label> command failed: ...`.
15
+ * - A RESP error reply — including a rejected AUTH/SELECT handshake — THROWS
16
+ * `<label> error: ...`. A rejected handshake is a transport failure, not a
17
+ * result, so it is surfaced ahead of the command reply.
18
+ *
19
+ * The miss/failure split is the whole contract: collapsing them is how a dead
20
+ * backend silently logs every user out instead of surfacing an outage.
21
+ */
22
+ export declare function respCommandSync(target: RespTarget, args: string[], label?: string): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The SQL engines the database session backend speaks.
3
+ *
4
+ * This IS the invariant: it is the engine set of the ORM Database layer minus
5
+ * the two non-SQL entries (mongodb has its own session backend, odbc has no
6
+ * session story in any of the four frameworks). Naming it once means the
7
+ * refusal message and the dispatch can never disagree about what is supported.
8
+ */
9
+ export declare const SQL_SESSION_ENGINES: readonly ["sqlite", "postgres", "mysql", "mssql", "firebird"];
10
+ export type SqlSessionEngine = (typeof SQL_SESSION_ENGINES)[number];
11
+ /** The engines that need the bridge - everything except already-sync SQLite. */
12
+ export type BridgedEngine = Exclude<SqlSessionEngine, "sqlite">;
13
+ /**
14
+ * A connection target for the worker.
15
+ *
16
+ * A PLAIN object, deliberately - never a `DatabaseUrl`. That class carries a
17
+ * cleartext password and its own docblock forbids persisting it across a
18
+ * structured-clone boundary (test/databaseUrlRedaction.test.ts enforces it).
19
+ * The worker genuinely needs credentials to authenticate, so it gets the fields
20
+ * it needs and nothing that renders itself.
21
+ */
22
+ export interface SqlTarget {
23
+ engine: BridgedEngine;
24
+ host: string;
25
+ port: number;
26
+ database: string;
27
+ username: string | null;
28
+ password: string | null;
29
+ }
30
+ /**
31
+ * Run one SQL statement synchronously against a networked engine.
32
+ *
33
+ * @returns the result rows - always an array, empty for a write or DDL.
34
+ * @throws Error on ANY driver failure (server unreachable, bad credentials,
35
+ * SQL error). It is never swallowed into an empty result: for a session
36
+ * store, "the database is down" and "no session yet" must stay
37
+ * distinguishable, or a dead backend silently logs every user out.
38
+ */
39
+ export declare function sqlCommandSync(target: SqlTarget, sql: string, params?: unknown[], label?: string): Record<string, unknown>[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tina4 sync-over-async bridge — call an ASYNC worker from SYNCHRONOUS code.
3
+ *
4
+ * The SessionHandler interface is synchronous; every backend client Node offers
5
+ * (node:net, the mongodb driver) is async-only. The old resolution was to run
6
+ * each command in a short-lived `node -e` child and block on execFileSync. That
7
+ * works but pays a process spawn — and a fresh connection, and for Mongo a fresh
8
+ * driver load — PER COMMAND. Measured on this machine: spawn min 38ms / p50 41ms
9
+ * / p99 487ms, with a bare TCP connect adding a ~0.5-0.9s tail of its own. That
10
+ * tail tripped the child's deadline under load, which is what made the Valkey
11
+ * session tests flaky with the signature "a value that was just written reads
12
+ * back null" — the write timed out for the caller while still landing on the
13
+ * server.
14
+ *
15
+ * This is the ONE piece of plumbing that replaces it. A Worker thread keeps its
16
+ * own event loop, so it can do ordinary async I/O and hold a long-lived
17
+ * connection. The caller hands it a message with postMessage (delivered on the
18
+ * WORKER's loop, so a blocked main thread cannot deadlock it), then blocks in
19
+ * Atomics.wait until the worker writes a reply into a SharedArrayBuffer and
20
+ * Atomics.notify wakes it.
21
+ *
22
+ * Every session backend that needs sync-over-async uses this — RESP
23
+ * (Redis/Valkey), memcached's text protocol, and MongoDB — so the blocking
24
+ * handshake exists once rather than once per backend.
25
+ */
26
+ import { Worker } from "node:worker_threads";
27
+ /** Reply status, written by the worker into control[IDX_STATUS]. */
28
+ export declare const STATUS_OK = 0;
29
+ /** A healthy server answered with an error (RESP `-ERR`, a Mongo command error). */
30
+ export declare const STATUS_ERROR = 1;
31
+ /** A genuine miss — no such key/document. NOT a failure. */
32
+ export declare const STATUS_NIL = 2;
33
+ /** The reply did not fit the shared buffer. */
34
+ export declare const STATUS_TOO_LARGE = 3;
35
+ /** Connection/socket/driver failure, as opposed to an answer from a healthy server. */
36
+ export declare const STATUS_TRANSPORT = 4;
37
+ export declare const IDX_SEQ = 0;
38
+ export declare const IDX_LENGTH = 1;
39
+ export declare const IDX_STATUS = 2;
40
+ export declare const IDX_READY = 3;
41
+ /**
42
+ * Reply payload ceiling. A session document beyond this is pathological, and a
43
+ * fixed buffer keeps the fast path allocation-free. A worker reports
44
+ * STATUS_TOO_LARGE rather than truncating — a silently truncated session would
45
+ * deserialise into garbage.
46
+ */
47
+ export declare const DATA_BYTES: number;
48
+ /** How long a caller blocks before giving up on a reply. */
49
+ export declare const REPLY_TIMEOUT_MS = 5000;
50
+ /**
51
+ * How long the FIRST caller waits for a brand-new worker to come up.
52
+ *
53
+ * Boot and the command round-trip must not share one budget. Cold start is
54
+ * normally ~27ms, but on a loaded machine (the session suite spawns a batch of
55
+ * blocking Mongo children immediately beforehand) it can stretch — and when it
56
+ * ate into the 5s command budget the very first Valkey write failed with
57
+ * "timed out after 5000ms", which read exactly like the flake this transport
58
+ * was built to remove. Boot gets its own generous budget so the per-command
59
+ * timeout can stay tight and mean what it says.
60
+ */
61
+ export declare const BOOT_TIMEOUT_MS = 15000;
62
+ /**
63
+ * The worker-side helper, injected into every worker body. Kept here so the
64
+ * reply protocol is written once: a worker only has to call
65
+ * `__reply(status, payload)` and never touches Atomics itself.
66
+ */
67
+ export declare const WORKER_REPLY_HELPER = "\nconst __control = new Int32Array(workerData.controlBuffer);\nconst __data = new Uint8Array(workerData.dataBuffer);\nconst __encoder = new TextEncoder();\n\n// Announce readiness the moment this thread is executing. The parent blocks on\n// this before its first command, so a slow boot can never be mistaken for a\n// slow command. This is signalled by the worker's OWN bootstrap and needs no\n// message from the parent, so it is safe even while the parent is blocked.\nAtomics.store(__control, 3, 1);\nAtomics.notify(__control, 3);\n\nfunction __reply(status, payload) {\n let length = 0;\n if (payload !== undefined && payload !== null && status !== 2) {\n const bytes = __encoder.encode(String(payload));\n if (bytes.length > __data.length) {\n Atomics.store(__control, 2, 3);\n Atomics.store(__control, 1, 0);\n Atomics.add(__control, 0, 1);\n Atomics.notify(__control, 0);\n return;\n }\n __data.set(bytes, 0);\n length = bytes.length;\n }\n Atomics.store(__control, 2, status);\n Atomics.store(__control, 1, length);\n Atomics.add(__control, 0, 1);\n Atomics.notify(__control, 0);\n}\n";
68
+ export interface BridgeReply {
69
+ status: number;
70
+ payload: string;
71
+ }
72
+ export interface Bridge {
73
+ /** Send a message to the worker and BLOCK until it replies. */
74
+ call(message: unknown, label: string): BridgeReply;
75
+ worker: Worker;
76
+ }
77
+ /**
78
+ * Get (or create) the bridge for a key. One worker per key, created once and
79
+ * unref'd so it can never hold the process open.
80
+ *
81
+ * @param key Identity of the connection target — same key, same worker
82
+ * @param workerSource The worker body; WORKER_REPLY_HELPER is prepended for it
83
+ * @param workerData Passed to the worker verbatim (plus the shared buffers)
84
+ */
85
+ export declare function getBridge(key: string, workerSource: string, workerData: Record<string, unknown>): Bridge;
86
+ /**
87
+ * Terminate every worker. Tests and short-lived scripts call this so a spawned
88
+ * worker never outlives the work that created it — "reap what you spawn". Normal
89
+ * apps do not need it: the workers are unref'd.
90
+ */
91
+ export declare function closeBridges(): void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tina4 synchronous socket transport — ONE persistent connection per target.
3
+ *
4
+ * Serves the RESP backends (Redis, Valkey) and memcached's text protocol. The
5
+ * sync-over-async plumbing lives in syncBridge; this file is only the socket
6
+ * worker and the two protocol entry points.
7
+ *
8
+ * See syncBridge for WHY: each command used to run in a short-lived `node -e`
9
+ * child, paying a process spawn AND a fresh TCP connection every time (spawn p50
10
+ * 41ms / p99 487ms, connect tail 0.5-0.9s), and that tail tripped the child's
11
+ * deadline under load — the cause of the Valkey session flake.
12
+ *
13
+ * Reconnection is the worker's business: a dropped socket is re-established on
14
+ * the next command rather than surfacing as a caller error.
15
+ */
16
+ import { closeBridges, DATA_BYTES } from "./syncBridge.js";
17
+ export interface SyncSocketTarget {
18
+ host: string;
19
+ port: number;
20
+ /** AUTH password (empty/undefined = no AUTH sent). */
21
+ password?: string;
22
+ /** SELECT db index (0/undefined = no SELECT sent). */
23
+ db?: number;
24
+ }
25
+ /**
26
+ * Run a single RESP command synchronously and return the reply.
27
+ *
28
+ * - A genuine nil / key-miss returns `""` (callers treat "" as "no session yet").
29
+ * - A transport FAILURE (unreachable, timeout, connection closed) THROWS
30
+ * `<label> command failed: ...`.
31
+ * - A RESP error reply — including a rejected AUTH/SELECT — THROWS
32
+ * `<label> error: ...`.
33
+ *
34
+ * The miss/failure split is the whole contract: collapsing them is how a dead
35
+ * backend silently logs every user out instead of surfacing an outage. A dead
36
+ * socket and a healthy server saying "-ERR" are different failures and read
37
+ * differently in a log, so they keep different wording.
38
+ */
39
+ export declare function syncCommand(target: SyncSocketTarget, args: string[], label?: string): string;
40
+ /**
41
+ * Run a raw text-protocol command (memcached) over the same persistent channel.
42
+ *
43
+ * The reply is whatever arrived once one of `terminators` appears — a text
44
+ * protocol carries no length prefix, so the caller names its own end markers.
45
+ * Failure semantics match {@link syncCommand}.
46
+ */
47
+ export declare function syncTextCommand(target: SyncSocketTarget, payload: string, terminators: string[], label?: string): string;
48
+ /** Re-exported so callers do not need to know the bridge exists. */
49
+ export { closeBridges as closeSyncSockets, DATA_BYTES };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Tina4 Valkey Session Handler — Valkey (Redis-compatible) via raw TCP, zero dependencies.
3
+ *
4
+ * Same as the Redis handler but uses VALKEY-prefixed configuration variables.
5
+ * Valkey is a Redis-compatible key-value store fork.
6
+ *
7
+ * Configure via environment variables:
8
+ * TINA4_SESSION_VALKEY_HOST (default: "127.0.0.1")
9
+ * TINA4_SESSION_VALKEY_PORT (default: 6379)
10
+ * TINA4_SESSION_VALKEY_PASSWORD (optional)
11
+ * TINA4_SESSION_VALKEY_PREFIX (default: "tina4:session:")
12
+ * TINA4_SESSION_VALKEY_DB (default: 0)
13
+ */
14
+ import type { SessionHandler } from "../session.js";
15
+ interface SessionData {
16
+ _created: number;
17
+ _accessed: number;
18
+ [key: string]: unknown;
19
+ }
20
+ export interface ValkeySessionConfig {
21
+ host?: string;
22
+ port?: number;
23
+ password?: string;
24
+ prefix?: string;
25
+ db?: number;
26
+ backend?: string;
27
+ path?: string;
28
+ ttl?: number;
29
+ redisHost?: string;
30
+ redisPort?: number;
31
+ redisPassword?: string;
32
+ redisPrefix?: string;
33
+ redisDb?: number;
34
+ }
35
+ /**
36
+ * Valkey session handler using raw TCP (RESP protocol).
37
+ *
38
+ * Uses synchronous socket communication — no external Valkey/Redis client required.
39
+ * Stores session data as JSON strings with Valkey TTL for automatic expiry.
40
+ *
41
+ * Valkey uses the same RESP protocol as Redis, so this handler is functionally
42
+ * identical to RedisSessionHandler but with VALKEY config variable names.
43
+ */
44
+ export declare class ValkeySessionHandler implements SessionHandler {
45
+ private host;
46
+ private port;
47
+ private password;
48
+ private prefix;
49
+ private db;
50
+ constructor(config?: ValkeySessionConfig);
51
+ /**
52
+ * Execute a RESP command synchronously against the live Valkey server.
53
+ *
54
+ * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
55
+ * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
56
+ * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
57
+ * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
58
+ */
59
+ private execSync;
60
+ private key;
61
+ read(sessionId: string): SessionData | null;
62
+ write(sessionId: string, data: SessionData, ttl: number): void;
63
+ destroy(sessionId: string): void;
64
+ }
65
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Tina4Request, Tina4Response } from "./types.js";
2
+ export declare function tryServeStatic(staticDir: string, req: Tina4Request, res: Tina4Response): boolean;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tina4 — The Intelligent Native Application 4ramework
3
+ * Copyright 2007 - current Tina4
4
+ * License: MIT https://opensource.org/licenses/MIT
5
+ *
6
+ * Tina4 xUnit-style Test base class.
7
+ *
8
+ * Chapter 18 of the documentation has long shown:
9
+ *
10
+ * class UserApiTest extends Tina4Test {
11
+ * async testHealth() {
12
+ * const resp = await this.get("/health");
13
+ * this.assertEqual(resp.status, 200);
14
+ * }
15
+ * }
16
+ *
17
+ * Until 3.13.0 this class did not exist — examples crashed with
18
+ * "ReferenceError: Tina4Test is not defined". This is the Node.js
19
+ * parity of the Python `tina4_python.test.Test`, PHP `Tina4\Test`,
20
+ * and Ruby `Tina4::Test` classes shipped at the same time.
21
+ *
22
+ * The class has a built-in runner (`Tina4Test.runAll`) so the docs'
23
+ * `npx tina4nodejs test` flow can discover every subclass without
24
+ * an external test framework. HTTP helpers (get/post/put/patch/delete)
25
+ * delegate to a lazy TestClient. Positional assertions match the
26
+ * cross-framework (actual, expected, message) shape.
27
+ */
28
+ import { TestClient, type TestResponse, type RequestOptions } from "./testClient.js";
29
+ /** Raised by Tina4Test assertion helpers when an assertion fails. */
30
+ export declare class AssertionError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ /** Result of running a Tina4Test subclass (or all subclasses). */
34
+ export interface TestRunResults {
35
+ passed: number;
36
+ failed: number;
37
+ errors: number;
38
+ details: Array<{
39
+ suite: string;
40
+ test: string;
41
+ status: "passed" | "failed" | "error";
42
+ message?: string;
43
+ }>;
44
+ }
45
+ /**
46
+ * Tina4 xUnit-style test base class — class-based suites with HTTP
47
+ * helpers and positional assertions, zero external deps.
48
+ *
49
+ * Subclass and define `test*` methods:
50
+ *
51
+ * class BasicTest extends Tina4Test {
52
+ * async testAddition() {
53
+ * this.assertEqual(2 + 2, 4, "addition works");
54
+ * }
55
+ * async testHttpHealth() {
56
+ * const resp = await this.get("/health");
57
+ * this.assertEqual(resp.status, 200);
58
+ * }
59
+ * }
60
+ *
61
+ * const results = await Tina4Test.runAll();
62
+ * // → { passed, failed, errors, details }
63
+ */
64
+ export declare class Tina4Test {
65
+ private _client;
66
+ /** snake_case lifecycle hook — runs before each test. Override in subclasses. */
67
+ setUp(): Promise<void>;
68
+ /** snake_case lifecycle hook — runs after each test. Override in subclasses. */
69
+ tearDown(): Promise<void>;
70
+ /** The lazily-created TestClient instance shared by this suite's tests. */
71
+ protected get client(): TestClient;
72
+ get(path: string, options?: RequestOptions): Promise<TestResponse>;
73
+ post(path: string, options?: RequestOptions): Promise<TestResponse>;
74
+ put(path: string, options?: RequestOptions): Promise<TestResponse>;
75
+ patch(path: string, options?: RequestOptions): Promise<TestResponse>;
76
+ delete(path: string, options?: RequestOptions): Promise<TestResponse>;
77
+ assertEqual(actual: unknown, expected: unknown, message?: string): void;
78
+ assertNotEqual(actual: unknown, expected: unknown, message?: string): void;
79
+ assertTrue(value: unknown, message?: string): void;
80
+ assertFalse(value: unknown, message?: string): void;
81
+ assertNull(value: unknown, message?: string): void;
82
+ assertNotNull(value: unknown, message?: string): void;
83
+ assertRaises(expectedClass: new (...args: never[]) => Error, fn: () => unknown | Promise<unknown>, message?: string): Promise<void>;
84
+ /** Register a subclass for discovery. Called automatically via `extends Tina4Test`. */
85
+ static register(klass: typeof Tina4Test): void;
86
+ /** Run every `test*` method on this class. Returns counts and per-test details. */
87
+ static run(this: typeof Tina4Test): Promise<TestRunResults>;
88
+ /** Run every Tina4Test subclass discovered via auto-registration. */
89
+ static runAll(options?: {
90
+ quiet?: boolean;
91
+ }): Promise<TestRunResults>;
92
+ /** Subclasses array — read-only view used by tests. */
93
+ static get subclasses(): ReadonlyArray<typeof Tina4Test>;
94
+ }