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.
Files changed (123) hide show
  1. package/CLAUDE.md +158 -30
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/packages/cli/dist/bin.js +30911 -28444
  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 +30810 -28261
  8. package/packages/core/public/css/tina4.min.css +1 -1
  9. package/packages/core/src/ai.ts +7 -1
  10. package/packages/core/src/auth.ts +191 -39
  11. package/packages/core/src/background.ts +19 -19
  12. package/packages/core/src/cache.ts +492 -49
  13. package/packages/core/src/devAdmin.ts +79 -32
  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 +6 -7
  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 +294 -106
  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 +1 -1
  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 +34 -16
  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 +886 -421
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  34. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  35. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
  36. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  37. package/packages/core/src/sessionHandlers/respClient.ts +16 -147
  38. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  39. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  40. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  41. package/packages/core/src/testClient.ts +18 -5
  42. package/packages/core/src/trustedProxy.ts +249 -0
  43. package/packages/core/src/types.ts +29 -5
  44. package/packages/core/src/websocket.ts +66 -0
  45. package/packages/orm/dist/index.js +22717 -20168
  46. package/packages/orm/src/adapters/firebird.ts +183 -56
  47. package/packages/orm/src/adapters/mongodb.ts +25 -4
  48. package/packages/orm/src/adapters/mssql.ts +114 -29
  49. package/packages/orm/src/adapters/mysql.ts +103 -40
  50. package/packages/orm/src/adapters/odbc.ts +44 -21
  51. package/packages/orm/src/adapters/postgres.ts +118 -26
  52. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  53. package/packages/orm/src/adapters/sqlite.ts +60 -24
  54. package/packages/orm/src/autoCrud.ts +12 -10
  55. package/packages/orm/src/baseModel.ts +135 -40
  56. package/packages/orm/src/cachedDatabase.ts +43 -19
  57. package/packages/orm/src/connectTimeout.ts +265 -0
  58. package/packages/orm/src/database.ts +241 -197
  59. package/packages/orm/src/databaseResult.ts +51 -28
  60. package/packages/orm/src/databaseUrl.ts +484 -0
  61. package/packages/orm/src/docstore.ts +386 -145
  62. package/packages/orm/src/index.ts +13 -6
  63. package/packages/orm/src/migration.ts +44 -11
  64. package/packages/orm/src/model.ts +4 -0
  65. package/packages/orm/src/queryBuilder.ts +47 -6
  66. package/packages/orm/src/sqlTranslator.ts +310 -4
  67. package/packages/orm/src/types.ts +21 -77
  68. package/packages/swagger/dist/index.js +78 -20
  69. package/packages/swagger/src/generator.ts +172 -29
  70. package/types/core/src/ai.d.ts +1 -1
  71. package/types/core/src/auth.d.ts +28 -5
  72. package/types/core/src/background.d.ts +3 -3
  73. package/types/core/src/cache.d.ts +15 -12
  74. package/types/core/src/dispatchPipeline.d.ts +117 -0
  75. package/types/core/src/dotenv.d.ts +38 -16
  76. package/types/core/src/index.d.ts +6 -9
  77. package/types/core/src/logger.d.ts +93 -16
  78. package/types/core/src/messenger.d.ts +47 -6
  79. package/types/core/src/metrics.d.ts +25 -61
  80. package/types/core/src/middleware.d.ts +134 -11
  81. package/types/core/src/queue.d.ts +54 -5
  82. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
  83. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  84. package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
  85. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
  86. package/types/core/src/router.d.ts +14 -3
  87. package/types/core/src/server.d.ts +15 -4
  88. package/types/core/src/session.d.ts +87 -2
  89. package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
  90. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  91. package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
  92. package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
  93. package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
  94. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  95. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  96. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  97. package/types/core/src/trustedProxy.d.ts +44 -0
  98. package/types/core/src/types.d.ts +28 -5
  99. package/types/core/src/websocket.d.ts +26 -0
  100. package/types/orm/src/adapters/firebird.d.ts +55 -10
  101. package/types/orm/src/adapters/mongodb.d.ts +2 -2
  102. package/types/orm/src/adapters/mssql.d.ts +18 -11
  103. package/types/orm/src/adapters/mysql.d.ts +11 -10
  104. package/types/orm/src/adapters/odbc.d.ts +9 -12
  105. package/types/orm/src/adapters/postgres.d.ts +11 -10
  106. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  107. package/types/orm/src/adapters/sqlite.d.ts +15 -3
  108. package/types/orm/src/baseModel.d.ts +45 -9
  109. package/types/orm/src/cachedDatabase.d.ts +18 -5
  110. package/types/orm/src/connectTimeout.d.ts +100 -0
  111. package/types/orm/src/database.d.ts +78 -28
  112. package/types/orm/src/databaseResult.d.ts +29 -15
  113. package/types/orm/src/databaseUrl.d.ts +125 -0
  114. package/types/orm/src/docstore.d.ts +102 -43
  115. package/types/orm/src/index.d.ts +6 -4
  116. package/types/orm/src/migration.d.ts +4 -3
  117. package/types/orm/src/queryBuilder.d.ts +23 -3
  118. package/types/orm/src/sqlTranslator.d.ts +126 -2
  119. package/types/orm/src/types.d.ts +21 -38
  120. package/packages/core/src/scss.ts +0 -623
  121. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
  122. package/types/core/src/scss.d.ts +0 -19
  123. package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, IncomingHttpHeaders } from "node:http";
