tina4-nodejs 3.13.137 → 3.13.139

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 (255) 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 +19979 -18102
  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 +19976 -18097
  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 +8 -0
  57. package/packages/core/src/api.ts +48 -7
  58. package/packages/core/src/auth.ts +94 -14
  59. package/packages/core/src/authGate.ts +35 -20
  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 +108 -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 +9 -0
  85. package/packages/core/src/job.ts +16 -1
  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 +11 -1
  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 +1181 -1097
  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 +24 -0
  101. package/packages/core/src/queue.ts +23 -2
  102. package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
  103. package/packages/core/src/queueBackends/liteBackend.ts +23 -0
  104. package/packages/core/src/queueBackends/mongoBackend.ts +61 -14
  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 +79 -25
  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 +68 -3
  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 +20 -4
  125. package/packages/core/src/ssoIdentity.ts +19 -0
  126. package/packages/core/src/ssrf.ts +221 -0
  127. package/packages/core/src/static.ts +8 -0
  128. package/packages/core/src/test.ts +8 -0
  129. package/packages/core/src/testClient.ts +11 -0
  130. package/packages/core/src/testing.ts +8 -0
  131. package/packages/core/src/transport.ts +369 -0
  132. package/packages/core/src/trustedProxy.ts +8 -0
  133. package/packages/core/src/types.ts +13 -0
  134. package/packages/core/src/validator.ts +8 -0
  135. package/packages/core/src/version.ts +8 -0
  136. package/packages/core/src/websocket.ts +12 -2
  137. package/packages/core/src/websocketBackplane.ts +49 -32
  138. package/packages/core/src/websocketConnection.ts +8 -0
  139. package/packages/core/src/wsdl.ts +197 -115
  140. package/packages/core/templates/errors/302.twig +8 -0
  141. package/packages/core/templates/errors/401.twig +8 -0
  142. package/packages/core/templates/errors/403.twig +8 -0
  143. package/packages/core/templates/errors/404.twig +8 -0
  144. package/packages/core/templates/errors/500.twig +8 -0
  145. package/packages/core/templates/errors/502.twig +8 -0
  146. package/packages/core/templates/errors/503.twig +8 -0
  147. package/packages/core/templates/errors/base.twig +8 -0
  148. package/packages/frond/dist/index.js +157 -17
  149. package/packages/frond/src/engine.ts +186 -17
  150. package/packages/frond/src/index.ts +8 -0
  151. package/packages/orm/dist/index.js +8641 -6765
  152. package/packages/orm/src/adapters/firebird.ts +50 -3
  153. package/packages/orm/src/adapters/mongodb.ts +8 -0
  154. package/packages/orm/src/adapters/mssql.ts +29 -7
  155. package/packages/orm/src/adapters/mysql.ts +17 -1
  156. package/packages/orm/src/adapters/odbc.ts +11 -0
  157. package/packages/orm/src/adapters/pg-types.d.ts +8 -0
  158. package/packages/orm/src/adapters/postgres.ts +34 -8
  159. package/packages/orm/src/adapters/sqlDialect.ts +57 -0
  160. package/packages/orm/src/adapters/sqlite.ts +26 -0
  161. package/packages/orm/src/autoCrud.ts +78 -36
  162. package/packages/orm/src/baseModel.ts +61 -29
  163. package/packages/orm/src/cachedDatabase.ts +67 -1
  164. package/packages/orm/src/connectTimeout.ts +8 -0
  165. package/packages/orm/src/database.ts +649 -401
  166. package/packages/orm/src/databaseResult.ts +8 -0
  167. package/packages/orm/src/databaseUrl.ts +8 -0
  168. package/packages/orm/src/docstore.ts +23 -1
  169. package/packages/orm/src/fakeData.ts +8 -0
  170. package/packages/orm/src/graph/adapters/arango.ts +8 -0
  171. package/packages/orm/src/graph/adapters/bolt.ts +8 -0
  172. package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
  173. package/packages/orm/src/graph/connectTimeout.ts +8 -0
  174. package/packages/orm/src/graph/errors.ts +8 -0
  175. package/packages/orm/src/graph/graphAdapter.ts +8 -0
  176. package/packages/orm/src/graph/graphDatabase.ts +8 -0
  177. package/packages/orm/src/graph/graphUrl.ts +10 -1
  178. package/packages/orm/src/graph/shapes.ts +8 -0
  179. package/packages/orm/src/index.ts +147 -139
  180. package/packages/orm/src/migration.ts +8 -0
  181. package/packages/orm/src/model.ts +8 -0
  182. package/packages/orm/src/modelCollection.ts +8 -0
  183. package/packages/orm/src/point.ts +8 -0
  184. package/packages/orm/src/query.ts +149 -33
  185. package/packages/orm/src/queryBuilder.ts +8 -0
  186. package/packages/orm/src/realtime/index.ts +8 -0
  187. package/packages/orm/src/realtime/models/attachment.ts +8 -0
  188. package/packages/orm/src/realtime/models/channel.ts +8 -0
  189. package/packages/orm/src/realtime/models/channelMember.ts +8 -0
  190. package/packages/orm/src/realtime/models/message.ts +8 -0
  191. package/packages/orm/src/realtime/models/workspace.ts +8 -0
  192. package/packages/orm/src/realtime/realtime.ts +8 -0
  193. package/packages/orm/src/realtime/storage.ts +43 -12
  194. package/packages/orm/src/seeder.ts +8 -0
  195. package/packages/orm/src/seederTable.ts +8 -0
  196. package/packages/orm/src/seederTypes.ts +8 -0
  197. package/packages/orm/src/sqlTranslator.ts +185 -8
  198. package/packages/orm/src/types.ts +15 -0
  199. package/packages/orm/src/validation.ts +8 -0
  200. package/packages/swagger/src/generator.ts +8 -0
  201. package/packages/swagger/src/index.ts +8 -0
  202. package/packages/swagger/src/ui.ts +8 -0
  203. package/types/core/src/api.d.ts +6 -0
  204. package/types/core/src/auth.d.ts +30 -0
  205. package/types/core/src/background.d.ts +0 -16
  206. package/types/core/src/cache.d.ts +6 -0
  207. package/types/core/src/devAdmin.d.ts +18 -12
  208. package/types/core/src/docs.d.ts +0 -17
  209. package/types/core/src/errorOverlay.d.ts +0 -24
  210. package/types/core/src/events.d.ts +0 -12
  211. package/types/core/src/feedback.d.ts +0 -23
  212. package/types/core/src/graphql.d.ts +25 -21
  213. package/types/core/src/htmlElement.d.ts +0 -15
  214. package/types/core/src/index.d.ts +1 -0
  215. package/types/core/src/job.d.ts +1 -0
  216. package/types/core/src/messenger.d.ts +12 -1
  217. package/types/core/src/metrics.d.ts +2 -0
  218. package/types/core/src/middleware.d.ts +45 -1
  219. package/types/core/src/mqtt.d.ts +0 -33
  220. package/types/core/src/optionalPackage.d.ts +25 -0
  221. package/types/core/src/plan.d.ts +0 -10
  222. package/types/core/src/projectIndex.d.ts +0 -10
  223. package/types/core/src/push.d.ts +5 -0
  224. package/types/core/src/queue.d.ts +7 -1
  225. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  226. package/types/core/src/queueBackends/mongoBackend.d.ts +10 -1
  227. package/types/core/src/request.d.ts +12 -0
  228. package/types/core/src/response.d.ts +13 -0
  229. package/types/core/src/server.d.ts +19 -0
  230. package/types/core/src/sessionHandlers/childError.d.ts +0 -15
  231. package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
  232. package/types/core/src/sso.d.ts +1 -0
  233. package/types/core/src/ssoIdentity.d.ts +10 -0
  234. package/types/core/src/ssrf.d.ts +19 -0
  235. package/types/core/src/transport.d.ts +28 -0
  236. package/types/core/src/trustedProxy.d.ts +0 -16
  237. package/types/core/src/types.d.ts +5 -0
  238. package/types/core/src/websocketBackplane.d.ts +1 -0
  239. package/types/core/src/wsdl.d.ts +8 -22
  240. package/types/frond/src/engine.d.ts +7 -1
  241. package/types/orm/src/adapters/firebird.d.ts +12 -0
  242. package/types/orm/src/adapters/mssql.d.ts +11 -0
  243. package/types/orm/src/adapters/mysql.d.ts +2 -0
  244. package/types/orm/src/adapters/postgres.d.ts +11 -0
  245. package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
  246. package/types/orm/src/adapters/sqlite.d.ts +7 -0
  247. package/types/orm/src/baseModel.d.ts +5 -0
  248. package/types/orm/src/cachedDatabase.d.ts +27 -0
  249. package/types/orm/src/database.d.ts +46 -7
  250. package/types/orm/src/graph/graphUrl.d.ts +0 -8
  251. package/types/orm/src/index.d.ts +1 -1
  252. package/types/orm/src/query.d.ts +48 -2
  253. package/types/orm/src/realtime/storage.d.ts +2 -1
  254. package/types/orm/src/sqlTranslator.d.ts +70 -18
  255. 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
  * Tina4 GraphQL — Zero-dependency GraphQL engine.
