tina4-nodejs 3.13.136 → 3.13.138

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 (243) hide show
  1. package/CLAUDE.md +33 -3
  2. package/COMMERCIAL-LICENSE.md +8 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE +11 -0
  5. package/README.md +24 -2
  6. package/package.json +44 -6
  7. package/packages/cli/dist/bin.js +19440 -18017
  8. package/packages/cli/src/bin.ts +8 -0
  9. package/packages/cli/src/commands/build.ts +8 -0
  10. package/packages/cli/src/commands/generate.ts +8 -0
  11. package/packages/cli/src/commands/init.ts +8 -0
  12. package/packages/cli/src/commands/lint.ts +8 -0
  13. package/packages/cli/src/commands/migrate.ts +8 -0
  14. package/packages/cli/src/commands/migrateCreate.ts +8 -0
  15. package/packages/cli/src/commands/migrateRollback.ts +8 -0
  16. package/packages/cli/src/commands/migrateStatus.ts +8 -0
  17. package/packages/cli/src/commands/queue.ts +8 -0
  18. package/packages/cli/src/commands/routes.ts +8 -0
  19. package/packages/cli/src/commands/seed.ts +8 -0
  20. package/packages/cli/src/commands/serve.ts +11 -0
  21. package/packages/cli/src/commands/test.ts +8 -0
  22. package/packages/core/dist/index.js +19432 -18012
  23. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
  24. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
  25. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
  26. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
  27. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
  28. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
  29. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
  30. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
  31. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
  32. package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
  33. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
  34. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
  35. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
  36. package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
  37. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
  38. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
  39. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
  40. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
  41. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
  42. package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
  43. package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
  44. package/packages/core/public/bundled-component-provenance.json +48 -0
  45. package/packages/core/public/js/tina4js.min.js +13 -3
  46. package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
  47. package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
  48. package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
  49. package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
  50. package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
  51. package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
  52. package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
  53. package/packages/core/src/__feedback/widget.js +8 -0
  54. package/packages/core/src/_missing.ts +8 -0
  55. package/packages/core/src/ai.ts +8 -0
  56. package/packages/core/src/aiClient.ts +22 -7
  57. package/packages/core/src/api.ts +15 -3
  58. package/packages/core/src/auth.ts +21 -8
  59. package/packages/core/src/authGate.ts +8 -0
  60. package/packages/core/src/background.ts +8 -0
  61. package/packages/core/src/cache.ts +43 -4
  62. package/packages/core/src/constants.ts +8 -0
  63. package/packages/core/src/container.ts +8 -0
  64. package/packages/core/src/context/chunker.ts +8 -0
  65. package/packages/core/src/context/index.ts +8 -0
  66. package/packages/core/src/devAdmin.ts +185 -59
  67. package/packages/core/src/devMailbox.ts +8 -0
  68. package/packages/core/src/dispatchPipeline.ts +8 -0
  69. package/packages/core/src/docs.ts +8 -0
  70. package/packages/core/src/docsAutoDiscovery.ts +8 -0
  71. package/packages/core/src/docsParser.ts +8 -0
  72. package/packages/core/src/docsScanner.ts +44 -0
  73. package/packages/core/src/docsSignatures.ts +8 -0
  74. package/packages/core/src/dotenv.ts +8 -0
  75. package/packages/core/src/env.ts +8 -0
  76. package/packages/core/src/errorOverlay.ts +8 -0
  77. package/packages/core/src/events.ts +8 -0
  78. package/packages/core/src/fakeData.ts +8 -0
  79. package/packages/core/src/feedback.ts +8 -0
  80. package/packages/core/src/graphql.ts +16 -5
  81. package/packages/core/src/health.ts +8 -0
  82. package/packages/core/src/htmlElement.ts +8 -0
  83. package/packages/core/src/i18n.ts +8 -0
  84. package/packages/core/src/index.ts +8 -0
  85. package/packages/core/src/job.ts +8 -0
  86. package/packages/core/src/logger.ts +8 -0
  87. package/packages/core/src/mcp.test.ts +8 -0
  88. package/packages/core/src/mcp.ts +8 -0
  89. package/packages/core/src/messenger.ts +143 -86
  90. package/packages/core/src/metrics.ts +13 -0
  91. package/packages/core/src/middleware.ts +1178 -1096
  92. package/packages/core/src/mqtt.ts +8 -0
  93. package/packages/core/src/mqttMessage.ts +8 -0
  94. package/packages/core/src/optionalPackage.ts +48 -0
  95. package/packages/core/src/plan.ts +8 -0
  96. package/packages/core/src/portTakeover.ts +8 -0
  97. package/packages/core/src/projectIndex.ts +8 -0
  98. package/packages/core/src/projectIndexExtractors.ts +8 -0
  99. package/packages/core/src/projectIndexStorage.ts +8 -0
  100. package/packages/core/src/push.ts +8 -0
  101. package/packages/core/src/queue.ts +8 -0
  102. package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
  103. package/packages/core/src/queueBackends/liteBackend.ts +8 -0
  104. package/packages/core/src/queueBackends/mongoBackend.ts +21 -8
  105. package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
  106. package/packages/core/src/rateLimiter.ts +8 -0
  107. package/packages/core/src/request.ts +59 -10
  108. package/packages/core/src/response.ts +79 -5
  109. package/packages/core/src/routeDiscovery.ts +8 -0
  110. package/packages/core/src/router.ts +8 -0
  111. package/packages/core/src/server.ts +73 -24
  112. package/packages/core/src/service.ts +8 -0
  113. package/packages/core/src/session.ts +8 -0
  114. package/packages/core/src/sessionHandlers/childError.ts +8 -0
  115. package/packages/core/src/sessionHandlers/databaseHandler.ts +8 -0
  116. package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
  117. package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
  118. package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
  119. package/packages/core/src/sessionHandlers/respClient.ts +8 -0
  120. package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
  121. package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
  122. package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
  123. package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
  124. package/packages/core/src/sso.ts +8 -0
  125. package/packages/core/src/static.ts +8 -0
  126. package/packages/core/src/test.ts +8 -0
  127. package/packages/core/src/testClient.ts +8 -0
  128. package/packages/core/src/testing.ts +8 -0
  129. package/packages/core/src/transport.ts +369 -0
  130. package/packages/core/src/trustedProxy.ts +8 -0
  131. package/packages/core/src/types.ts +13 -0
  132. package/packages/core/src/validator.ts +8 -0
  133. package/packages/core/src/version.ts +8 -0
  134. package/packages/core/src/websocket.ts +8 -0
  135. package/packages/core/src/websocketBackplane.ts +49 -32
  136. package/packages/core/src/websocketConnection.ts +8 -0
  137. package/packages/core/src/wsdl.ts +197 -115
  138. package/packages/core/templates/errors/302.twig +8 -0
  139. package/packages/core/templates/errors/401.twig +8 -0
  140. package/packages/core/templates/errors/403.twig +8 -0
  141. package/packages/core/templates/errors/404.twig +8 -0
  142. package/packages/core/templates/errors/500.twig +8 -0
  143. package/packages/core/templates/errors/502.twig +8 -0
  144. package/packages/core/templates/errors/503.twig +8 -0
  145. package/packages/core/templates/errors/base.twig +8 -0
  146. package/packages/frond/dist/index.js +151 -17
  147. package/packages/frond/src/engine.ts +177 -17
  148. package/packages/frond/src/index.ts +8 -0
  149. package/packages/orm/dist/index.js +8135 -6713
  150. package/packages/orm/src/adapters/firebird.ts +50 -3
  151. package/packages/orm/src/adapters/mongodb.ts +8 -0
  152. package/packages/orm/src/adapters/mssql.ts +29 -7
  153. package/packages/orm/src/adapters/mysql.ts +17 -1
  154. package/packages/orm/src/adapters/odbc.ts +11 -0
  155. package/packages/orm/src/adapters/pg-types.d.ts +8 -0
  156. package/packages/orm/src/adapters/postgres.ts +34 -8
  157. package/packages/orm/src/adapters/sqlDialect.ts +57 -0
  158. package/packages/orm/src/adapters/sqlite.ts +26 -0
  159. package/packages/orm/src/autoCrud.ts +78 -36
  160. package/packages/orm/src/baseModel.ts +61 -29
  161. package/packages/orm/src/cachedDatabase.ts +67 -1
  162. package/packages/orm/src/connectTimeout.ts +8 -0
  163. package/packages/orm/src/database.ts +649 -401
  164. package/packages/orm/src/databaseResult.ts +8 -0
  165. package/packages/orm/src/databaseUrl.ts +8 -0
  166. package/packages/orm/src/docstore.ts +23 -1
  167. package/packages/orm/src/fakeData.ts +8 -0
  168. package/packages/orm/src/graph/adapters/arango.ts +8 -0
  169. package/packages/orm/src/graph/adapters/bolt.ts +8 -0
  170. package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
  171. package/packages/orm/src/graph/connectTimeout.ts +8 -0
  172. package/packages/orm/src/graph/errors.ts +8 -0
  173. package/packages/orm/src/graph/graphAdapter.ts +8 -0
  174. package/packages/orm/src/graph/graphDatabase.ts +8 -0
  175. package/packages/orm/src/graph/graphUrl.ts +10 -1
  176. package/packages/orm/src/graph/shapes.ts +8 -0
  177. package/packages/orm/src/index.ts +147 -139
  178. package/packages/orm/src/migration.ts +21 -0
  179. package/packages/orm/src/model.ts +8 -0
  180. package/packages/orm/src/modelCollection.ts +8 -0
  181. package/packages/orm/src/point.ts +8 -0
  182. package/packages/orm/src/query.ts +149 -33
  183. package/packages/orm/src/queryBuilder.ts +8 -0
  184. package/packages/orm/src/realtime/index.ts +8 -0
  185. package/packages/orm/src/realtime/models/attachment.ts +8 -0
  186. package/packages/orm/src/realtime/models/channel.ts +8 -0
  187. package/packages/orm/src/realtime/models/channelMember.ts +8 -0
  188. package/packages/orm/src/realtime/models/message.ts +8 -0
  189. package/packages/orm/src/realtime/models/workspace.ts +8 -0
  190. package/packages/orm/src/realtime/realtime.ts +8 -0
  191. package/packages/orm/src/realtime/storage.ts +43 -12
  192. package/packages/orm/src/seeder.ts +8 -0
  193. package/packages/orm/src/seederTable.ts +8 -0
  194. package/packages/orm/src/seederTypes.ts +8 -0
  195. package/packages/orm/src/sqlTranslator.ts +185 -8
  196. package/packages/orm/src/types.ts +15 -0
  197. package/packages/orm/src/validation.ts +8 -0
  198. package/packages/swagger/src/generator.ts +8 -0
  199. package/packages/swagger/src/index.ts +8 -0
  200. package/packages/swagger/src/ui.ts +8 -0
  201. package/types/core/src/aiClient.d.ts +2 -2
  202. package/types/core/src/background.d.ts +0 -16
  203. package/types/core/src/cache.d.ts +6 -0
  204. package/types/core/src/devAdmin.d.ts +18 -12
  205. package/types/core/src/docs.d.ts +0 -17
  206. package/types/core/src/errorOverlay.d.ts +0 -24
  207. package/types/core/src/events.d.ts +0 -12
  208. package/types/core/src/feedback.d.ts +0 -23
  209. package/types/core/src/graphql.d.ts +0 -21
  210. package/types/core/src/htmlElement.d.ts +0 -15
  211. package/types/core/src/messenger.d.ts +12 -1
  212. package/types/core/src/metrics.d.ts +2 -0
  213. package/types/core/src/middleware.d.ts +45 -1
  214. package/types/core/src/mqtt.d.ts +0 -33
  215. package/types/core/src/optionalPackage.d.ts +25 -0
  216. package/types/core/src/plan.d.ts +0 -10
  217. package/types/core/src/projectIndex.d.ts +0 -10
  218. package/types/core/src/queueBackends/mongoBackend.d.ts +2 -0
  219. package/types/core/src/request.d.ts +12 -0
  220. package/types/core/src/response.d.ts +13 -0
  221. package/types/core/src/server.d.ts +19 -0
  222. package/types/core/src/sessionHandlers/childError.d.ts +0 -15
  223. package/types/core/src/transport.d.ts +28 -0
  224. package/types/core/src/trustedProxy.d.ts +0 -16
  225. package/types/core/src/types.d.ts +5 -0
  226. package/types/core/src/websocketBackplane.d.ts +1 -0
  227. package/types/core/src/wsdl.d.ts +8 -22
  228. package/types/frond/src/engine.d.ts +7 -1
  229. package/types/orm/src/adapters/firebird.d.ts +12 -0
  230. package/types/orm/src/adapters/mssql.d.ts +11 -0
  231. package/types/orm/src/adapters/mysql.d.ts +2 -0
  232. package/types/orm/src/adapters/postgres.d.ts +11 -0
  233. package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
  234. package/types/orm/src/adapters/sqlite.d.ts +7 -0
  235. package/types/orm/src/baseModel.d.ts +5 -0
  236. package/types/orm/src/cachedDatabase.d.ts +27 -0
  237. package/types/orm/src/database.d.ts +46 -7
  238. package/types/orm/src/graph/graphUrl.d.ts +0 -8
  239. package/types/orm/src/index.d.ts +1 -1
  240. package/types/orm/src/query.d.ts +48 -2
  241. package/types/orm/src/realtime/storage.d.ts +2 -1
  242. package/types/orm/src/sqlTranslator.d.ts +70 -18
  243. package/types/orm/src/types.d.ts +6 -0
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Programmatic HTML builder — avoids string concatenation.
3
11
  *
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  // Tina4 i18n — Internationalization and localization, zero dependencies.
2
10
  // Load JSON translation files, support nested keys, parameter substitution, locale fallback.