2
2
  import type { Tina4Request, UploadedFile } from "./types.js";
3
+ import { resolveClientIp } from "./trustedProxy.js";
3
4
 
4
5
  /**
5
6
  * Wrap Node's `IncomingHttpHeaders` in a Proxy so mixed-case lookups
@@ -102,15 +103,11 @@ export function createRequest(req: IncomingMessage): Tina4Request {
102
103
  }
103
104
  tReq.cookies = cookies;
104
105
 
105
- // Determine client IP with X-Forwarded-For support
106
- const forwarded = req.headers["x-forwarded-for"];
107
- if (typeof forwarded === "string") {
108
- tReq.ip = forwarded.split(",")[0].trim();
109
- } else if (Array.isArray(forwarded) && forwarded.length > 0) {
110
- tReq.ip = forwarded[0].split(",")[0].trim();
111
- } else {
112
- tReq.ip = req.socket?.remoteAddress ?? "127.0.0.1";
113
- }
106
+ // Raw socket peer NEVER honours a forwarding header, so it can be trusted
107
+ // for security decisions. Resolved BEFORE .ip: the peer decides whether the
108
+ // forwarding headers may be believed at all (TINA4_TRUSTED_PROXIES, ADR-0019).
109
+ tReq.remoteIp = req.socket?.remoteAddress ?? "";
110
+ tReq.ip = resolveClientIp(req.headers, tReq.remoteIp) || "127.0.0.1";
114
111
 
115
112
  // Add convenience methods
116
113
  tReq.header = function (name: string): string | undefined {
@@ -163,19 +160,40 @@ async function parseBody(req: Tina4Request): Promise<void> {
163
160
  const chunks: Buffer[] = [];
164
161
 
165
162
  await new Promise<void>((resolve, reject) => {
166
- req.on("data", (chunk: Buffer) => chunks.push(chunk));
167
- req.on("end", resolve);
163
+ // A RUNNING cap, checked per chunk.
164
+ //
165
+ // The content-length check above only sees what the client DECLARES. A
166
+ // chunked request declares nothing, so declaredLength is 0 and it sails
167
+ // through. Without this counter the body is buffered in full and only
168
+ // then measured, which means the limit cannot prevent the thing it exists
169
+ // to prevent. Measured against a 1MB limit: a 40MB chunked POST with no
170
+ // content-length was accepted whole and grew the server's RSS by exactly
171
+ // 40.0MB before it was refused.
172
+ //
173
+ // Same defect as PHP's unbounded read buffer, and the same fix: stop at
174
+ // the limit instead of measuring the damage afterwards.
175
+ let received = 0;
176
+ let refused = false;
177
+ req.on("data", (chunk: Buffer) => {
178
+ if (refused) return;
179
+ received += chunk.length;
180
+ if (received > TINA4_MAX_UPLOAD_SIZE) {
181
+ refused = true;
182
+ chunks.length = 0; // drop what we have; the request is dead
183
+ reject(new PayloadTooLargeError(received, TINA4_MAX_UPLOAD_SIZE));
184
+ return;
185
+ }
186
+ chunks.push(chunk);
187
+ });
188
+ req.on("end", () => {
189
+ if (!refused) resolve();
190
+ });
168
191
  req.on("error", reject);
169
192
  });
170
193
 
171
194
  const raw = Buffer.concat(chunks);
172
195
  if (raw.length === 0) return;
173
196
 
174
- // Check actual body size against upload size limit
175
- if (raw.length > TINA4_MAX_UPLOAD_SIZE) {
176
- throw new PayloadTooLargeError(raw.length, TINA4_MAX_UPLOAD_SIZE);
177
- }
178
-
179
197
  if (contentType.includes("multipart/form-data")) {
180
198
  const boundary = extractBoundary(contentType);
181
199
  if (boundary) {
@@ -293,11 +293,56 @@ export function createResponse(res: ServerResponse): Tina4Response {
293
293
  return response.json({ error: true, code, message, status: statusCode }, statusCode);
294
294
  };
295
295
 
296
- response.file = function (filePath: string, options?: { download?: boolean; contentType?: string }): Tina4Response {
296
+ response.file = function (
297
+ filePath: string,
298
+ options?: { download?: boolean; contentType?: string; root?: string },
299
+ ): Tina4Response {
297
300
  if (res.headersSent) return response;
298
301
 
302
+ // SECURITY: confine the read. The natural spelling of a download route,
303
+ //
304
+ // response.file("downloads/" + name) // name = "../secret.env"
305
+ //
306
+ // served any file the process could read - measured over real HTTP at 200
307
+ // with the contents of a .env one directory above the intended one.
308
+ //
309
+ // TWO checks. Containment ALONE does not close it: that payload resolves to
310
+ // <project>/secret.env, which IS inside the project root, and the project
311
+ // root is exactly where .env lives. Rejecting ".." on the way in is the
312
+ // check that closes it; containment then catches absolute paths and
313
+ // symlinks, neither of which carries a ".." segment. Same shape as
314
+ // static.ts's startsWith guard, which this function never had.
315
+ // Containment ONLY when a root is declared. Defaulting to cwd broke every
316
+ // legitimate absolute path.
317
+ const base = options?.root ? nodePath.resolve(options.root) : null;
318
+ let forbidden = filePath.split(/[\\/]/).includes("..");
319
+
320
+ if (!forbidden) {
321
+ const candidate = (base === null || nodePath.isAbsolute(filePath)) ? filePath : nodePath.join(base, filePath);
322
+ let resolved = candidate;
323
+ try {
324
+ resolved = fs.realpathSync(candidate);
325
+ } catch {
326
+ /* missing file: fall through to the 404 below with the joined path */
327
+ }
328
+ if (base !== null && base !== nodePath.sep && resolved !== base && !resolved.startsWith(base + nodePath.sep)) {
329
+ forbidden = true;
330
+ } else {
331
+ filePath = resolved;
332
+ }
333
+ }
334
+
335
+ if (forbidden) {
336
+ // Refuse BEFORE reading: never load bytes we will not send.
337
+ res.statusCode = 403;
338
+ safeSetHeader("Content-Type", "text/plain");
339
+ safeEnd("Forbidden");
340
+ return response;
341
+ }
342
+
299
343
  if (!fs.existsSync(filePath)) {
300
344
  res.statusCode = 404;
345
+ safeSetHeader("Content-Type", "text/plain");
301
346
  safeEnd("File not found");
302
347
  return response;
303
348
  }
