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
@@ -70,6 +70,20 @@ interface CacheBackend {
70
70
  set(key: string, value: unknown, ttl: number): Promise<void>;
71
71
  delete(key: string): Promise<boolean>;
72
72
  clear(): Promise<void>;
73
+ /**
74
+ * Evict expired entries and return HOW MANY were actually evicted.
75
+ *
76
+ * REQUIRED, not optional. It used to be neither declared nor implemented, so
77
+ * the module-level sweep() found no backend method and returned a permanent
78
+ * 0: the one API whose job is reclaiming expired space did nothing and
79
+ * reported success. Declaring it here makes "every provider can sweep" a
80
+ * compile-time fact instead of a runtime hope.
81
+ *
82
+ * 0 is the HONEST answer on redis/valkey/memcached/mongodb - they expire
83
+ * entries server-side, so there is nothing left for us to evict. It is the
84
+ * WRONG answer for memory, file and database, which own their own expiry.
85
+ */
86
+ sweep(): Promise<number>;
73
87
  stats(): Promise<{
74
88
  hits: number;
75
89
  misses: number;
@@ -107,18 +121,7 @@ export declare function createBackend(config?: {
107
121
  cacheDir?: string;
108
122
  maxEntries?: number;
109
123
  }): Promise<CacheBackend>;
110
- /**
111
- * Response cache middleware for GET requests.
112
- * Caches the full response body, content-type, and status code through the
113
- * unified async backend. Cache key is method + url (including query string).
114
- *
115
- * The middleware is ASYNC: the before-path awaits `backend.get` (serve hit) and
116
- * the after-path awaits `backend.set` (store on the captured `res.raw.end`).
117
- * The framework's middleware chain (`runRouteMiddlewares` / `MiddlewareChain`)
118
- * already awaits middleware, so async is transparent. Honors ttl/statusCodes/
119
- * maxEntries. With the default `memory` backend behaviour is unchanged; a
120
- * redis/etc. backend distributes cross-instance.
121
- */
124
+ export declare function _getResponseBackend(config?: ResponseCacheConfig): Promise<CacheBackend>;
122
125
  export declare function responseCache(config?: ResponseCacheConfig): Middleware;
123
126
  /**
124
127
  * Clear all cached responses (the responseCache middleware backend).
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The dispatch pipeline: the concerns of `dispatch`, named and extracted.
3
+ *
4
+ * `dispatch` was a 485-line closure at cyclomatic complexity 65 against a
5
+ * ceiling of 10, on the path of every request, nested inside `startServer`
6
+ * (which is why that measured 45 as well). These are its concerns as
7
+ * standalone functions, so each can be read and tested without standing up a
8
+ * server.
9
+ *
10
+ * PROLOGUE_STAGES run before anything else, in order. They are extracted FIRST
11
+ * because they close over nothing from `startServer` - only the raw
12
+ * request/response - so no context object is needed for them at all. The later
13
+ * stages need `router`, `staticDir`, `port` and `middleware`, and follow.
14
+ * `sessionAutoStart` is the one prologue stage that runs INSIDE the dispatch
15
+ * try-block, so a TINA4_SESSION_STRICT refusal renders a 500 like every other
16
+ * request error instead of rejecting `dispatch` into an unhandled rejection
17
+ * that takes the worker down (ADR-0021; parity with Python, where the raise
18
+ * leaves the request path and the ASGI server turns it into a 500).
19
+ *
20
+ * Ordering here is BEHAVIOUR, not taste:
21
+ * * `headStripIntercept` MUST run before anything can write. Node streams its
22
+ * response, so there is no single exit point to strip at - the interception
23
+ * IS the mechanism (ADR-0011: the CONTRACT is the outcome, and Ruby and
24
+ * Python satisfy it by stripping late at their single return instead).
25
+ * * `sessionAutoStart` wraps `end` after that, so its save-and-set-cookie
26
+ * runs on the real `end` rather than on the HEAD interceptor's.
27
+ *
28
+ * @see tina4-ruby/lib/tina4/dispatch_pipeline.rb - the same extraction, and
29
+ * the source of the stage-list-as-data pattern.
30
+ */
31
+ import type { IncomingMessage, ServerResponse } from "node:http";
32
+ import type { Tina4Request } from "./types.js";
33
+ /**
34
+ * The prologue, in order. Exported as DATA so the pipeline can be asserted and
35
+ * compared across frameworks without reading an implementation.
36
+ */
37
+ export declare const PROLOGUE_STAGES: readonly ["resetRequestCaches", "headStripIntercept", "sessionAutoStart"];
38
+ /**
39
+ * After the prologue, before a route is looked up.
40
+ *
41
+ * `wrapResponseEnd` MUST come before the global pass: it installs the end()
42
+ * wrapper that injects the dev toolbar and captures the request, and a
43
+ * middleware that short-circuits still has to be captured.
44
+ */
45
+ export declare const REQUEST_STAGES: readonly ["blockAiPortReload", "wrapResponseEnd", "runGlobalMiddlewarePass"];
46
+ /**
47
+ * A matched route, in order - and the order is BEHAVIOUR (ADR-0012):
48
+ * POST-MATCH globals -> auth gate -> the route's OWN middleware -> handler.
49
+ *
50
+ * The globals run BEFORE the gate so a rate limiter can throttle a brute-force
51
+ * login and an access log records the 401 - neither is possible if they only
52
+ * run on authenticated requests. The route's own middleware stays AFTER the
53
+ * gate, so middleware attached to a secured route never processes an
54
+ * unauthenticated request.
55
+ *
56
+ * `runGlobalMiddlewarePass` appears here AND in REQUEST_STAGES on purpose:
57
+ * one function, two phases. That split IS ADR-0012.
58
+ */
59
+ export declare const ROUTE_STAGES: readonly ["runGlobalMiddlewarePass", "enforceRouteAuth", "runRouteMiddlewares", "invokeRouteHandler", "renderIfTemplateRoute"];
60
+ /**
61
+ * Nothing matched a route: the fallback chain, walked until one answers.
62
+ *
63
+ * Order is BEHAVIOUR: a template beats the landing page (so a project's own
64
+ * pages/index.twig wins at "/"), 405 beats static (a known path with the wrong
65
+ * method is not a missing file), and the 404 is terminal.
66
+ *
67
+ * This chain runs AFTER matching because routes beat files (ADR-0010): a file
68
+ * from a build step or a careless deploy must never shadow a reviewed route.
69
+ *
70
+ * server.ts holds the same order as an array of the real FUNCTIONS - that is
71
+ * what dispatch actually walks. dispatchPipeline.test.ts asserts the two agree,
72
+ * so this list cannot drift from the runner.
73
+ */
74
+ export declare const FALLBACK_STAGES: readonly ["serveTemplateFallback", "serveLandingPage", "serveMethodNotAllowed", "serveStaticAsset", "serveNotFound"];
75
+ /** The catch arm. Everything above throws into this one. */
76
+ export declare const ERROR_STAGES: readonly ["renderDispatchError"];
77
+ /**
78
+ * Request-scoped DB query cache boundary.
79
+ *
80
+ * Clears the request-scoped cache on every live connection at the START of each
81
+ * request so it never serves rows across requests (persistent-mode connections
82
+ * are left alone). The ORM is loaded lazily and may be absent, so this is
83
+ * best-effort: a failure here must never break a request. Mirrors Python's
84
+ * dispatcher calling `Database.reset_request_caches()`.
85
+ */
86
+ export declare function resetRequestCaches(): Promise<void>;
87
+ /**
88
+ * RFC 9110 s9.3.2: the server MUST NOT send content in a HEAD response.
89
+ *
90
+ * Intercepts `write` / `end` so every code path - an explicit `Router.head()`
91
+ * handler, the GET auto-fallback, 405 and 404 responses - drops its body.
92
+ * Content-Length is preserved when present, so cache validators, link checkers
93
+ * and monitoring probes still see the size the equivalent GET would have sent.
94
+ *
95
+ * No-op for any method other than HEAD.
96
+ *
97
+ * @param rawReq Node's incoming message, read for the method
98
+ * @param rawRes Node's server response, whose write/end are replaced in place
99
+ */
100
+ export declare function headStripIntercept(rawReq: IncomingMessage, rawRes: ServerResponse): void;
101
+ /**
102
+ * Auto-start the session: read the cookie, create the session, then save it and
103
+ * set the cookie when the response ends.
104
+ *
105
+ * The incoming cookie is read by the SAME configured name the write side emits
106
+ * (`TINA4_SESSION_NAME`, default `tina4_session`) via the shared
107
+ * `sessionCookieName()` resolver - otherwise a renamed cookie would be written
108
+ * but never read back and the session would silently never resume. A whole
109
+ * cookie pair is matched by its exact `name=` prefix (split on ";", trim,
110
+ * startsWith) so `tina4_session` never matches `tina4_session_foo=` nor a value
111
+ * mid-header. Parity with Python `core/server._init_session`.
112
+ *
113
+ * @param rawReq Node's incoming message, read for cookies and the proxy scheme
114
+ * @param rawRes Node's server response, whose `end` is wrapped
115
+ * @param req The Tina4 request the session is attached to
116
+ */
117
+ export declare function sessionAutoStart(rawReq: IncomingMessage, rawRes: ServerResponse, req: Tina4Request): Promise<void>;
@@ -1,23 +1,33 @@
1
1
  /**
2
- * Load environment variables from a .env file into process.env.
2
+ * Load environment variables from a root DIRECTORY or a single .env file.
3
3
  *
4
- * By default does NOT override existing process.env values it is first-wins:
5
- * a key is only set if it is not already present. This is how real env vars
6
- * always win. To get the precedence real-env > `.env.local` > `.env`, load
7
- * `.env.local` FIRST then `.env`, both with override=false (the default): the
8
- * real env (already present) wins over both, `.env.local` fills local-only keys,
9
- * and `.env` fills the rest. Do NOT load `.env.local` with override=true — that
10
- * would let a stray gitignored `.env.local` clobber an explicitly set real env
11
- * var (e.g. a production TINA4_SECRET).
4
+ * Pass a **directory** and it loads `<dir>/.env.local` then `<dir>/.env`, both
5
+ * first-wins, which IS the precedence real-env > `.env.local` > `.env`. That is
6
+ * the canonical form in all four frameworks.
12
7
  *
13
- * Resolution order for the env file path:
14
- * 1. Explicit `path` argument
15
- * 2. `TINA4_ENV_FILE` env var (if set and non-empty)
16
- * 3. `.env` in the current working directory
8
+ * Before this, the ordering was the CALLER's job and this doc comment was the
9
+ * only place it was written down: load `.env.local` first, then `.env`, both
10
+ * with override=false. Every caller had to remember, and getting it wrong
11
+ * (override=true on `.env.local`) lets a stray gitignored file clobber an
12
+ * explicitly set real env var such as a production TINA4_SECRET. A rule nobody
13
+ * can forget beats a rule written in a comment.
17
14
  *
18
- * @param path - Path to the .env file. Optional override.
15
+ * A **file** path still works exactly as before: only that file is read, and the
16
+ * caller owns the ordering.
17
+ *
18
+ * By default this does NOT override existing process.env values — it is
19
+ * first-wins, which is how a real env var always beats both files.
20
+ *
21
+ * Resolution order when `path` is omitted:
22
+ * 1. `TINA4_ENV_FILE` env var (if set and non-empty) — the named file, plus
23
+ * `.env.local` BESIDE it, so pointing at `.env.staging` does not silently
24
+ * stop honouring local overrides
25
+ * 2. the current working directory, as a root
26
+ *
27
+ * @param path - A root directory (canonical) OR a path to a single .env file.
19
28
  * @param override - When true, overwrite keys already present in process.env.
20
- * @returns The parsed key-value pairs, or an empty object if the file doesn't exist.
29
+ * @returns The parsed key-value pairs. For the directory form this is the merge
30
+ * of both files, with `.env.local` winning on a duplicate key.
21
31
  */
22
32
  export declare function loadEnv(path?: string, override?: boolean): Record<string, string>;
23
33
  /**
@@ -35,7 +45,19 @@ export declare function getEnv(key: string, defaultValue?: string): string | und
35
45
  * @returns The environment variable value.
36
46
  * @throws Error if the variable is not set.
37
47
  */
38
- export declare function requireEnv(key: string): string;
48
+ /**
49
+ * Validate that required environment variables exist, and return them.
50
+ *
51
+ * Takes VARARGS and returns a map, matching Python, PHP and Ruby. It used to
52
+ * take one key and return that value, so checking five variables meant five
53
+ * calls that each failed on the first problem - an operator fixing a deployment
54
+ * got one name per restart instead of the whole list.
55
+ *
56
+ * @param keys - Variable names that must be set.
57
+ * @returns Every requested key mapped to its value.
58
+ * @throws Error naming ALL missing variables, not just the first.
59
+ */
60
+ export declare function requireEnv(...keys: string[]): Record<string, string>;
39
61
  /**
40
62
  * Check if an environment variable exists (is defined in process.env).
41
63
  *
@@ -1,4 +1,4 @@
1
- export type { Tina4Request, Tina4Response, RouteHandler, RouteDefinition, RouteMeta, Tina4Config, Middleware, MiddlewareSpec, UploadedFile, CookieOptions, WebSocketRouteHandler, WebSocketRouteDefinition, } from "./types.js";
1
+ export type { Tina4Request, Tina4Response, RouteHandler, RouteDefinition, RouteMeta, Tina4Config, Middleware, MiddlewareClass, MiddlewareSpec, UploadedFile, CookieOptions, WebSocketRouteHandler, WebSocketRouteDefinition, } from "./types.js";
2
2
  export { startServer, resolvePortAndHost, handle, start, stop, httpReason, resolveTemplate, resetTemplateCache, templateAutoRoutingEnabled, isBannerSuppressed } from "./server.js";
3
3
  export { background, stopAllBackgroundTasks, backgroundTaskCount } from "./background.js";
4
4
  export { Router, RouteGroup, RouteRef, WsRouteRef, defaultRouter, runRouteMiddlewares, resolveStringMiddleware, isTrailingSlashRedirectEnabled } from "./router.js";
@@ -15,15 +15,14 @@ export { Env } from "./env.js";
15
15
  export { Log } from "./logger.js";
16
16
  export { createHealthRoute, createHealthRoutes, healthPath } from "./health.js";
17
17
  export { rateLimiter } from "./rateLimiter.js";
18
+ export { isTrustedProxy, trustedProxyNetworks, resolveClientIp, resetTrustedProxyCache } from "./trustedProxy.js";
18
19
  export type { RateLimiterConfig } from "./rateLimiter.js";
19
20
  export { HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, HTTP_NO_CONTENT, HTTP_MOVED, HTTP_REDIRECT, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_CONFLICT, HTTP_GONE, HTTP_UNPROCESSABLE, HTTP_TOO_MANY, HTTP_SERVER_ERROR, HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, APPLICATION_JSON, APPLICATION_XML, APPLICATION_FORM, APPLICATION_OCTET, TEXT_HTML, TEXT_PLAIN, TEXT_CSV, TEXT_XML, } from "./constants.js";
20
- export { getToken, validToken, getPayload, hashPassword, checkPassword, authMiddleware, refreshToken, authenticateRequest, validateApiKey, ensureDevSecret, Auth, } from "./auth.js";
21
- export { Session, FileSessionHandler, RedisSessionHandler, buildSessionCookie, isSecureScheme, sessionCookieName } from "./session.js";
21
+ export { getToken, validToken, getPayload, hashPassword, checkPassword, authMiddleware, refreshToken, authenticateRequest, validateApiKey, ensureDevSecret, resolveAlgorithm, algorithmAvailable, availableAlgorithms, Auth, } from "./auth.js";
22
+ export { Session, FileSessionHandler, RedisSessionHandler, buildSessionCookie, isSecureScheme, isValidSessionId, sessionCookieName, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS } from "./session.js";
22
23
  export type { SessionConfig, SessionHandler } from "./session.js";
23
24
  export { I18n } from "./i18n.js";
24
25
  export { FakeData } from "./fakeData.js";
25
- export { ScssCompiler } from "./scss.js";
26
- export type { ScssConfig } from "./scss.js";
27
26
  export { Queue } from "./queue.js";
28
27
  export type { QueueConfig, QueueJob, ProcessOptions } from "./queue.js";
29
28
  export { createJob } from "./job.js";
@@ -38,7 +37,7 @@ export { WebSocketServer, devReloadWs, computeAcceptKey, parseUpgradeHeaders, bu
38
37
  export type { WebSocketClient } from "./websocket.js";
39
38
  export { ServiceRunner, Tina4Service, matchCronField, matchesCron } from "./service.js";
40
39
  export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
41
- export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, createBackend, _resetBackend } from "./cache.js";
40
+ export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, sweep, createBackend, _resetBackend } from "./cache.js";
42
41
  export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
43
42
  export { Api } from "./api.js";
44
43
  export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions } from "./api.js";
@@ -56,7 +55,7 @@ export { HtmlElement, htmlElement, addHtmlHelpers, Raw, SafeString } from "./htm
56
55
  export { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorOverlay.js";
57
56
  export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
58
57
  export type { AiTool } from "./ai.js";
59
- export type { ImapMessage, ImapFullMessage } from "./messenger.js";
58
+ export type { ImapMessage, ImapFullMessage, ImapAttachment } from "./messenger.js";
60
59
  export { LiteBackend } from "./queueBackends/liteBackend.js";
61
60
  export { RabbitMQBackend, parseAmqpUrl } from "./queueBackends/rabbitmqBackend.js";
62
61
  export type { RabbitMQConfig } from "./queueBackends/rabbitmqBackend.js";
@@ -70,8 +69,6 @@ export { MongoSessionHandler } from "./sessionHandlers/mongoHandler.js";
70
69
  export type { MongoSessionConfig } from "./sessionHandlers/mongoHandler.js";
71
70
  export { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
72
71
  export type { ValkeySessionConfig } from "./sessionHandlers/valkeyHandler.js";
73
- export { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
74
- export type { RedisNpmSessionConfig } from "./sessionHandlers/redisHandler.js";
75
72
  export { tests, assertEqual, assertRaises, assertTrue, assertFalse, runAll, reset } from "./testing.js";
76
73
  export { TestClient, TestResponse } from "./testClient.js";
77
74
  export { Tina4Test, AssertionError as Tina4AssertionError } from "./test.js";
@@ -1,23 +1,36 @@
1
1
  /**
2
2
  * Structured logger for Tina4.
3
3
  *
4
- * Development (TINA4_DEBUG=true): colorized human-readable to stdout + file.
5
- * Production (TINA4_DEBUG not truthy): clean structured JSON to stdout ONLY
6
- * no log file by default (writing logs/tina4.log inside a container bloats the
7
- * writable layer + disk; 12-factor wants logs on stdout). stdout is ALWAYS on.
4
+ * FORMAT IS TEXT BY DEFAULT, and TINA4_LOG_FORMAT=json is the ONLY thing that
5
+ * selects JSON. Nothing else may. Until 3.13.95 an unset TINA4_DEBUG silently
6
+ * flipped BOTH sinks to JSON here, and "production" meant four different things
7
+ * across the four frameworks (Node: !TINA4_DEBUG; Ruby: TINA4_ENV/RACK_ENV/
8
+ * RUBY_ENV == "production"; Python: only configure(production=True); PHP: no
9
+ * switch at all, JSON always) — same machine, same .env, four log formats. That
10
+ * implicit switch is deleted; an object passed as the message is still
11
+ * JSON-encoded INLINE inside the text line, which is the only JSON a default
12
+ * install emits.
13
+ *
14
+ * TINA4_DEBUG still decides COLOUR — a terminal concern, not a format one — so
15
+ * a production pipe gets clean uncoloured bytes and a dev terminal stays
16
+ * readable.
8
17
  *
9
18
  * Default file-output rule (TINA4_LOG_OUTPUT unset): the log FILE is written
10
19
  * only in development. An explicit TINA4_LOG_OUTPUT=file/both, OR an explicit
11
- * TINA4_LOG_FILE path, always forces a file (explicit wins).
20
+ * TINA4_LOG_FILE path, always forces a file (explicit wins). stdout is ALWAYS on.
12
21
  *
13
22
  * Env vars:
14
23
  * TINA4_LOG_FILE — explicit log file (absolute or relative). Setting it forces a file even in production. Empty = use TINA4_LOG_DIR + tina4.log
15
24
  * TINA4_LOG_DIR — directory for log files (default: "logs")
16
- * TINA4_LOG_FORMAT — "text" | "json" (default: "text")
25
+ * TINA4_LOG_FORMAT — "text" | "json" (default: "text") — the ONLY format switch
17
26
  * TINA4_LOG_OUTPUT — "stdout" | "file" | "both" (default: "stdout" → file only in dev)
18
27
  * TINA4_LOG_ROTATE_SIZE — bytes; 0 disables rotation (default: 10485760 = 10MB)
19
28
  * TINA4_LOG_ROTATE_KEEP — number of historical files to keep (default: 5)
20
29
  * TINA4_LOG_LEVEL — minimum console level: DEBUG | INFO | WARNING | ERROR | CRITICAL (default: "INFO")
30
+ * TINA4_LOG_STRICT — truthy: a log-write failure THROWS instead of being swallowed (default: off)
31
+ *
32
+ * Every one of these is read LAZILY, on each log() call — a script, worker, CLI
33
+ * tool or test that never boots a server still gets the operator's configuration.
21
34
  *
22
35
  * Rotation is stdlib roll-your-own:
23
36
  * - On each write, statSync the file. If size >= TINA4_LOG_ROTATE_SIZE, rotate.
@@ -27,6 +40,21 @@
27
40
  */
28
41
  export declare class Log {
29
42
  private static requestId;
43
+ /**
44
+ * What configure() was explicitly told, held HERE rather than written back
45
+ * into process.env (ADR-0041).
46
+ *
47
+ * configure() used to assign to process.env.TINA4_LOG_DIR / _LOG_FILE. That
48
+ * reached the right answer -- the argument won -- through a mechanism no
49
+ * other framework has: it DESTROYED the operator's value for the rest of the
50
+ * process, and every child process spawned afterwards inherited the
51
+ * argument instead of what the operator set. Reading configuration must not
52
+ * write it. Keeping the explicit values in their own slot means resolution
53
+ * is explicit > env > default with the environment left intact and still
54
+ * readable.
55
+ */
56
+ private static explicitLogDir;
57
+ private static explicitLogFile;
30
58
  /**
31
59
  * Re-read all log-related env vars. Called on every log() so tests that
32
60
  * mutate process.env between calls see the new values without having to
@@ -68,23 +96,65 @@ export declare class Log {
68
96
  */
69
97
  static getRequestId(): string | undefined;
70
98
  /**
71
- * Configure the log directory / filename. Mostly a no-op now —
72
- * env vars are re-read on every call. Kept for backwards compatibility.
99
+ * Configure where logs are written.
100
+ *
101
+ * Logs land in a `logs/` folder by default. The argument OVERRIDES that, and
102
+ * it accepts a DIRECTORY or a FILE PATH:
103
+ *
104
+ * configure() -> ./logs/tina4.log + ./logs/error.log
105
+ * configure("/var/log/myapp") -> /var/log/myapp/tina4.log + error.log
106
+ * configure("/var/log/myapp/app.log") -> that exact file (no error.log sibling)
107
+ * configure({ logDir, logFile }) -> the explicit object form still works
108
+ *
109
+ * A plain string used to be accepted and silently ignored, because only the
110
+ * object form was read - so the call that works in the other three
111
+ * frameworks produced no log file here and said nothing (feature 2 of the
112
+ * audit, D4). Both forms now work.
73
113
  */
74
- static configure(options: {
114
+ static configure(options?: string | {
75
115
  logDir?: string;
76
116
  logFile?: string;
77
117
  }): void;
118
+ /**
119
+ * Forget what configure() was told, so resolution falls back to the
120
+ * environment and then the built-in defaults. Parity with PHP's Log::reset().
121
+ *
122
+ * This exists because the explicit values are now HELD here rather than
123
+ * written back into process.env, and that makes them STICKY for the life of
124
+ * the process -- which is right for an application (configure() at boot is
125
+ * the operator's instruction and a later stray env write should not silently
126
+ * re-point the logs) and wrong for a long-lived test process that wants to
127
+ * drive the logger purely from the environment afterwards.
128
+ *
129
+ * I removed this method once for having no callers. That was correct about
130
+ * the grep and wrong about the code: the full suite is the caller. Before the
131
+ * explicit slots existed, configure() ASSIGNED to process.env, so a later
132
+ * direct assignment simply overwrote it and env-driven cases kept working by
133
+ * accident. test/logger.test.ts depends on exactly that -- it configures a
134
+ * file early, then runs the whole default-output block off the environment
135
+ * (see its own note: "these cases must NOT route through Log.configure").
136
+ * Three of those cases failed on the lab until this came back.
137
+ */
138
+ static reset(): void;
139
+ /**
140
+ * TINA4_LOG_APPEND — append (default) or overwrite on startup.
141
+ *
142
+ * APPEND IS THE DEFAULT: a log you can lose by restarting the process is not
143
+ * a log. Set it false for one file per run (a short CLI, a test fixture, a
144
+ * container shipping logs elsewhere); the files are truncated once here at
145
+ * configure time, never per line.
146
+ */
147
+ private static applyAppendMode;
78
148
  /** Log an informational message. */
79
- static info(message: string, data?: unknown): void;
149
+ static info(message: unknown, data?: unknown): void;
80
150
  /** Log a debug message. */
81
- static debug(message: string, data?: unknown): void;
151
+ static debug(message: unknown, data?: unknown): void;
82
152
  /** Log a warning message. */
83
- static warning(message: string, data?: unknown): void;
153
+ static warning(message: unknown, data?: unknown): void;
84
154
  /** Backwards-compat alias for warning(). */
85
- static warn(message: string, data?: unknown): void;
155
+ static warn(message: unknown, data?: unknown): void;
86
156
  /** Log an error message. */
87
- static error(message: string, data?: unknown): void;
157
+ static error(message: unknown, data?: unknown): void;
88
158
  /**
89
159
  * Log a critical message. CRITICAL is the highest severity (priority 4 >
90
160
  * error 3) and ALWAYS emits like every other level — subject only to the
@@ -93,7 +163,7 @@ export declare class Log {
93
163
  * critical 4 >= warning 2 so it would be in error.log on a split-file model).
94
164
  * Matches Python master parity — there is no enable toggle.
95
165
  */
96
- static critical(message: string, data?: unknown): void;
166
+ static critical(message: unknown, data?: unknown): void;
97
167
  /** Check if running in production mode (TINA4_DEBUG is not truthy). */
98
168
  private static isProduction;
99
169
  /** Get current ISO timestamp */
@@ -116,7 +186,14 @@ export declare class Log {
116
186
  * `rotateSize` of 0 disables rotation entirely.
117
187
  */
118
188
  private static rotateIfNeeded;
119
- /** Write a line to the log file, stripping ANSI codes. */
189
+ /**
190
+ * Write a line to the log file, stripping ANSI codes.
191
+ *
192
+ * A failure is swallowed by default — logging must never crash the app. With
193
+ * TINA4_LOG_STRICT truthy it is RE-THROWN instead: an app that believes it is
194
+ * writing an audit trail into a read-only directory, and is not, is worse off
195
+ * than one that dies at the first line. Same contract in all four frameworks.
196
+ */
120
197
  private static writeToFile;
121
198
  /** Core log method */
122
199
  private static log;
@@ -2,7 +2,13 @@ import { DevMailbox } from "./devMailbox.js";
2
2
  export interface SendResult {
3
3
  success: boolean;
4
4
  message: string;
5
- id?: string;
5
+ /**
6
+ * The real Message-ID on success, `null` on failure — but ALWAYS present, so a
7
+ * caller reading `result.id` gets one shape from both branches (G6). It used to
8
+ * be omitted on the failure path, handing back `undefined` there and a string on
9
+ * success.
10
+ */
11
+ id: string | null;
6
12
  }
7
13
  /**
8
14
  * Raised when an IMAP read fails to connect, authenticate, or speak the
@@ -62,15 +68,33 @@ export interface ImapMessage {
62
68
  snippet: string;
63
69
  seen: boolean;
64
70
  }
71
+ /**
72
+ * An attachment from a read() message. `content` is the RAW DECODED BYTES of the
73
+ * part (transfer-decoded from base64 / quoted-printable), the SAME convention as
74
+ * req.files[x].content — raw bytes, not base64 — so an attachment is downloadable
75
+ * as-is; `size` is that decoded byte length. Parity with Python's read()
76
+ * attachment dict {filename, content_type, size, content}, in Node's idiomatic
77
+ * camelCase (ADR-0008 / G5). #69 folded the bytes in HERE — there is no separate
78
+ * carrier (Python retired its attachments_data in 3.13.96).
79
+ */
80
+ export interface ImapAttachment {
81
+ filename: string;
82
+ contentType: string;
83
+ size: number;
84
+ content: Buffer;
85
+ }
65
86
  export interface ImapFullMessage {
66
87
  uid: string;
67
88
  subject: string;
68
89
  from: string;
69
90
  to: string;
70
91
  cc: string;
92
+ /** ISO-8601, parsed from the Date header (parity with Python's _iso_date). */
71
93
  date: string;
72
94
  bodyText: string;
73
95
  bodyHtml: string;
96
+ /** Attachments, each carrying its decoded bytes. Empty when the message has none (G5 / #69). */
97
+ attachments: ImapAttachment[];
74
98
  headers: Record<string, string>;
75
99
  }
76
100
  export declare class Messenger {
@@ -116,6 +140,13 @@ export declare class Messenger {
116
140
  /** The local mailbox, created on first capture and reused after. */
117
141
  private getDevMailbox;
118
142
  send(to: string | string[], subject: string, body: string, html?: boolean, text?: string, cc?: string | string[], bcc?: string | string[], replyTo?: string, attachments?: string[], headers?: Record<string, string>): Promise<SendResult>;
143
+ /**
144
+ * Render a Frond template STRING and send it as an HTML email (G7, parity with
145
+ * Python's send_template). Extra send() options (cc, bcc, replyTo, attachments,
146
+ * headers) pass through. If the Frond package cannot be loaded the raw template
147
+ * is sent verbatim (matches Python's ImportError fallback) rather than failing.
148
+ */
149
+ sendTemplate(to: string | string[], subject: string, template: string, data?: Record<string, unknown>, cc?: string | string[], bcc?: string | string[], replyTo?: string, attachments?: string[], headers?: Record<string, string>): Promise<SendResult>;
119
150
  /**
120
151
  * Test the SMTP connection without sending an email.
121
152
  */
@@ -136,23 +167,33 @@ export declare class Messenger {
136
167
  * Fetch latest messages from a folder.
137
168
  * Returns list of message summaries.
138
169
  */
139
- inbox(limit?: number, offset?: number, folder?: string): Promise<ImapMessage[]>;
170
+ inbox(folder?: string, limit?: number, offset?: number): Promise<ImapMessage[]>;
140
171
  /**
141
- * Read a single message by sequence number or UID.
172
+ * Read a single message by its IMAP UID.
142
173
  */
143
- read(uid: string, folder?: string): Promise<ImapFullMessage>;
174
+ read(uid: string, folder?: string): Promise<ImapFullMessage | null>;
144
175
  /**
145
176
  * Search messages using IMAP search criteria.
146
177
  */
147
178
  search(folder?: string, subject?: string, sender?: string, since?: string, before?: string, unseenOnly?: boolean, limit?: number): Promise<ImapMessage[]>;
148
179
  /**
149
- * Delete a message by UID.
180
+ * Delete a message by UID (mark \Deleted, then EXPUNGE).
181
+ *
182
+ * `delete` is the one cross-framework name (python/php/ruby/node all spell it
183
+ * `delete`). `deleteMessage` remains as a DEPRECATED alias for one release.
150
184
  */
185
+ delete(uid: string, folder?: string): Promise<void>;
186
+ /** @deprecated Use {@link delete} — kept as an alias for one release (G7). */
151
187
  deleteMessage(uid: string, folder?: string): Promise<void>;
152
188
  /**
153
- * Mark a message as read.
189
+ * Mark a message as read (+FLAGS \Seen).
154
190
  */
155
191
  markRead(uid: string, folder?: string): Promise<void>;
192
+ /**
193
+ * Mark a message as unread (-FLAGS \Seen) — the inverse of markRead (G7,
194
+ * parity with Python's mark_unread).
195
+ */
196
+ markUnread(uid: string, folder?: string): Promise<void>;
156
197
  /**
157
198
  * Count unseen messages in a folder.
158
199
  */
@@ -1,77 +1,41 @@
1
+ export declare function quickMetrics(root?: string): Record<string, any>;
1
2
  /**
2
- * Two-tier analysis:
3
- * 1. Quick metrics (instant): LOC, file counts, class/function counts
4
- * 2. Full analysis (on-demand, cached): cyclomatic complexity, maintainability
5
- * index, coupling, Halstead metrics, violations
3
+ * The native metrics engine could not produce a payload.
6
4
  *
7
- * Zero dependencies uses only Node.js built-in modules.
5
+ * Thrown instead of falling back to a second implementation.
8
6
  */
9
- interface FunctionInfo {
10
- name: string;
11
- line: number;
12
- complexity: number;
13
- loc: number;
14
- args: string[];
15
- file?: string;
7
+ export declare class MetricsEngineError extends Error {
8
+ constructor(message: string);
16
9
  }
10
+ export declare const SEVERITY_RANK: Record<string, number>;
17
11
  /**
18
- * Stop a function being charged for the complexity of the functions nested
19
- * inside it.
20
- *
21
- * Each function's raw score is measured over its whole span, so a branch inside
22
- * a nested function landed on BOTH that function and every function enclosing
23
- * it. The over-count compounded with depth: an IIFE wrapper or a registrar
24
- * defining twenty inner handlers absorbed the entire file's complexity and
25
- * topped the offenders list, hiding the genuine hot spots.
26
- *
27
- * The correction is exact. A raw score is 1 + every decision in the span, so
28
- * (raw - 1) is the total decision count of a function's whole subtree.
29
- * Subtracting that for each DIRECT child leaves the function's own branches:
12
+ * Return [directory to scan, scanMode] for any metrics producer.
30
13
  *
31
- * own(F) = raw(F) - sum over direct children C of (raw(C) - 1)
32
- *
33
- * Anything the extractor does NOT list is deliberately unaffected: nothing
34
- * subtracts it, so its decisions stay with the function that contains it -
35
- * moved, never lost.
14
+ * The engine is language-agnostic and cannot know which directory holds a
15
+ * framework package, so root resolution and the "framework" label stay here,
16
+ * shared by the census and the engine adapter so the two never disagree.
36
17
  */
37
- export declare function chargeNestedComplexityToTheNestedFunction(functions: FunctionInfo[]): FunctionInfo[];
38
- export declare function quickMetrics(root?: string): Record<string, any>;
18
+ export declare function resolveScanTarget(root?: string): [string, string];
19
+ /** Absolute path to the tina4 CLI binary, or null when it is not installed. */
20
+ export declare function enginePath(): string | null;
21
+ /** Full code analysis from the native engine, shaped for the dashboard. */
39
22
  export declare function fullAnalysis(root?: string): Record<string, any>;
40
- /** Severity ranking for sorting (higher = more severe). */
41
- export declare const SEVERITY_RANK: Record<string, number>;
42
- export interface Offender {
43
- file: string;
44
- line: number;
45
- kind: string;
46
- severity: "error" | "warn" | "info";
47
- score: number;
48
- detail: string;
49
- }
50
23
  export interface OffendersResult {
51
- offenders: Offender[];
24
+ offenders: Record<string, any>[];
52
25
  summary: Record<string, any>;
53
26
  }
54
27
  /**
55
- * Rank the worst code-quality issues into a single "top offenders" list.
56
- *
57
- * Reuses {@link fullAnalysis} (does NOT re-analyze — the result is mtime-cached).
58
- * Each offender is `{ file, line, kind, severity, score, detail }`.
59
- *
60
- * Rules (one offender per matching condition — SAME scoring as the master):
61
- * - function complexity > 10 → kind "complexity"
62
- * severity "error" if > 20 else "warn"; score = complexity
63
- * - file loc > 500 → kind "large_file" (warn); score = loc / 100
64
- * - file functions > 20 → kind "too_many_functions" (warn); score = functions / 4
65
- * - file maintainability < 40 → kind "low_maintainability"
66
- * severity "error" if < 20 else "warn"; score = 50 - mi
67
- * - file has_tests === false → kind "untested" (info); score = loc / 100
68
- *
69
- * Sorted by (severity rank, score) DESCENDING and truncated to `top`.
28
+ * Top code-health offenders from the native engine.
70
29
  *
71
- * Returns `{ offenders, summary }` where summary carries the headline numbers
72
- * the CLI prints (files_analyzed, total_functions, avg_complexity,
73
- * avg_maintainability, scan_mode, scan_root, total_offenders).
30
+ * The engine ranks and severity-tags them, and its own --fail-on gate reads the
31
+ * same list, so the CLI and the dashboard can never disagree about what counts
32
+ * as an offender.
74
33
  */
75
34
  export declare function offenders(root?: string, top?: number): OffendersResult;
35
+ /**
36
+ * Per-file metrics from the native engine.
37
+ *
38
+ * The engine accepts a single file for --path, so one code path serves both the
39
+ * whole-tree scan and one file.
40
+ */
76
41
  export declare function fileDetail(filePath: string): Record<string, any>;
77
- export {};