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
  import type { ServerResponse } from "node:http";
2
10
  import fs from "node:fs";
3
11
  import nodePath from "node:path";
@@ -24,6 +32,39 @@ async function closeSource(source: AsyncIterable<unknown>): Promise<void> {
24
32
  }
25
33
  }
26
34
 
35
+ /** RFC 9110 `token`: one or more `tchar`. */
36
+ const HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
37
+
38
+ /**
39
+ * The caller's name as a JSON string literal with non-ASCII escaped, byte-
40
+ * identical to Python's json.dumps (ADR-0068), so a name that is itself broken
41
+ * cannot break the error message or the log line carrying it.
42
+ */
43
+ export function quoteHeaderName(name: string): string {
44
+ return JSON.stringify(name).replace(/[\u007f-\uffff]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
45
+ }
46
+
47
+ /** A TypeError carrying node:http's own error code, as setHeader() throws natively. */
48
+ function headerRefusal(message: string, code: string): TypeError {
49
+ return Object.assign(new TypeError(message), { code });
50
+ }
51
+
52
+ /**
53
+ * Refuse a response header whose name is not an HTTP token or whose value
54
+ * carries CR, LF or NUL, with the exact ADR-0068 wording. node:http refuses
55
+ * these too, but names a broken header raw (a CR/LF in the name reaches the
56
+ * log line) - this is the ADR's form, checked first.
57
+ */
58
+ export function assertSafeHeader(name: string, value: unknown): void {
59
+ if (!HEADER_TOKEN.test(name)) {
60
+ throw headerRefusal(`Header name must be a valid HTTP token [${quoteHeaderName(name)}]`, "ERR_INVALID_HTTP_TOKEN");
61
+ }
62
+ const values = Array.isArray(value) ? value : [value];
63
+ if (values.some((item) => /[\r\n\0]/.test(String(item)))) {
64
+ throw headerRefusal(`Invalid character in header content [${quoteHeaderName(name)}]`, "ERR_INVALID_CHAR");
65
+ }
66
+ }
67
+
27
68
  /** Cache Frond instances by template directory to avoid repeated instantiation. */
28
69
  const _frondCache = new Map<string, InstanceType<any>>();
29
70
 
@@ -147,10 +188,24 @@ export function createResponse(res: ServerResponse): Tina4Response {
147
188
  res.end(chunk, encoding);
148
189
  }
149
190
  };
191
+ // Every header this Response sets goes through here, so every one is
192
+ // checked (ADR-0068) - even after the headers are sent, since an unsafe
193
+ // value is a bug in the code that built it either way.
150
194
  const safeSetHeader = (name: string, value: string | number | readonly string[]) => {
195
+ assertSafeHeader(name, value);
151
196
  if (!res.headersSent) res.setHeader(name, value);
152
197
  };
153
198
 
199
+ // True once the route set Content-Type with header(); response(data) then
200
+ // keeps it instead of detecting one (ADR-0072, #144).
201
+ let contentTypeFromHeader = false;
202
+ const setDetectedContentType = (contentType: string) => {
203
+ if (!contentTypeFromHeader) safeSetHeader("Content-Type", contentType);
204
+ };
205
+ const noteContentTypeHeader = (name: string) => {
206
+ if (name.toLowerCase() === "content-type") contentTypeFromHeader = true;
207
+ };
208
+
154
209
  // ── The callable: response(data, status, contentType) ──