@@ -1,5 +1,6 @@
1
1
  import type { RouteHandler, RouteDefinition, RouteMeta, Middleware, MiddlewareSpec, Tina4Request, Tina4Response, WebSocketRouteHandler, WebSocketRouteDefinition } from "./types.js";
2
2
  import { isTruthy } from "./dotenv.js";
3
+ import { MiddlewareRunner, isMiddlewareClass } from "./middleware.js";
3
4
 
4
5
  /**
5
6
  * Whether `TINA4_TRAILING_SLASH_REDIRECT` is enabled.
@@ -821,16 +822,29 @@ export async function resolveStringMiddleware(spec: string): Promise<Middleware>
821
822
  /**
822
823
  * Resolve a single route-middleware spec to a middleware function. Functions
823
824
  * pass through unchanged; strings are resolved via resolveStringMiddleware.
825
+ * A middleware CLASS never reaches here — runRouteMiddlewares runs it through
826
+ * the MiddlewareRunner instead.
824
827
  */
825
828
  async function resolveMiddlewareSpec(spec: MiddlewareSpec): Promise<Middleware> {
826
- return typeof spec === "string" ? resolveStringMiddleware(spec) : spec;
829
+ return typeof spec === "string" ? resolveStringMiddleware(spec) : spec as Middleware;
827
830
  }
