tina4-nodejs 3.13.92 → 3.13.95

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 (193) hide show
  1. package/CLAUDE.md +170 -28
  2. package/README.md +2 -2
  3. package/package.json +13 -9
  4. package/packages/cli/dist/bin.js +33126 -30055
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +33062 -29908
  8. package/packages/core/src/ai.ts +7 -1
  9. package/packages/core/src/auth.ts +191 -39
  10. package/packages/core/src/background.ts +19 -19
  11. package/packages/core/src/cache.ts +492 -49
  12. package/packages/core/src/devAdmin.ts +79 -32
  13. package/packages/core/src/devMailbox.ts +20 -44
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +7 -6
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +81 -13
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +6 -9
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +751 -414
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  34. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  35. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  36. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
  37. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  38. package/packages/core/src/sessionHandlers/respClient.ts +16 -143
  39. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  40. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  41. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  42. package/packages/core/src/testClient.ts +18 -5
  43. package/packages/core/src/trustedProxy.ts +249 -0
  44. package/packages/core/src/types.ts +29 -5
  45. package/packages/core/src/websocket.ts +66 -0
  46. package/packages/frond/dist/index.js +74 -31
  47. package/packages/frond/src/engine.ts +99 -33
  48. package/packages/orm/dist/index.js +26554 -23400
  49. package/packages/orm/src/adapters/firebird.ts +183 -56
  50. package/packages/orm/src/adapters/mongodb.ts +25 -4
  51. package/packages/orm/src/adapters/mssql.ts +114 -29
  52. package/packages/orm/src/adapters/mysql.ts +103 -40
  53. package/packages/orm/src/adapters/odbc.ts +44 -21
  54. package/packages/orm/src/adapters/postgres.ts +118 -26
  55. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  56. package/packages/orm/src/adapters/sqlite.ts +64 -25
  57. package/packages/orm/src/baseModel.ts +135 -40
  58. package/packages/orm/src/cachedDatabase.ts +43 -19
  59. package/packages/orm/src/connectTimeout.ts +265 -0
  60. package/packages/orm/src/database.ts +338 -198
  61. package/packages/orm/src/databaseResult.ts +65 -13
  62. package/packages/orm/src/databaseUrl.ts +484 -0
  63. package/packages/orm/src/docstore.ts +386 -145
  64. package/packages/orm/src/index.ts +13 -3
  65. package/packages/orm/src/migration.ts +18 -3
  66. package/packages/orm/src/queryBuilder.ts +38 -4
  67. package/packages/orm/src/sqlTranslator.ts +310 -4
  68. package/packages/orm/src/types.ts +15 -4
  69. package/types/cli/src/bin.d.ts +92 -0
  70. package/types/cli/src/commands/build.d.ts +2 -0
  71. package/types/cli/src/commands/generate.d.ts +47 -0
  72. package/types/cli/src/commands/init.d.ts +1 -0
  73. package/types/cli/src/commands/metrics.d.ts +6 -0
  74. package/types/cli/src/commands/migrate.d.ts +1 -0
  75. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  76. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  77. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  78. package/types/cli/src/commands/queue.d.ts +20 -0
  79. package/types/cli/src/commands/routes.d.ts +1 -0
  80. package/types/cli/src/commands/seed.d.ts +1 -0
  81. package/types/cli/src/commands/serve.d.ts +6 -0
  82. package/types/cli/src/commands/test.d.ts +1 -0
  83. package/types/core/src/ai.d.ts +64 -0
  84. package/types/core/src/api.d.ts +262 -0
  85. package/types/core/src/auth.d.ts +177 -0
  86. package/types/core/src/authGate.d.ts +20 -0
  87. package/types/core/src/background.d.ts +34 -0
  88. package/types/core/src/cache.d.ts +163 -0
  89. package/types/core/src/constants.d.ts +38 -0
  90. package/types/core/src/container.d.ts +44 -0
  91. package/types/core/src/context/chunker.d.ts +31 -0
  92. package/types/core/src/context/index.d.ts +93 -0
  93. package/types/core/src/devAdmin.d.ts +179 -0
  94. package/types/core/src/devMailbox.d.ts +54 -0
  95. package/types/core/src/dispatchPipeline.d.ts +117 -0
  96. package/types/core/src/docs.d.ts +141 -0
  97. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  98. package/types/core/src/dotenv.d.ts +87 -0
  99. package/types/core/src/env.d.ts +28 -0
  100. package/types/core/src/errorOverlay.d.ts +36 -0
  101. package/types/core/src/events.d.ts +75 -0
  102. package/types/core/src/fakeData.d.ts +55 -0
  103. package/types/core/src/feedback.d.ts +90 -0
  104. package/types/core/src/graphql.d.ts +207 -0
  105. package/types/core/src/health.d.ts +22 -0
  106. package/types/core/src/htmlElement.d.ts +75 -0
  107. package/types/core/src/i18n.d.ts +37 -0
  108. package/types/core/src/index.d.ts +92 -0
  109. package/types/core/src/job.d.ts +39 -0
  110. package/types/core/src/logger.d.ts +200 -0
  111. package/types/core/src/mcp.d.ts +248 -0
  112. package/types/core/src/messenger.d.ts +191 -0
  113. package/types/core/src/metrics.d.ts +41 -0
  114. package/types/core/src/middleware.d.ts +330 -0
  115. package/types/core/src/mqtt.d.ts +257 -0
  116. package/types/core/src/mqttMessage.d.ts +67 -0
  117. package/types/core/src/plan.d.ts +96 -0
  118. package/types/core/src/projectIndex.d.ts +56 -0
  119. package/types/core/src/queue.d.ts +268 -0
  120. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  121. package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
  122. package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
  123. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  124. package/types/core/src/rateLimiter.d.ts +49 -0
  125. package/types/core/src/request.d.ts +25 -0
  126. package/types/core/src/response.d.ts +28 -0
  127. package/types/core/src/routeDiscovery.d.ts +12 -0
  128. package/types/core/src/router.d.ts +366 -0
  129. package/types/core/src/scss.d.ts +19 -0
  130. package/types/core/src/server.d.ts +146 -0
  131. package/types/core/src/service.d.ts +115 -0
  132. package/types/core/src/session.d.ts +341 -0
  133. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  134. package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
  135. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  136. package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
  137. package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
  138. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  139. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  140. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  141. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  142. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  143. package/types/core/src/static.d.ts +2 -0
  144. package/types/core/src/test.d.ts +94 -0
  145. package/types/core/src/testClient.d.ts +36 -0
  146. package/types/core/src/testing.d.ts +58 -0
  147. package/types/core/src/trustedProxy.d.ts +44 -0
  148. package/types/core/src/types.d.ts +242 -0
  149. package/types/core/src/validator.d.ts +52 -0
  150. package/types/core/src/websocket.d.ts +402 -0
  151. package/types/core/src/websocketBackplane.d.ts +166 -0
  152. package/types/core/src/websocketConnection.d.ts +54 -0
  153. package/types/core/src/wsdl.d.ts +101 -0
  154. package/types/frond/src/engine.d.ts +263 -0
  155. package/types/frond/src/index.d.ts +2 -0
  156. package/types/orm/src/adapters/firebird.d.ts +183 -0
  157. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  158. package/types/orm/src/adapters/mssql.d.ts +77 -0
  159. package/types/orm/src/adapters/mysql.d.ts +67 -0
  160. package/types/orm/src/adapters/odbc.d.ts +94 -0
  161. package/types/orm/src/adapters/postgres.d.ts +86 -0
  162. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  163. package/types/orm/src/adapters/sqlite.d.ts +68 -0
  164. package/types/orm/src/autoCrud.d.ts +73 -0
  165. package/types/orm/src/baseModel.d.ts +427 -0
  166. package/types/orm/src/cachedDatabase.d.ts +190 -0
  167. package/types/orm/src/connectTimeout.d.ts +100 -0
  168. package/types/orm/src/database.d.ts +655 -0
  169. package/types/orm/src/databaseResult.d.ts +109 -0
  170. package/types/orm/src/databaseUrl.d.ts +125 -0
  171. package/types/orm/src/docstore.d.ts +241 -0
  172. package/types/orm/src/fakeData.d.ts +22 -0
  173. package/types/orm/src/index.d.ts +43 -0
  174. package/types/orm/src/migration.d.ts +275 -0
  175. package/types/orm/src/model.d.ts +7 -0
  176. package/types/orm/src/query.d.ts +14 -0
  177. package/types/orm/src/queryBuilder.d.ts +193 -0
  178. package/types/orm/src/realtime/index.d.ts +7 -0
  179. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  180. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  181. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  182. package/types/orm/src/realtime/models/message.d.ts +36 -0
  183. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  184. package/types/orm/src/realtime/realtime.d.ts +24 -0
  185. package/types/orm/src/realtime/storage.d.ts +61 -0
  186. package/types/orm/src/seeder.d.ts +118 -0
  187. package/types/orm/src/sqlTranslator.d.ts +258 -0
  188. package/types/orm/src/types.d.ts +148 -0
  189. package/types/orm/src/validation.d.ts +6 -0
  190. package/types/swagger/src/generator.d.ts +46 -0
  191. package/types/swagger/src/index.d.ts +2 -0
  192. package/types/swagger/src/ui.d.ts +11 -0
  193. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