3
11
 
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  export type {
2
10
  Tina4Request,
3
11
  Tina4Response,
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 Queue Job — a single queue job with lifecycle methods.
3
11
  */
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  import {
2
10
  appendFileSync,
3
11
  closeSync,
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Unit tests for the MCP (Model Context Protocol) implementation.
3
11
  * Run with: npx tsx packages/core/src/mcp.test.ts
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  // Tina4 MCP Server — Model Context Protocol for AI tool integration.
2
10
  //
3
11
  // Built-in MCP server for dev tools + developer API for custom MCP servers.
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  /**
2
10
  * Tina4 Messenger — SMTP email client using Node.js built-in modules only.
3
11
  *
@@ -32,15 +40,80 @@ import { isTruthy } from "./dotenv.js";
32
40
  import { DevMailbox } from "./devMailbox.js";
33
41
  import { Log } from "./logger.js";
34
42
 
43
+ // ── Transport security (ADR-0071) ───────────────────────────
44
+ //
45
+ // `encryption` means what it says. Port 465 is always implicit TLS; `ssl` is
46
+ // implicit TLS on ANY port; `tls` / `starttls` upgrade with STARTTLS and FAIL
47
+ // when the server does not offer it; `none` never upgrades. An unknown value
48
+ // is refused when the Messenger is built, so a typo never means cleartext.
49
+ //
50
+ // Every TLS connection verifies the certificate and the host name. There is
51
+ // no Tina4 switch to turn that off (TINA4_MAIL_TLS_INSECURE is withdrawn);
52
+ // `rejectUnauthorized: true` is passed explicitly so NODE_TLS_REJECT_UNAUTHORIZED=0
53
+ // cannot turn it off either. Trust a private CA the standard Node way:
54
+ // NODE_EXTRA_CA_CERTS=/path/ca.pem.
55
+
56
+ const MAIL_ENCRYPTION_VALUES = ["ssl", "tls", "starttls", "none"];
57
+
58
+ /** Trim + lowercase an encryption setting, refusing anything that is not a known value. */
59
+ function normaliseEncryption(value: string, kind: "mail" | "IMAP"): string {
60
+ const normalised = value.trim().toLowerCase();
61
+ if (!MAIL_ENCRYPTION_VALUES.includes(normalised)) {
62
+ throw new Error(`Unknown ${kind} encryption '${value}'. Valid values: ${MAIL_ENCRYPTION_VALUES.join(", ")}.`);
63
+ }
64
+ return normalised;
65
+ }
66
+
67
+ type SmtpTransport = "implicit_tls" | "starttls" | "plain";
68
+
69
+ /** Matches Python's smtplib timeouts: 30 s for a send, 10 s for a connection check. */
70
+ const SMTP_SEND_TIMEOUT_MS = 30_000;
71
+ const SMTP_CHECK_TIMEOUT_MS = 10_000;
72
+
35
73
  /**
36
- * TLS certificate validation defaults to SECURE (rejectUnauthorized: true).
37
- * Set TINA4_MAIL_TLS_INSECURE=true to disable validation for dev / self-signed
38
- * certificates ONLY — never in production. Previously this was hard-coded to
39
- * `rejectUnauthorized: false`, silently disabling certificate validation for
40
- * every TLS connection (a man-in-the-middle risk).
74
+ * Give a socket an idle timeout that fails it with a clear error. Without one a
75
+ * client that waits for a greeting the server never sends (a plaintext client
76
+ * on a TLS-only port) hangs forever.
41
77
  */
42
- function tlsRejectUnauthorized(): boolean {
43
- return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
78
+ function failOnIdle<T extends net.Socket>(socket: T, timeoutMs: number, what: string): T {
79
+ socket.setTimeout(timeoutMs, () => {
80
+ socket.destroy(new Error(`${what} timed out after ${timeoutMs / 1000}s`));
81
+ });
82
+ return socket;
83
+ }
84
+
85
+ /** Open a verified TLS connection (implicit TLS), or upgrade `socket` (STARTTLS). */
86
+ function connectVerifiedTls(host: string, port: number, socket?: net.Socket): Promise<tls.TLSSocket> {
87
+ const secureSocket = tls.connect({
88
+ host,
89
+ port,
90
+ socket,
91
+ servername: net.isIP(host) ? undefined : host,
92
+ rejectUnauthorized: true,
93
+ });
94
+ return new Promise((resolve, reject) => {
95
+ secureSocket.once("secureConnect", () => {
96
+ secureSocket.removeListener("error", reject);
97
+ resolve(secureSocket);
98
+ });
99
+ secureSocket.once("error", reject);
100
+ });
101
+ }
102
+
103
+ function connectPlain(host: string, port: number): Promise<net.Socket> {
104
+ const socket = net.createConnection({ host, port });
105
+ return new Promise((resolve, reject) => {
106
+ socket.once("connect", () => {
107
+ socket.removeListener("error", reject);
108
+ resolve(socket);
109
+ });
110
+ socket.once("error", reject);
111
+ });
112
+ }
113
+
114
+ /** Does this EHLO reply advertise the STARTTLS extension? */
115
+ function offersStarttls(ehloReply: string): boolean {
116
+ return /^\d{3}[ -]STARTTLS\b/im.test(ehloReply);
44
117
  }
45
118
 
46
119
  /**
@@ -384,7 +457,6 @@ export class Messenger {
384
457
  private fromAddress: string;
385
458
  private fromName: string;
386
459
  private encryption: string;
387
- private useTls: boolean;
388
460
  /** Whether an SMTP host was actually configured (see the constructor). */
389
461
  private smtpConfigured: boolean = false;
390
462
  /** The local mailbox, present only when this messenger captures. */
@@ -421,17 +493,18 @@ export class Messenger {
421
493
  ?? process.env.TINA4_MAIL_FROM_NAME
422
494
  ?? "";
423
495
 
424
- // Encryption: constructor > .env > backward-compat useTls > default "tls"
425
- const envEncryption = options?.encryption
496
+ // Encryption: constructor > .env > backward-compat useTls > default "tls".
497
+ // A configured value is validated (ADR-0071): an unknown one, an empty one
498
+ // included, raises here rather than sending in cleartext later.
499
+ const configuredEncryption = options?.encryption
426
500
  ?? process.env.TINA4_MAIL_ENCRYPTION;
427
- if (envEncryption) {
428
- this.encryption = envEncryption.toLowerCase();
501
+ if (configuredEncryption !== undefined) {
502
+ this.encryption = normaliseEncryption(configuredEncryption, "mail");
429
503
  } else if (options?.useTls !== undefined) {
430
504
  this.encryption = options.useTls ? "tls" : "none";
431
505
  } else {
432
506
  this.encryption = "tls";
433
507
  }
434
- this.useTls = ["tls", "starttls"].includes(this.encryption);
435
508
 
436
509
  this.imapHost = options?.imapHost
437
510
  ?? process.env.TINA4_MAIL_IMAP_HOST
@@ -448,10 +521,23 @@ export class Messenger {
448
521
  // IMAP encryption — separate from SMTP encryption because IMAP almost
449
522
  // always uses port 993 + implicit TLS while SMTP toggles between 587
450
523
  // (STARTTLS) and 465 (implicit TLS). Default is "tls" to match
451
- // industry-standard IMAPS port 993 behaviour.
452
- this.imapEncryption = (options?.imapEncryption
524
+ // industry-standard IMAPS port 993 behaviour. "tls" and "ssl" are implicit
525
+ // TLS, "starttls" is a REQUIRED STARTTLS upgrade, "none" is plaintext; any
526
+ // other value raises here.
527
+ this.imapEncryption = normaliseEncryption(options?.imapEncryption
453
528
  ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION
454
- ?? "tls").toLowerCase();
529
+ ?? "tls", "IMAP");
530
+ }
531
+
532
+ /**
533
+ * How the SMTP connection is secured (ADR-0071 section 1). Port 465 is the
534
+ * implicit-TLS submission port whatever the setting; `ssl` is implicit TLS on
535
+ * any port; `tls` / `starttls` upgrade with a required STARTTLS; `none` is
536
+ * plaintext on purpose.
537
+ */
538
+ private smtpTransport(): SmtpTransport {
539
+ if (this.port === 465 || this.encryption === "ssl") return "implicit_tls";
540
+ return this.encryption === "none" ? "plain" : "starttls";
455
541
  }
456
542
 
457
543
  /**
@@ -507,21 +593,12 @@ export class Messenger {
507
593
  return { toList, ccList, bccList, allRecipients };
508
594
  }
509
595
 
510
- private async connectSmtpSocket(): Promise<SmtpSocket> {
511
- if (this.port === 465) {
512
- const socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
513
- await new Promise<void>((resolve, reject) => {
514
- socket.once("secureConnect", resolve);
515
- socket.once("error", reject);
516
- });
517
- return socket;
596
+ private async connectSmtpSocket(timeoutMs: number): Promise<SmtpSocket> {
597
+ const what = `SMTP connection to ${this.host}:${this.port}`;
598
+ if (this.smtpTransport() === "implicit_tls") {
599
+ return failOnIdle(await connectVerifiedTls(this.host, this.port), timeoutMs, what);
518
600
  }
519
- const socket = net.createConnection({ host: this.host, port: this.port });
520
- await new Promise<void>((resolve, reject) => {
521
- socket.once("connect", resolve);
522
- socket.once("error", reject);
523
- });
524
- return socket;
601
+ return failOnIdle(await connectPlain(this.host, this.port), timeoutMs, what);
525
602
  }
526
603
 
527
604
  private async requireSmtpResponse(socket: SmtpSocket, command: string, expected: number, failure: string): Promise<{ code: number; text: string }> {
@@ -530,20 +607,23 @@ export class Messenger {
530
607
  return response;
531
608
  }
532
609
 
533
- private async openSmtpSession(): Promise<SmtpSocket> {
534
- let socket = await this.connectSmtpSocket();
610
+ /**
611
+ * Connect, read the greeting, EHLO, and - for the STARTTLS transport - upgrade.
612
+ * STARTTLS is REQUIRED: when the server does not offer it the session fails
613
+ * here, before AUTH or MAIL FROM is ever sent.
614
+ */
615
+ private async openSmtpSession(timeoutMs: number = SMTP_SEND_TIMEOUT_MS): Promise<SmtpSocket> {
616
+ let socket = await this.connectSmtpSocket(timeoutMs);
535
617
  try {
536
618
  await this.requireSmtpResponse(socket, "", 220, "SMTP greeting failed");
537
619
  const ehlo = await this.requireSmtpResponse(socket, `EHLO ${this.host}`, 250, "EHLO failed");
538
- if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
620
+ if (this.smtpTransport() === "starttls") {
621
+ if (!offersStarttls(ehlo.text)) {
622
+ throw new SmtpCommandError(`STARTTLS was requested but ${this.host}:${this.port} does not offer it`);
623
+ }
539
624
  await this.requireSmtpResponse(socket, "STARTTLS", 220, "STARTTLS failed");
540
- const plainSocket = socket as net.Socket;
541
- const secureSocket = tls.connect({ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() });
542
- await new Promise<void>((resolve, reject) => {
543
- secureSocket.once("secureConnect", resolve);
544
- secureSocket.once("error", reject);
545
- });
546
- socket = secureSocket;
625
+ socket = failOnIdle(await connectVerifiedTls(this.host, this.port, socket as net.Socket), timeoutMs,
626
+ `SMTP connection to ${this.host}:${this.port}`);
547
627
  await this.requireSmtpResponse(socket, `EHLO ${this.host}`, 250, "EHLO after STARTTLS failed");
548
628
  }
549
629
  return socket;
@@ -679,42 +759,19 @@ export class Messenger {
679
759
  * Test the SMTP connection without sending an email.
680
760
  */
681
761
  async testConnection(): Promise<{ success: boolean; message: string }> {
762
+ // Same transport rules as send(): implicit TLS / required STARTTLS / plain,
763
+ // certificates verified.
764
+ let socket: SmtpSocket | null = null;
682
765
  try {
683
- let socket: net.Socket | tls.TLSSocket;
684
-
685
- if (this.port === 465) {
686
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
687
- await new Promise<void>((resolve, reject) => {
688
- socket.once("secureConnect", resolve);
689
- socket.once("error", reject);
690
- });
691
- } else {
692
- socket = net.createConnection({ host: this.host, port: this.port });
693
- await new Promise<void>((resolve, reject) => {
694
- socket.once("connect", resolve);
695
- socket.once("error", reject);
696
- });
697
- }
698
-
699
- const greeting = await readResponse(socket);
700
- if (greeting.code !== 220) {
701
- socket.destroy();
702
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
703
- }
704
-
705
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
706
- if (ehlo.code !== 250) {
707
- socket.destroy();
708
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
709
- }
710
-
766
+ socket = await this.openSmtpSession(SMTP_CHECK_TIMEOUT_MS);
711
767
  await sendCommand(socket, "QUIT");
712
- socket.destroy();
713
-
714
768
  return { success: true, message: `Connected to ${this.host}:${this.port}` };
715
769
  } catch (err) {
770
+ if (err instanceof SmtpCommandError) return { success: false, message: err.message };
716
771
  const errMsg = err instanceof Error ? err.message : String(err);
717
772
  return { success: false, message: `Connection failed: ${errMsg}` };
773
+ } finally {
774
+ socket?.destroy();
718
775
  }
719
776
  }
720
777
 
@@ -731,28 +788,28 @@ export class Messenger {
731
788
 
732
789
  let socket: net.Socket | tls.TLSSocket;
733
790
 
734
- // Honour TINA4_MAIL_IMAP_ENCRYPTION when set; otherwise infer from port.
735
- // "tls" / "ssl" → implicit TLS connect; anything else → plain connect.
736
- const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl"
737
- || (this.imapEncryption === "" && this.imapPort === 993);
738
-
739
- if (useTls) {
740
- socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
741
- await new Promise<void>((resolve, reject) => {
742
- socket.once("secureConnect", resolve);
743
- socket.once("error", reject);
744
- });
791
+ // "tls" / "ssl" → implicit TLS; "starttls" → plain connect, then a REQUIRED
792
+ // STARTTLS upgrade before LOGIN; "none" → plaintext. (Validated in the constructor.)
793
+ if (this.imapEncryption === "tls" || this.imapEncryption === "ssl") {
794
+ socket = await connectVerifiedTls(this.imapHost, this.imapPort);
745
795
  } else {
746
- socket = net.createConnection({ host: this.imapHost, port: this.imapPort });
747
- await new Promise<void>((resolve, reject) => {
748
- socket.once("connect", resolve);
749
- socket.once("error", reject);
750
- });
796
+ socket = await connectPlain(this.imapHost, this.imapPort);
751
797
  }
752
798
 
753
799
  // Read server greeting
754
800
  await imapReadLine(socket);
755
801
 
802
+ if (this.imapEncryption === "starttls") {
803
+ try {
804
+ await imapCommand(socket, "STARTTLS");
805
+ } catch {
806
+ socket.destroy();
807
+ throw new MessengerConnectionError(
808
+ `STARTTLS was requested but ${this.imapHost}:${this.imapPort} does not offer it`);
809
+ }
810
+ socket = await connectVerifiedTls(this.imapHost, this.imapPort, socket);
811
+ }
812
+
756
813
  // Login
757
814
  if (this.imapUser && this.imapPass) {
758
815
  const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
@@ -1,3 +1,11 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
1
9
  // Thin dev-admin adapter for the native `tina4 metrics` engine (ADR-0054).
2
10
 
3
11
  import * as fs from "node:fs";
@@ -119,6 +127,11 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
119
127
  };
120
128
  }
121
129
 
130
+ /** The root the last full scan covered ("" before any scan). */
131
+ export function metricsScanRoot(): string {
132
+ return lastScanRoot;
133
+ }
134
+
122
135
  export function fileDetail(filePath: string): Record<string, any> {
123
136
  if (!filePath) throw new MetricsEngineError("fileDetail needs a path");
124
137
  let target = filePath;