3
11
  *
@@ -62,12 +70,14 @@ const TOKEN_PATTERNS: Array<[string, RegExp]> = [
62
70
  ["EQUALS", /=/y],
63
71
  ["AT", /@/y],
64
72
  ["DOLLAR", /\$/y],
65
- ["COMMA", /,/y],
66
73
  ["STRING", /"(?:[^"\\]|\\.)*"/y],
67
74
  ["NUMBER", /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y],
68
75
  ["BOOL", /\b(?:true|false)\b/y],
69
76
  ["NULL", /\bnull\b/y],
70
77
  ["NAME", /[_a-zA-Z]\w*/y],
78
+ // GraphQL spec 2.1.7: a comma is insignificant, exactly like whitespace. It
79
+ // used to be a COMMA token that only the argument and variable loops
80
+ // consumed, so a comma between fields or list values was a parse error.
71
81
  ["SKIP", /[\s,]+/y],
72
82
  ["COMMENT", /#[^\n]*/y],
73
83
  ];
@@ -165,10 +175,17 @@ type ParsedDefinition = ParsedOperation | ParsedFragment;
165
175
  class Parser {
166
176
  private tokens: Token[];
167
177
  private pos: number;
178
+ // F2: bound the parser's own recursion. A deeply nested query
179
+ // ("{a{a{a...}}}") otherwise recurses in parseSelectionSet until the call
180
+ // stack overflows — BEFORE the execution-time depth guard can run. Reusing
181
+ // TINA4_GRAPHQL_MAX_DEPTH keeps parse and execution on one bound.
182
+ private depth = 0;
183
+ private readonly maxDepth: number;
168
184
 
169
185
  constructor(tokens: Token[]) {
170
186
  this.tokens = tokens;
171
187
  this.pos = 0;
188
+ this.maxDepth = graphqlMaxDepth();
172
189
  }
173
190
 
174
191
  peek(): Token | null {
@@ -262,6 +279,18 @@ class Parser {
262
279
  }
263
280
 
264
281
  private parseSelectionSet(): ParsedSelection[] {
282
+ this.depth++;
283
+ if (this.maxDepth > 0 && this.depth > this.maxDepth) {
284
+ throw new Error(`Query exceeds maximum depth of ${this.maxDepth}`);
285
+ }
286
+ try {
287
+ return this.parseSelectionSetInner();
288
+ } finally {
289
+ this.depth--;
290
+ }
291
+ }
292
+
293
+ private parseSelectionSetInner(): ParsedSelection[] {
265
294
  this.expect("LBRACE");
266
295
  const selections: ParsedSelection[] = [];
267
296
  while (!this.match("RBRACE")) {
@@ -331,7 +360,6 @@ class Parser {
331
360
  const name = this.expect("NAME").value;
332
361
  this.expect("COLON");
333
362
  args[name] = this.parseValue();
334
- this.match("COMMA");
335
363
  }
336
364
  return args;
337
365
  }
@@ -416,7 +444,6 @@ class Parser {
416
444
  defaultVal = this.parseValue();
417
445
  }
418
446
  defs.push({ name, type: typeName, default: defaultVal });
419
- this.match("COMMA");
420
447
  }
421
448
  return defs;
422
449
  }
@@ -480,6 +507,24 @@ export function graphqlMaxDepth(): number {
480
507
  return Number.isNaN(n) ? 50 : n;
481
508
  }
482
509
 
510
+ /**
511
+ * F2: maximum number of expanded selection nodes a single query may request.
512
+ * The depth guard bounds NESTING but not WIDTH: a fragment that spreads another
513
+ * fragment many times (a "fragment bomb") stays shallow while expanding to
514
+ * millions of fields, and a query with thousands of aliases of one field is not
515
+ * deep at all. This budget bounds the expanded selection tree (fragments
516
+ * expanded, aliases counted) so both are rejected before any resolver runs.
517
+ * `TINA4_GRAPHQL_MAX_NODES` overrides the default (1000); `<= 0` disables it.
518
+ */
519
+ export function graphqlMaxNodes(): number {
520
+ const raw = (process.env.TINA4_GRAPHQL_MAX_NODES ?? "1000").trim();
521
+ const n = parseInt(raw, 10);
522
+ return Number.isNaN(n) ? 1000 : n;
523
+ }
524
+
525
+ /** Thrown when a query's expanded selection count exceeds the node budget (F2). */
526
+ class ComplexityError extends Error {}
527
+
483
528
  // ── GraphQL Engine ───────────────────────────────────────────
484
529
 
485
530
  export class GraphQL {
@@ -507,6 +552,13 @@ export class GraphQL {
507
552
  */
508
553
  public maxDepth: number = graphqlMaxDepth();
509
554
 
555
+ /**
556
+ * Maximum expanded selection nodes per query (from `TINA4_GRAPHQL_MAX_NODES`,
557
+ * default 1000; `<= 0` disables). Bounds query WIDTH — fragment bombs and
558
+ * alias explosions — which the depth guard does not. See {@link graphqlMaxNodes}.
559
+ */
560
+ public maxNodes: number = graphqlMaxNodes();
561
+
510
562
  /**
511
563
  * Decorator-style resolver registration.
512
564
  *
@@ -684,6 +736,20 @@ export class GraphQL {
684
736
  }
685
737
 
686
738
  const op = operations[0];
739
+
740
+ // F2: reject an over-complex query (fragment bomb / alias explosion) before
741
+ // any resolver runs.
742
+ if (this.maxNodes > 0) {
743
+ try {
744
+ this.assertComplexity(op.selections, fragments);
745
+ } catch (e: unknown) {
746
+ if (e instanceof ComplexityError) {
747
+ return { data: null, errors: [{ message: e.message }] };
748
+ }
749
+ throw e;
750
+ }
751
+ }
752
+
687
753
  const resolvers = op.operation === "query" ? this.queries : this.mutations;
688
754
 
689
755
  // Apply variable defaults
@@ -715,6 +781,40 @@ export class GraphQL {
715
781
  * instead of recursing until the interpreter stack overflows. Top-level
716
782
  * starts at depth 1; `maxDepth <= 0` disables the guard.
717
783
  */
784
+ /**
785
+ * Count the expanded selection nodes, throwing {@link ComplexityError} past
786
+ * `maxNodes` (F2). Fragment spreads are expanded (so a fragment bomb is
787
+ * counted, not the small unexpanded document) and every field — including
788
+ * each alias — counts once. The count is capped by `maxNodes` and the walk by
789
+ * `maxDepth`, so a circular fragment trips the budget instead of looping and
790
+ * the check itself can never cost more than the budget.
791
+ */
792
+ private assertComplexity(
793
+ selections: ParsedSelection[],
794
+ fragments: Map<string, ParsedFragment>,
795
+ ): void {
796
+ const depthCap = this.maxDepth > 0 ? this.maxDepth : 1000;
797
+ let count = 0;
798
+ const walk = (sels: ParsedSelection[], depth: number): void => {
799
+ if (depth > depthCap) return;
800
+ for (const sel of sels) {
801
+ count++;
802
+ if (count > this.maxNodes) {
803
+ throw new ComplexityError(`Query exceeds maximum complexity of ${this.maxNodes} nodes`);
804
+ }
805
+ if (sel.kind === "fragment_spread") {
806
+ const frag = fragments.get(sel.name);
807
+ if (frag) walk(frag.selections, depth + 1);
808
+ } else if (sel.kind === "inline_fragment") {
809
+ walk(sel.selections, depth + 1);
810
+ } else if (sel.kind === "field" && sel.selections) {
811
+ walk(sel.selections, depth + 1);
812
+ }
813
+ }
814
+ };
815
+ walk(selections, 1);
816
+ }
817
+
718
818
  private async resolveSelectionsInto(
719
819
  selections: ParsedSelection[],
720
820
  resolvers: Map<string, QueryConfig>,
@@ -930,7 +1030,9 @@ export class GraphQL {
930
1030
  className,
931
1031
  (root, args) => {
932
1032
  const db = getDb();
933
- const fieldNames = Object.keys(args);
1033
+ // Declared fields only: the executor passes every argument the query
1034
+ // names, declared or not, and each one becomes a column (ADR-0069).
1035
+ const fieldNames = Object.keys(args).filter((k) => Object.hasOwn(mutationArgs, k));
934
1036
  const placeholders = fieldNames.map(() => "?");
935
1037
  const values = fieldNames.map((f) => args[f]);
936
1038
 
@@ -960,7 +1062,8 @@ export class GraphQL {
960
1062
  const values: unknown[] = [];
961
1063
 
962
1064
  for (const [k, v] of Object.entries(args)) {
963
- if (k !== "id") {
1065
+ // Declared fields only (see create above).
1066
+ if (k !== "id" && Object.hasOwn(mutationArgs, k)) {
964
1067
  setClauses.push(`"${k}" = ?`);
965
1068
  values.push(v);
966
1069
  }
@@ -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 type { RouteDefinition, RouteHandler } from "./types.js";
2
10
 
3
11
  /** Server start time, set when health route is created */
@@ -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,
@@ -82,6 +90,7 @@ export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from
82
90
  export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, sweep, createBackend, _resetBackend } from "./cache.js";
83
91
  export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
84
92
  export { Api, ApiStreamError, parseLineStream, parseSseStream } from "./api.js";
93
+ export { guardUrl, isBlockedAddress, allowPrivateRequests, SsrfError, ALLOW_PRIVATE_ENV } from "./ssrf.js";
85
94
  export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions, StreamOptions, SseEvent } from "./api.js";
86
95
  export { Context, defaultContext, existingContext, fts5Supported, _sharedContexts } from "./context/index.js";
87
96
  export type { SearchHit } from "./context/index.js";
@@ -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
  */
@@ -39,6 +47,7 @@ export type QueueJob = JobData & JobLifecycle;
39
47
 
40
48
  export interface JobQueueBridge {
41
49
  _failJob(topic: string, job: QueueJob, reason: string, maxRetries: number): void;
50
+ _rejectJob(topic: string, job: QueueJob, reason: string, maxRetries: number): void;
42
51
  _retryJob(topic: string, job: QueueJob, delaySeconds?: number): void;
43
52
  _completeJob(topic: string, job: QueueJob): void;
44
53
  getMaxRetries(): number;
@@ -66,7 +75,13 @@ export function createJob(data: JobData, queue: JobQueueBridge): QueueJob {
66
75
  queue._failJob(job.topic, job, reason, queue.getMaxRetries());
67
76
  },
68
77
  reject(reason = "") {
69
- job.fail(reason);
78
+ // Reject permanently — dead-letter NOW, no retry (ADR-0023). Distinct
79
+ // from fail(): fail() retries until maxRetries is spent; reject() is for
80
+ // a message the consumer KNOWS is poison and sends it straight to the
81
+ // dead-letter store. Was a literal alias for fail() before 3.13.139.
82
+ job.status = "dead";
83
+ job.error = reason;
84
+ queue._rejectJob(job.topic, job, reason, queue.getMaxRetries());
70
85
  },
71
86
  retry(delaySeconds?: number) {
72
87
  queue._retryJob(job.topic, job, delaySeconds);
@@ -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.
@@ -230,7 +238,9 @@ function envTruthy(val: string | undefined): boolean {
230
238
  * Python master parity: tina4_python.mcp.is_loopback.
231
239
  */
232
240
  export function isLoopback(ip: string | undefined | null): boolean {
233
- if (ip == null || ip === "") return true;
241
+ // An empty or missing peer is UNKNOWN and is not loopback: a runtime path
242
+ // that lost its socket address must fail closed (ADR-0079 s4).
243
+ if (ip == null || ip === "") return false;
234
244
  let addr = ip.trim().toLowerCase();
235
245
  if (addr.startsWith("::ffff:")) addr = addr.slice(7);
236
246
  return addr === "::1" || addr === "localhost" || addr.startsWith("127.");
@@ -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)}`);