@@ -1,5 +1,5 @@
1
- import { readFileSync, existsSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { readFileSync, existsSync, statSync } from "node:fs";
2
+ import { resolve, join, dirname } from "node:path";
3
3
 
4
4
  /**
5
5
  * Parse a .env file content string into key-value pairs.
@@ -12,13 +12,52 @@ import { resolve } from "node:path";
12
12
  * - Empty lines
13
13
  * - Multi-line with trailing backslash \
14
14
  */
15
+ const VALID_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+ const REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
17
+
18
+ /** Emit a parse warning. dotenv loads before the logger exists, so stderr. */
19
+ function warnEnv(message: string): void {
20
+ process.stderr.write(`[tina4] ${message}\n`);
21
+ }
22
+
23
+ /**
24
+ * Expand ${VAR} against already-loaded keys plus the real environment.
25
+ *
26
+ * `process.env` is checked FIRST so the effective value wins: loading is
27
+ * first-wins, so a key already in the real environment is what the process will
28
+ * actually see, and an interpolation that resolved against the file's value
29
+ * instead would disagree with it. `parsed` then supplies keys set earlier in
30
+ * this same file, which are not in process.env until the whole file is applied.
31
+ *
32
+ * An unresolved name stays LITERAL and is warned about once per name, so a typo
33
+ * is visible without breaking the load.
34
+ */
35
+ function interpolate(
36
+ value: string,
37
+ parsed: Record<string, string>,
38
+ lineNo: number,
39
+ warnedRefs: Set<string>
40
+ ): string {
41
+ return value.replace(REFERENCE, (whole, name: string) => {
42
+ const resolved = process.env[name] ?? parsed[name];
43
+ if (resolved !== undefined) return resolved;
44
+ if (!warnedRefs.has(name)) {
45
+ warnedRefs.add(name);
46
+ warnEnv(`.env:${lineNo}: \${${name}} is not set, left as-is`);
47
+ }
48
+ return whole;
49
+ });
50
+ }
51
+
15
52
  function parseEnvContent(content: string): Record<string, string> {
16
53
  const result: Record<string, string> = {};
17
54
  const lines = content.split("\n");
55
+ const warnedRefs = new Set<string>();
18
56
  let i = 0;
19
57
 
20
58
  while (i < lines.length) {
21
59
  let line = lines[i].trim();
60
+ const lineNo = i + 1;
22
61
  i++;
23
62
 
24
63
  // Skip empty lines and comments
@@ -31,18 +70,41 @@ function parseEnvContent(content: string): Record<string, string> {
31
70
  line = line.slice(7).trim();
32
71
  }
33
72
 
34
- // Find the first = sign
73
+ // Find the first = sign. A line with no "=" sets nothing, so say so rather
74
+ // than dropping it in silence.
35
75
  const eqIndex = line.indexOf("=");
36
76
  if (eqIndex === -1) {
77
+ warnEnv(`.env:${lineNo}: no '=' in "${line}", line skipped`);
37
78
  continue;
38
79
  }
39
80
 
40
81
  const key = line.slice(0, eqIndex).trim();
82
+ if (!VALID_KEY.test(key)) {
83
+ warnEnv(`.env:${lineNo}: invalid key "${key}", line skipped`);
84
+ continue;
85
+ }
41
86
  let value = line.slice(eqIndex + 1).trim();
42
87
 
43
- // Handle quoted values
44
- if (value.startsWith('"') && value.endsWith('"')) {
45
- value = value.slice(1, -1);
88
+ // Handle quoted values. Quoting decides escapes AND interpolation, in that
89
+ // order -- the cross-framework behaviour table (feature 1 of the audit).
90
+ // A QUOTED value ends at its CLOSING QUOTE, and anything after it is a
91
+ // comment. Testing the LAST character instead was wrong: a trailing comment
92
+ // makes the last character non-quote, so `PW="s3cret" # note` fell to the
93
+ // unquoted branch, which strips only the ` #` and left the QUOTE CHARACTERS
94
+ // in the value -- a credential handed to a driver as '"s3cret"'. PHP already
95
+ // scanned for the terminator; this is that mechanism, and the scan SKIPS a
96
+ // quote preceded by a backslash so an escaped \" cannot end the value early.
97
+ const quote = value[0];
98
+ let closing = -1;
99
+ if (quote === '"' || quote === "'") {
100
+ for (let j = 1; j < value.length; j++) {
101
+ if (value[j] === "\\" && quote === '"' && j + 1 < value.length) { j++; continue; }
102
+ if (value[j] === quote) { closing = j; break; }
103
+ }
104
+ }
105
+
106
+ if (closing !== -1 && quote === '"') {
107
+ value = value.slice(1, closing);
46
108
  // Process escape sequences in double-quoted values
47
109
  value = value
48
110
  .replace(/\\n/g, "\n")
@@ -50,20 +112,25 @@ function parseEnvContent(content: string): Record<string, string> {
50
112
  .replace(/\\t/g, "\t")
51
113
  .replace(/\\"/g, '"')
52
114
  .replace(/\\\\/g, "\\");
53
- } else if (value.startsWith("'") && value.endsWith("'")) {
54
- // Single-quoted: literal, no escape processing
55
- value = value.slice(1, -1);
115
+ value = interpolate(value, result, lineNo, warnedRefs);
116
+ } else if (closing !== -1 && quote === "'") {
117
+ // Single-quoted: verbatim. No escape processing, and NO interpolation --
118
+ // shell semantics, and the documented way to keep a literal ${...}.
119
+ value = value.slice(1, closing);
56
120
  } else {
57
- // Unquoted: handle multi-line with trailing backslash
58
- while (value.endsWith("\\") && i < lines.length) {
59
- value = value.slice(0, -1) + lines[i].trim();
60
- i++;
61
- }
121
+ // NOTE: the backslash line-continuation loop that used to live here is
122
+ // GONE. It was a Node-only extension -- absent from Python, PHP and Ruby,
123
+ // absent from the shared corpus, and undocumented -- so the SAME .env file
124
+ // produced a DIFFERENT SET OF VARIABLES on Node than on the other three.
125
+ // That is the strongest form of swap break (ADR-0024), and the least-code
126
+ // fix is deletion: four lines removed, nothing added.
127
+
62
128
  // Strip inline comments (only for unquoted values)
63
129
  const commentIndex = value.indexOf(" #");
64
130
  if (commentIndex !== -1) {
65
131
  value = value.slice(0, commentIndex).trim();
66
132
  }
133
+ value = interpolate(value, result, lineNo, warnedRefs);
67
134
  }
68
135
 
69
136
  result[key] = value;
@@ -73,46 +140,98 @@ function parseEnvContent(content: string): Record<string, string> {
73
140
  }
74
141
 
75
142
  /**
76
- * Load environment variables from a .env file into process.env.
77
- *
78
- * By default does NOT override existing process.env values it is first-wins:
79
- * a key is only set if it is not already present. This is how real env vars
80
- * always win. To get the precedence real-env > `.env.local` > `.env`, load
81
- * `.env.local` FIRST then `.env`, both with override=false (the default): the
82
- * real env (already present) wins over both, `.env.local` fills local-only keys,
83
- * and `.env` fills the rest. Do NOT load `.env.local` with override=true that
84
- * would let a stray gitignored `.env.local` clobber an explicitly set real env
85
- * var (e.g. a production TINA4_SECRET).
86
- *
87
- * Resolution order for the env file path:
88
- * 1. Explicit `path` argument
89
- * 2. `TINA4_ENV_FILE` env var (if set and non-empty)
90
- * 3. `.env` in the current working directory
91
- *
92
- * @param path - Path to the .env file. Optional override.
143
+ * Load environment variables from a root DIRECTORY or a single .env file.
144
+ *
145
+ * Pass a **directory** and it loads `<dir>/.env.local` then `<dir>/.env`, both
146
+ * first-wins, which IS the precedence real-env > `.env.local` > `.env`. That is
147
+ * the canonical form in all four frameworks.
148
+ *
149
+ * Before this, the ordering was the CALLER's job and this doc comment was the
150
+ * only place it was written down: load `.env.local` first, then `.env`, both
151
+ * with override=false. Every caller had to remember, and getting it wrong
152
+ * (override=true on `.env.local`) lets a stray gitignored file clobber an
153
+ * explicitly set real env var such as a production TINA4_SECRET. A rule nobody
154
+ * can forget beats a rule written in a comment.
155
+ *
156
+ * A **file** path still works exactly as before: only that file is read, and the
157
+ * caller owns the ordering.
158
+ *
159
+ * By default this does NOT override existing process.env values it is
160
+ * first-wins, which is how a real env var always beats both files.
161
+ *
162
+ * Resolution order when `path` is omitted:
163
+ * 1. `TINA4_ENV_FILE` env var (if set and non-empty) — the named file, plus
164
+ * `.env.local` BESIDE it, so pointing at `.env.staging` does not silently
165
+ * stop honouring local overrides
166
+ * 2. the current working directory, as a root
167
+ *
168
+ * @param path - A root directory (canonical) OR a path to a single .env file.
93
169
  * @param override - When true, overwrite keys already present in process.env.
94
- * @returns The parsed key-value pairs, or an empty object if the file doesn't exist.
170
+ * @returns The parsed key-value pairs. For the directory form this is the merge
171
+ * of both files, with `.env.local` winning on a duplicate key.
95
172
  */
96
173
  export function loadEnv(path?: string, override = false): Record<string, string> {
97
174
  const fromEnv = (process.env.TINA4_ENV_FILE ?? "").trim();
98
- const target = path ?? (fromEnv.length > 0 ? fromEnv : ".env");
99
- const envPath = resolve(target);
100
175
 
176
+ if (path === undefined && fromEnv.length > 0) {
177
+ const named = resolve(fromEnv);
178
+ return mergeFirstWins(
179
+ loadEnvFile(join(dirname(named), ".env.local"), override),
180
+ loadEnvFile(named, override),
181
+ );
182
+ }
183
+
184
+ const target = resolve(path ?? ".");
185
+ if (existsSync(target) && statSync(target).isDirectory()) {
186
+ // .env.local FIRST so it beats .env; both first-wins, so a variable already
187
+ // in the real environment still beats both.
188
+ return mergeFirstWins(
189
+ loadEnvFile(join(target, ".env.local"), override),
190
+ loadEnvFile(join(target, ".env"), override),
191
+ );
192
+ }
193
+
194
+ return loadEnvFile(target, override);
195
+ }
196
+
197
+ /** Merge two parsed maps, first argument winning on a duplicate key. */
198
+ function mergeFirstWins(
199
+ first: Record<string, string>,
200
+ second: Record<string, string>,
201
+ ): Record<string, string> {
202
+ return { ...second, ...first };
203
+ }
204
+
205
+ /**
206
+ * Load ONE .env file. A missing file is not an error and yields `{}` - a fresh
207
+ * checkout has no `.env.local`, and the directory form reads it unconditionally.
208
+ */
209
+ function loadEnvFile(envPath: string, override: boolean): Record<string, string> {
101
210
  if (!existsSync(envPath)) {
102
211
  return {};
103
212
  }
104
213
 
105
214
  const content = readFileSync(envPath, "utf-8");
106
215
  const parsed = parseEnvContent(content);
216
+ const effective: Record<string, string> = {};
107
217
 
108
218
  for (const [key, value] of Object.entries(parsed)) {
219
+ // First-wins by default: a variable already in the real environment is
220
+ // never clobbered.
109
221
  if (override || process.env[key] === undefined) {
110
222
  process.env[key] = value;
111
223
  _loadedKeys.push(key);
112
224
  }
225
+ // Report the value that WON, not the one this file declared. They differ
226
+ // exactly when the real environment beat the file, which is the case an
227
+ // operator most needs to see: reporting the file's value there means the
228
+ // returned map says "from_local" while the process is actually running on
229
+ // "from_REAL". A map that disagrees with process.env is worse than no map,
230
+ // because it looks authoritative.
231
+ effective[key] = process.env[key] as string;
113
232
  }
114
233
 
115
- return parsed;
234
+ return effective;
116
235
  }
117
236
 
118
237
  /**
@@ -133,12 +252,38 @@ export function getEnv(key: string, defaultValue?: string): string | undefined {
133
252
  * @returns The environment variable value.
134
253
  * @throws Error if the variable is not set.
135
254
  */
136
- export function requireEnv(key: string): string {
137
- const value = process.env[key];
138
- if (value === undefined) {
139
- throw new Error(`Required environment variable "${key}" is not set.`);
255
+ /**
256
+ * Validate that required environment variables exist, and return them.
257
+ *
258
+ * Takes VARARGS and returns a map, matching Python, PHP and Ruby. It used to
259
+ * take one key and return that value, so checking five variables meant five
260
+ * calls that each failed on the first problem - an operator fixing a deployment
261
+ * got one name per restart instead of the whole list.
262
+ *
263
+ * @param keys - Variable names that must be set.
264
+ * @returns Every requested key mapped to its value.
265
+ * @throws Error naming ALL missing variables, not just the first.
266
+ */
267
+ export function requireEnv(...keys: string[]): Record<string, string> {
268
+ const missing: string[] = [];
269
+ const found: Record<string, string> = {};
270
+
271
+ for (const key of keys) {
272
+ const value = process.env[key];
273
+ if (value === undefined) {
274
+ missing.push(key);
275
+ continue;
276
+ }
277
+ found[key] = value;
140
278
  }
141
- return value;
279
+
280
+ if (missing.length > 0) {
281
+ throw new Error(
282
+ `Missing required environment variables: ${missing.join(", ")}`,
283
+ );
284
+ }
285
+
286
+ return found;
142
287
  }
143
288
 
144
289
  /**
@@ -6,6 +6,7 @@ export type {
6
6
  RouteMeta,
7
7
  Tina4Config,
8
8
  Middleware,
9
+ MiddlewareClass,
9
10
  MiddlewareSpec,
10
11
  UploadedFile,
11
12
  CookieOptions,
@@ -29,6 +30,7 @@ export { Env } from "./env.js";
29
30
  export { Log } from "./logger.js";
30
31
  export { createHealthRoute, createHealthRoutes, healthPath } from "./health.js";
31
32
  export { rateLimiter } from "./rateLimiter.js";
33
+ export { isTrustedProxy, trustedProxyNetworks, resolveClientIp, resetTrustedProxyCache } from "./trustedProxy.js";
32
34
  export type { RateLimiterConfig } from "./rateLimiter.js";
33
35
  export {
34
36
  HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, HTTP_NO_CONTENT,
@@ -46,9 +48,10 @@ export {
46
48
  authMiddleware,
47
49
  refreshToken, authenticateRequest, validateApiKey,
48
50
  ensureDevSecret,
51
+ resolveAlgorithm, algorithmAvailable, availableAlgorithms,
49
52
  Auth,
50
53
  } from "./auth.js";
51
- export { Session, FileSessionHandler, RedisSessionHandler, buildSessionCookie, isSecureScheme, sessionCookieName } from "./session.js";
54
+ export { Session, FileSessionHandler, RedisSessionHandler, buildSessionCookie, isSecureScheme, isValidSessionId, sessionCookieName, VALID_SESSION_BACKENDS, CANONICAL_SESSION_BACKENDS } from "./session.js";
52
55
  export type { SessionConfig, SessionHandler } from "./session.js";
53
56
  export { I18n } from "./i18n.js";
54
57
  export { FakeData } from "./fakeData.js";
@@ -75,7 +78,7 @@ export {
75
78
  export type { WebSocketClient } from "./websocket.js";
76
79
  export { ServiceRunner, Tina4Service, matchCronField, matchesCron } from "./service.js";
77
80
  export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
78
- export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, createBackend, _resetBackend } from "./cache.js";
81
+ export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, sweep, createBackend, _resetBackend } from "./cache.js";
79
82
  export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
80
83
  export { Api } from "./api.js";
81
84
  export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions } from "./api.js";
@@ -94,9 +97,9 @@ export {
94
97
  handleFeedbackWidgetJs,
95
98
  registerFeedbackRoutes,
96
99
  } from "./feedback.js";
97
- export { Messenger, MessengerConnectionError } from "./messenger.js";
100
+ export { Messenger, MessengerConnectionError, createMessenger } from "./messenger.js";
98
101
  export type { SendResult, EmailMessage } from "./messenger.js";
99
- export { DevMailbox, createMessenger } from "./devMailbox.js";
102
+ export { DevMailbox } from "./devMailbox.js";
100
103
  export { WSDLService, WSDLOperation } from "./wsdl.js";
101
104
  export type { WSDLOperationMeta } from "./wsdl.js";
102
105
  export { HtmlElement, htmlElement, addHtmlHelpers, Raw, SafeString } from "./htmlElement.js";
@@ -121,8 +124,6 @@ export { MongoSessionHandler } from "./sessionHandlers/mongoHandler.js";
121
124
  export type { MongoSessionConfig } from "./sessionHandlers/mongoHandler.js";
122
125
  export { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
123
126
  export type { ValkeySessionConfig } from "./sessionHandlers/valkeyHandler.js";
124
- export { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
125
- export type { RedisNpmSessionConfig } from "./sessionHandlers/redisHandler.js";
126
127
  export { tests, assertEqual, assertRaises, assertTrue, assertFalse, runAll, reset } from "./testing.js";
127
128
  export { TestClient, TestResponse } from "./testClient.js";
128
129
  export { Tina4Test, AssertionError as Tina4AssertionError } from "./test.js";