828
831
 
829
832
  /**
830
- * Run per-route middleware chain, then call the handler.
833
+ * Run the per-route middleware chain. Returns false when it short-circuited
834
+ * and the handler must be skipped.
831
835
  *
832
- * Accepts middleware functions and/or string specs (e.g. "ResponseCache:300").
833
- * Each spec is resolved to a middleware function just before it runs.
836
+ * Accepts middleware functions, middleware CLASSES, and string specs
837
+ * (e.g. "ResponseCache:300"). A function or string spec is resolved and
838
+ * invoked as `mw(req, res, next)` exactly as before.
839
+ *
840
+ * A CLASS runs its beforeX hooks through the SAME `MiddlewareRunner.runBefore`
841
+ * and the SAME return-value table as global middleware — no parallel runner.
842
+ * Its afterX hooks run with the global after pass once the handler is done
843
+ * (server.ts / testClient.ts append the route's classes to that list), because
844
+ * "after" means after the handler, not after this function. Every spec used to
845
+ * be invoked as `mw(req, res, next)`, which for a class throws "Class
846
+ * constructor cannot be invoked without 'new'", so a class attached per-route
847
+ * was inert. Python and PHP both ran per-route class hooks already.
834
848
  */
835
849
  export async function runRouteMiddlewares(
836
850
  middlewares: MiddlewareSpec[],
@@ -838,6 +852,17 @@ export async function runRouteMiddlewares(
838
852
  res: Tina4Response,
839
853
  ): Promise<boolean> {
840
854
  for (const spec of middlewares) {
855
+ if (isMiddlewareClass(spec)) {
856
+ // tina4: a class hook that returns a DIFFERENT [req, res] pair cannot be
857
+ // rebound here — this returns a bool and is public API (four of this
858
+ // repo's own test files import it). Node's req/res are per-request
859
+ // singletons, so every built-in returns the same pair back; widen the
860
+ // return type if that ever stops being true.
861
+ const [, , proceed] = await MiddlewareRunner.runBefore([spec], req, res);
862
+ if (!proceed || res.raw.writableEnded) return false;
863
+ continue;
864
+ }
865
+
841
866
  const mw = await resolveMiddlewareSpec(spec);
842
867
  let nextCalled = false;
843
868
  await mw(req, res, () => {