155
210
  const response = function (data?: unknown, statusCode?: number, contentType?: string): Tina4Response {
156
211
  if (res.headersSent) return response;
@@ -158,6 +213,10 @@ export function createResponse(res: ServerResponse): Tina4Response {
158
213
  // Normalise ORM models / collections / query results so handlers can
159
214
  // `return response(model)` without serialising by hand.
160
215
  data = toJsonable(data);
216
+ // Any byte array is a binary body: view it as a Buffer (no copy) so every branch below writes it as bytes.
217
+ if (data instanceof Uint8Array && !Buffer.isBuffer(data)) {
218
+ data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
219
+ }
161
220
 
162
221
  if (statusCode !== undefined) {
163
222
  res.statusCode = statusCode;
@@ -166,21 +225,23 @@ export function createResponse(res: ServerResponse): Tina4Response {
166
225
  if (contentType) {
167
226
  // Explicit content type
168
227
  safeSetHeader("Content-Type", contentType);
169
- if (typeof data === "object" && data !== null && !Buffer.isBuffer(data)) {
228
+ if (Buffer.isBuffer(data)) {
229
+ safeEnd(data);
230
+ } else if (typeof data === "object" && data !== null) {
170
231
  safeEnd(JSON.stringify(data));
171
232
  } else {
172
233
  safeEnd(data == null ? "" : String(data));
173
234
  }
174
235
  } else if (typeof data === "object" && data !== null && !Buffer.isBuffer(data)) {
175
236
  // dict/array → auto JSON
176
- safeSetHeader("Content-Type", "application/json");
237
+ setDetectedContentType("application/json");
177
238
  safeEnd(JSON.stringify(data));
178
239
  } else if (typeof data === "string") {
179
240
  const trimmed = data.trim();
180
241
  if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
181
- safeSetHeader("Content-Type", "text/html; charset=utf-8");
242
+ setDetectedContentType("text/html; charset=utf-8");
182
243
  } else {
183
- safeSetHeader("Content-Type", "text/plain; charset=utf-8");
244
+ setDetectedContentType("text/plain; charset=utf-8");
184
245
  }
185
246
  safeEnd(data);
186
247
  } else if (Buffer.isBuffer(data)) {
@@ -191,7 +252,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
191
252
  } else if (data == null) {
192
253
  safeEnd("");
193
254
  } else {
194
- safeSetHeader("Content-Type", "text/plain; charset=utf-8");
255
+ setDetectedContentType("text/plain; charset=utf-8");
195
256
  safeEnd(String(data));
196
257
  }
197
258
 
@@ -246,6 +307,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
246
307
 
247
308
  response.header = function (name: string, value: string | number | readonly string[]): Tina4Response {
248
309
  safeSetHeader(name, value);
310
+ noteContentTypeHeader(name);
249
311
  return response;
250
312
  };
251
313
 
@@ -254,9 +316,11 @@ export function createResponse(res: ServerResponse): Tina4Response {
254
316
  */
255
317
  response.addHeader = function (name: string, value: string): void {
256
318
  safeSetHeader(name, value);
319
+ noteContentTypeHeader(name);
257
320
  };
258
321
 
259
322
  response.redirect = function (url: string, code?: number): Tina4Response {
323
+ assertSafeHeader("Location", url); // before the status changes
260
324
  if (res.headersSent) return response;
261
325
  res.statusCode = code ?? 302;
262
326
  safeSetHeader("Location", url);
@@ -265,6 +329,15 @@ export function createResponse(res: ServerResponse): Tina4Response {
265
329
  };
266
330
 
267
331
  response.cookie = function (name: string, value: string, options?: CookieOptions): Tina4Response {
332
+ // The name and value are percent-encoded below, so they cannot carry CR,
333
+ // LF, NUL or ';' to the wire and keep being encoded (ADR-0068). Attribute
334
+ // values are written as given, so one carrying those is refused: a ';'
335
+ // would add an attribute (say Domain=) the application never set.
336
+ for (const attribute of [options?.path, options?.domain, options?.sameSite, options?.maxAge]) {
337
+ if (attribute !== undefined && /[\r\n\0;]/.test(String(attribute))) {
338
+ throw headerRefusal(`Invalid character in cookie content [${quoteHeaderName(name)}]`, "ERR_INVALID_CHAR");
339
+ }
340
+ }
268
341
  const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
269
342
  if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);
270
343
  if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
@@ -419,6 +492,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
419
492
  source: AsyncIterable<string | Buffer>,
420
493
  contentType: string = "text/event-stream",
421
494
  ): Promise<Tina4Response> {
495
+ assertSafeHeader("Content-Type", contentType);
422
496
  if (res.headersSent) return response;
423
497
  res.writeHead(200, {
424
498
  "Content-Type": contentType,
@@ -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 { readdirSync, statSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
2
10
  import { join, relative, basename, extname } from "node:path";
3
11
  import type { RouteDefinition, RouteHandler, RouteMeta } from "./types.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
  import type { RouteHandler, RouteDefinition, RouteMeta, Middleware, MiddlewareSpec, Tina4Request, Tina4Response, WebSocketRouteHandler, WebSocketRouteDefinition } from "./types.js";
2
10
  import { isTruthy } from "./dotenv.js";
3
11
  import { MiddlewareRunner, isMiddlewareClass } from "./middleware.js";
@@ -1,4 +1,13 @@
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 { createServer, IncomingMessage, ServerResponse } from "node:http";
10
+ import { createTina4HttpServer, guardUnsafeHeaders, sendTransportRejection } from "./transport.js";
2
11
  import { randomBytes } from "node:crypto";
3
12
  import { resolve, dirname, join, relative } from "node:path";
4
13
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
@@ -21,7 +30,7 @@ import {
21
30
  TAKEOVER_KILLED,
22
31
  TAKEOVER_REFUSALS,
23
32
  } from "./portTakeover.js";
24
- import { createRequest } from "./request.js";
33
+ import { createRequest, PayloadTooLargeError } from "./request.js";
25
34
  import {
26
35
  resetRequestCaches,
27
36
  headStripIntercept,
@@ -36,7 +45,7 @@ import { isDebugMode } from "./errorOverlay.js";
36
45
  import { createHealthRoutes } from "./health.js";
37
46
  import { rateLimiter } from "./rateLimiter.js";
38
47
  import { Log } from "./logger.js";
39
- import { DevAdmin, RequestInspector, WsTracker, devMutationDenial, DEV_SAFE_METHODS } from "./devAdmin.js";
48
+ import { DevAdmin, RequestInspector, WsTracker, devRequestDenial, devHostAllowed } from "./devAdmin.js";
40
49
  import { CLOSE_GOING_AWAY, devReloadWs, serveWebSocketRoute, wsRouteManager } from "./websocket.js";
41
50
  import { feedbackEnabled, injectFeedbackWidget } from "./feedback.js";
42
51
  import { I18n } from "./i18n.js";
@@ -355,6 +364,35 @@ function findAvailablePort(start: number): number {
355
364
  }
356
365
  }
357
366
 
367
+ /**
368
+ * Variables that mark a CI run, in ADR-0070's order (ci_env_vars). A run is CI
369
+ * when any of them holds a value that, trimmed and lower-cased, is non-empty
370
+ * and not in CI_NOT_SET_VALUES.
371
+ */
372
+ export const CI_ENVIRONMENT_VARIABLES = ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "JENKINS_URL", "TF_BUILD", "TEAMCITY_VERSION"];
373
+
374
+ /** Values that say "not under CI" (ADR-0070 ci_not_set_values), compared trimmed and lower-cased. */
375
+ export const CI_NOT_SET_VALUES = ["false", "0", "no", "off"];
376
+
377
+ /**
378
+ * Whether the server may open a browser (ADR-0070): only for a developer at a
379
+ * desk. Every rule is a veto; nothing forces a browser open past one.
380
+ *
381
+ * 1. development only - the caller's resolved debug mode, and never with
382
+ * TINA4_PRODUCTION truthy or in a cluster worker (the primary decides);
383
+ * 2. never when TINA4_NO_BROWSER is truthy (true/1/yes/on);
384
+ * 3. never with --no-browser / a programmatic noBrowser (it can only veto);
385
+ * 4. never under CI.
386
+ */
387
+ export function shouldOpenBrowser(isDevelopment: boolean, noBrowser = false): boolean {
388
+ if (!isDevelopment || isTruthy(process.env.TINA4_PRODUCTION) || cluster.isWorker) return false;
389
+ if (noBrowser || isTruthy(process.env.TINA4_NO_BROWSER)) return false;
390
+ return !CI_ENVIRONMENT_VARIABLES.some((name) => {
391
+ const value = (process.env[name] ?? "").trim().toLowerCase();
392
+ return value !== "" && !CI_NOT_SET_VALUES.includes(value);
393
+ });
394
+ }
395
+
358
396
  /**
359
397
  * Open the user's default browser after a short delay so the server is ready.
360
398
  */
@@ -578,9 +616,9 @@ const HTTP_REASON_PHRASES: Record<number, string> = {
578
616
  304: "Not Modified", 307: "Temporary Redirect", 308: "Permanent Redirect",
579
617
  400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
580
618
  404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable",
581
- 409: "Conflict", 410: "Gone", 413: "Content Too Large",
619
+ 408: "Request Timeout", 409: "Conflict", 410: "Gone", 413: "Content Too Large",
582
620
  415: "Unsupported Media Type", 422: "Unprocessable Content",
583
- 429: "Too Many Requests",
621
+ 429: "Too Many Requests", 431: "Request Header Fields Too Large",
584
622
  500: "Internal Server Error", 501: "Not Implemented",
585
623
  502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout",
586
624
  };
@@ -628,6 +666,9 @@ export function resolveTemplate(pathname: string, templatesDir: string): string
628
666
  // Skip underscore-prefixed files even within pages/ — they're private
629
667
  // by Hugo/Jekyll convention (helpers, fragments) and shouldn't auto-serve.
630
668
  if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
669
+ // ADR-0082: a "." / ".." segment (or a backslash) could walk out of pages/
670
+ // - the only directory that auto-routes. Refuse it outright.
671
+ if (cleanPath.includes("\\") || cleanPath.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) return null;
631
672
  const pagesDir = resolve(templatesDir, TEMPLATE_PAGES_DIR);
632
673
  for (const ext of [".twig", ".html"]) {
633
674
  if (existsSync(resolve(pagesDir, cleanPath + ext))) {
@@ -1101,21 +1142,6 @@ async function runMatchedRoute(ctx: MatchedRouteContext): Promise<void> {
1101
1142
  const { req, res, match, postMatchMiddleware } = ctx;
1102
1143
  req.params = match.params as never;
1103
1144
 
1104
- // DEVADMIN-DEC-01/02: fail-closed same-origin + loopback gate on every /__dev
1105
- // write (POST/PUT/PATCH/DELETE), BEFORE the handler runs. Closes drive-by CSRF
1106
- // (a cross-origin page POSTing to /file/save then /reload) and a network-exposed
1107
- // debug box. Scoped to /__dev so /__feedback + /ai are unaffected; GET/HEAD/
1108
- // OPTIONS are safe and skip the gate. The MCP endpoints keep their own 404 gate
1109
- // (devMutationDenial skips the mcp prefixes for the loopback part).
1110
- const devMethod = (req.method ?? "GET").toUpperCase();
1111
- if (ctx.pathname.startsWith("/__dev") && !DEV_SAFE_METHODS.has(devMethod)) {
1112
- const denial = devMutationDenial(req);
1113
- if (denial) {
1114
- res.json({ ok: false, error: denial.error }, denial.status);
1115
- return;
1116
- }
1117
- }
1118
-
1119
1145
  if (await runGlobalMiddlewarePass(postMatchMiddleware, req, res)) return;
1120
1146
 
1121
1147
  // Auth enforcement lives in enforceRouteAuth (authGate.ts) so the in-process
@@ -1707,6 +1733,10 @@ async function dispatchInner(
1707
1733
  // These four close over nothing but the raw req/res (never ctx), which is
1708
1734
  // why they take no context object at all.
1709
1735
  await resetRequestCaches();
1736
+ // Installed FIRST so it is the innermost wrapper of end()/write(): whatever
1737
+ // the later interceptors do, a response carrying a header node:http refused
1738
+ // leaves as the ADR-0068 500 {"error":"Invalid response header"}.
1739
+ guardUnsafeHeaders(rawRes);
1710
1740
  headStripIntercept(rawReq, rawRes);
1711
1741
  // Feature 40 (CE-DEC-01/02): gzip + ETag + conditional-GET for every
1712
1742
  // dynamic response. Installed right after headStripIntercept so it runs
@@ -1718,6 +1748,11 @@ async function dispatchInner(
1718
1748
  // res.render() is handled natively by response.ts via Frond
1719
1749
 
1720
1750
  try {
1751
+ // Gate before middleware, matching and automatic OPTIONS handling (ADR-0082).
1752
+ if (req.path.replace(/\/+/g, "/").startsWith("/__dev")) {
1753
+ const denial = devRequestDenial(req);
1754
+ if (denial) { res.json({ ok: false, error: denial.error }, denial.status); return; }
1755
+ }
1721
1756
  // sessionAutoStart is the one prologue stage INSIDE the try. It degrades
1722
1757
  // on its own (ADR-0021), so the only thing that escapes it is a
1723
1758
  // TINA4_SESSION_STRICT refusal - and that must become a 500 through the
@@ -1740,6 +1775,16 @@ async function dispatchInner(
1740
1775
  try {
1741
1776
  await req.parseBody();
1742
1777
  } catch (err) {
1778
+ // The transport already answered (a 408 for a stalled body) and the
1779
+ // connection is going away: nothing left to say.
1780
+ if (rawRes.writableEnded) return;
1781
+ // A body over TINA4_MAX_UPLOAD_SIZE is a transport rejection (ADR-0068
1782
+ // section 4): exact body, security headers, and Connection: close, so
1783
+ // node:http does not go on reading the rest of the upload.
1784
+ if (err instanceof PayloadTooLargeError) {
1785
+ sendTransportRejection(rawRes, 413, err.message);
1786
+ return;
1787
+ }
1743
1788
  const status = (err as { statusCode?: number })?.statusCode;
1744
1789
  if (typeof status === "number" && status >= 400 && status < 500) {
1745
1790
  if (!rawRes.writableEnded) {
@@ -1910,7 +1955,7 @@ function startLoopbackSiblings(
1910
1955
  siblingHosts.map(
1911
1956
  (siblingHost) =>
1912
1957
  new Promise<ReturnType<typeof createServer> | null>((resolveSibling) => {
1913
- const sibling = createServer(dispatch);
1958
+ const sibling = createTina4HttpServer(dispatch);
1914
1959
  let settled = false;
1915
1960
  sibling.on("error", (err: NodeJS.ErrnoException) => {
1916
1961
  const expected =
@@ -2011,9 +2056,14 @@ export async function startServer(config?: Tina4Config): Promise<{
2011
2056
  // Auto-generate a per-machine dev secret to a gitignored .env.local when one
2012
2057
  // is missing (dev only, never CI/prod). Must run after env load and before
2013
2058
  // any auth use. Local import avoids a load-time cycle through auth.
2014
- const { ensureDevSecret } = await import("./auth.js");
2059
+ const { ensureDevSecret, requireBootSecret } = await import("./auth.js");
2015
2060
  ensureDevSecret();
2016
2061
 
2062
+ // Refuse to serve with a secret anyone can reproduce: blank outside dev, or
2063
+ // set but shorter than 32 bytes in any mode (ADR-0079 s2). Throws the
2064
+ // actionable message naming TINA4_SECRET.
2065
+ requireBootSecret();
2066
+
2017
2067
  // Refuse to boot with pre-3.12 un-prefixed env vars set.
2018
2068
  _checkLegacyEnvVars();
2019
2069
 
@@ -2186,7 +2236,7 @@ export async function startServer(config?: Tina4Config): Promise<{
2186
2236
  // dev; the stable AI port (port+1000, created below) suppresses reload/toolbar so an
2187
2237
  // AI tool can drive it without its own edits triggering refreshes. The tina4 client
2188
2238
  // posts /__dev/api/reload to the MAIN port. Matches Python (master).
2189
- const server = createServer(dispatch);
2239
+ const server = createTina4HttpServer(dispatch);
2190
2240
 
2191
2241
  // WebSocket upgrade handling on the MAIN port. Two responsibilities:
2192
2242
  //
@@ -2217,6 +2267,11 @@ export async function startServer(config?: Tina4Config): Promise<{
2217
2267
  const upPath = (req.url ?? "/").split("?")[0];
2218
2268
  // Dev-reload channel (debug only).
2219
2269
  if (isDevMode() && upPath === "/__dev_reload") {
2270
+ // ADR-0082: the reload socket answers only a loopback / TINA4_HOST Host.
2271
+ if (!devHostAllowed(req.headers.host)) {
2272
+ try { socket.write("HTTP/1.1 403 Forbidden\r\n\r\n"); socket.destroy(); } catch { /* gone */ }
2273
+ return;
2274
+ }
2220
2275
  devReloadWs.handleUpgrade(req, socket, head);
2221
2276
  return;
2222
2277
  }
@@ -2295,7 +2350,7 @@ export async function startServer(config?: Tina4Config): Promise<{
2295
2350
 
2296
2351
  if (isDebug && !noAiPort && aiPortInRange) {
2297
2352
  // Stable AI port (port+1000): tag requests so /__dev_reload + toolbar are suppressed.
2298
- aiServer = createServer(async (req, res) => {
2353
+ aiServer = createTina4HttpServer(async (req, res) => {
2299
2354
  (req as any)._tina4AiPort = true;
2300
2355
  await dispatch(req, res);
2301
2356
  });
@@ -2352,8 +2407,7 @@ ${reset}
2352
2407
  Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${dualPortLines}
2353
2408
  `);
2354
2409
  }
2355
- const noBrowser = isTruthy(process.env.TINA4_NO_BROWSER);
2356
- if (!noBrowser) {
2410
+ if (shouldOpenBrowser(isDebug, config?.noBrowser === true)) {
2357
2411
  // Open the browser on the MAIN port — that's the hot-reload port.
2358
2412
  openBrowser(`http://${displayHost}:${port}`);
2359
2413
  }
@@ -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
  * Service Runner — in-process background services using Node.js timers.
3
11
  * Zero dependencies. Supports cron timing, simple intervals, and daemon mode.
@@ -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 Session — Pluggable session backends, zero core dependencies.
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
  /**
2
10
  * Tina4 session handlers — turn a failed `execFileSync` child into a readable cause.
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
  /**
2
10
  * Tina4 Database Session Handler — sessions in a `tina4_session` table on ANY
3
11
  * engine the ORM Database layer supports: sqlite, postgres, mysql, mssql,
@@ -152,6 +160,16 @@ const CREATE_TABLE: Record<string, string> = {
152
160
  `,
153
161
  };
154
162
 
163
+ /** How many times ensureTable() tries the CREATE before a failure is real. */
164
+ const CREATE_ATTEMPTS = 5;
165
+ /** Base pause between tries; it grows linearly (50, 100, 150, 200 ms). */
166
+ const CREATE_RETRY_DELAY_MS = 50;
167
+
168
+ /** Block this thread for `ms` - the SessionHandler interface is synchronous. */
169
+ function sleepSync(ms: number): void {
170
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
171
+ }
172
+
155
173
  /**
156
174
  * Read a column out of a result row, case-insensitively.
157
175
  *
@@ -215,6 +233,12 @@ export class DatabaseSessionHandler implements SessionHandler {
215
233
  private get sqlite(): any {
216
234
  if (this.sqliteHandle === null) {
217
235
  this.sqliteHandle = new DatabaseSync(this.dbPath as string);
236
+ // Wait up to 5 s for another process's write lock instead of failing at
237
+ // once with "database is locked" - node:sqlite's default is NO wait, so
238
+ // racing processes collided on the first overlap (five of six died,
239
+ // measured). 5000 ms matches PHP's busyTimeout(5000) and Python's sqlite3
240
+ // default. Set BEFORE the WAL pragma so that write waits too.
241
+ this.sqliteHandle.exec("PRAGMA busy_timeout = 5000");
218
242
  this.sqliteHandle.exec("PRAGMA journal_mode = WAL");
219
243
  }
220
244
  return this.sqliteHandle;
@@ -334,16 +358,57 @@ export class DatabaseSessionHandler implements SessionHandler {
334
358
  }
335
359
 
336
360
  /**
337
- * Ensure the session table exists (called once on first use).
361
+ * Ensure the session table exists (called once on first use) - safely when
362
+ * several processes race to create it.
363
+ *
364
+ * Every per-engine statement above is check-then-act, so two processes can
365
+ * both find the table absent and both CREATE; the loser errors, and every
366
+ * engine spells that differently (a pg_type unique violation on PostgreSQL,
367
+ * Msg 2714 on SQL Server, an RDB$RELATIONS unique violation on Firebird). So
368
+ * a failure is judged by RE-CHECKING whether the table now exists, never by
369
+ * parsing the message - the rescue the Firebird note above has always
370
+ * assumed, and which was not here. MEASURED on the lab before it: six real
371
+ * processes racing lost one to five of them on PostgreSQL, SQLite and
372
+ * Firebird.
373
+ *
374
+ * The re-check is RETRIED because the loser can fail BEFORE the winner has
375
+ * committed. On MySQL, CREATE TABLE IF NOT EXISTS takes a shared metadata
376
+ * lock, checks, then upgrades it to exclusive; two upgrades deadlock, and when
377
+ * MySQL cannot back the victim off silently it answers 1213 "Deadlock found"
378
+ * while the winner's table is still invisible (tina4-php CI run 35972320442).
379
+ * Bounded, so a genuine failure (no CREATE grant) still throws. Same attempts
380
+ * and delays in all four frameworks.
338
381
  */
339
382
  private ensureTable(): void {
340
383
  if (this.initialized) return;
341
384
  const ddl = CREATE_TABLE[this.engine];
342
- if (this.target === null) this.sqlite.exec(ddl);
343
- else sqlCommandSync(this.target, ddl, []);
385
+ for (let attempt = 1; ; attempt++) {
386
+ try {
387
+ if (this.target === null) this.sqlite.exec(ddl);
388
+ else sqlCommandSync(this.target, ddl, []);
389
+ break;
390
+ } catch (error) {
391
+ if (this.tableExists()) break;
392
+ if (attempt >= CREATE_ATTEMPTS) throw error;
393
+ sleepSync(CREATE_RETRY_DELAY_MS * attempt);
394
+ }
395
+ }
344
396
  this.initialized = true;
345
397
  }
346
398
 
399
+ /**
400
+ * Whether tina4_session exists, asked the one way every engine answers alike:
401
+ * a query against it that returns no rows succeeds only if it is there.
402
+ */
403
+ private tableExists(): boolean {
404
+ try {
405
+ this.query("SELECT 1 FROM tina4_session WHERE 1 = 0", []);
406
+ return true;
407
+ } catch {
408
+ return false;
409
+ }
410
+ }
411
+
347
412
  read(sessionId: string): SessionData | null {
348
413
  this.ensureTable();
349
414
 
@@ -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 Memcached Session Handler — zero-dependency text protocol over TCP.
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
  /**
2
10
  * Tina4 synchronous MongoDB transport — ONE persistent client per target.
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
  /**
2
10
  * Tina4 MongoDB Session Handler — MongoDB wire protocol via raw TCP, zero dependencies.
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
  /**
2
10
  * Tina4 synchronous RESP transport — shared by every Redis/Valkey session handler.
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
  /**
2
10
  * Tina4 synchronous SQL transport for the database session backend.
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
  /**
2
10
  * Tina4 sync-over-async bridge — call an ASYNC worker from SYNCHRONOUS code.
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
  /**
2
10
  * Tina4 synchronous socket transport — ONE persistent connection per target.
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
  /**
2
10
  * Tina4 Valkey Session Handler — Valkey (Redis-compatible) via raw TCP, zero dependencies.
3
11
  *