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
@@ -995,4 +995,10 @@ export function generateContext(toolName: string = "claude-code"): string {
995
995
  }
996
996
  }
997
997
 
998
- export { AiTool as AiToolType };
998
+ // `export type`, not `export {}`: AiTool is an interface, so a value re-export
999
+ // emits a runtime binding that does not exist. tsc (without isolatedModules)
1000
+ // and esbuild both infer the intent and elide it, but any single-file
1001
+ // transpiler must be told -- Node's own TypeScript support compiles module by
1002
+ // module and threw `SyntaxError: Export 'AiTool' is not defined in module`,
1003
+ // which made every module reachable from this barrel unloadable by plain node.
1004
+ export type { AiTool as AiToolType };
@@ -127,38 +127,122 @@ export function ensureDevSecret(cwd?: string): string | null {
127
127
  return newSecret;
128
128
  }
129
129
 
130
- // ── JWT algorithms ────────────────────────────────────────────────
130
+ // ── JWT algorithms + runtime capability ───────────────────────────
131
131
  //
132
- // Mirrors the Python master (tina4_python/auth/__init__.py)the digest is
133
- // LOOKED UP from the configured algorithm rather than hardcoded, so the "alg"
134
- // advertised in the header is always the one that actually produced the
135
- // signature (python#105). TINA4_JWT_ALGORITHM is read for real, and an
136
- // algorithm we cannot sign fails loudly instead of silently downgrading to
137
- // HS256 (python#106).
138
-
139
- /** HMAC algorithms their node:crypto digest name. All in node:crypto zero dependencies. */
132
+ // HMAC HS256 / HS384 / HS512 is THE Tina4 JWT algorithm family, and it is
133
+ // zero-dependency in all four frameworks:
134
+ // python hmac + hashlib (stdlib)
135
+ // php hash_hmac (ext-hash, PHP core)
136
+ // ruby OpenSSL::HMAC (stdlib default gem, NOT the jwt gem)
137
+ // node node:crypto createHmac (builtin)
138
+ //
139
+ // RS256 is an OPT-IN EXTRA, offered only where the RUNTIME provides asymmetric
140
+ // crypto natively — never as a third-party dependency. Node gets it from the
141
+ // same builtin node:crypto, so tina4-nodejs is the REFERENCE for what RS256
142
+ // looks like when available; tina4-php has it from core ext-openssl and
143
+ // tina4-ruby from its stdlib openssl default gem. tina4-python does NOT offer
144
+ // it, because Python's standard library has no asymmetric crypto at all and
145
+ // Tina4 will not add a package for it. That is not an outlier: HMAC is the
146
+ // standard, not a fallback.
147
+ //
148
+ // The capability is PROBED rather than assumed (see `capabilityFailure`). A
149
+ // table says what we intend to support; only running the primitive says what
150
+ // THIS build can do. A crypto provider that refuses RSA-SHA256 must produce the
151
+ // same loud, actionable failure a developer gets from tina4-python — never a
152
+ // silent downgrade to HMAC, never a mysterious `false`.
153
+ //
154
+ // SECURITY, documented rather than buried: HMAC is SYMMETRIC. Every service
155
+ // that VERIFIES a token holds the SAME secret that signs it, so any verifier
156
+ // can also MINT tokens. That is fine for one app or a fleet you control, and
157
+ // WRONG for handing tokens to a third party you do not — RS256 exists so a
158
+ // verifier can hold only the public key.
159
+ //
160
+ // The digest is LOOKED UP from the configured algorithm rather than hardcoded,
161
+ // so the "alg" advertised in the header is always the one that actually
162
+ // produced the signature (python#105). TINA4_JWT_ALGORITHM is read for real,
163
+ // and an algorithm we cannot sign fails loudly instead of silently downgrading
164
+ // to HS256 (python#106).
165
+
166
+ /** HMAC algorithms → their node:crypto digest name. The cross-framework standard. */
140
167
  const HMAC_DIGESTS = new Map<string, string>([
141
168
  ["HS256", "sha256"],
142
169
  ["HS384", "sha384"],
143
170
  ["HS512", "sha512"],
144
171
  ]);
145
172
 
146
- /**
147
- * RSA algorithms → their node:crypto sign/verify algorithm name.
148
- *
149
- * Node ships `node:crypto`, so RS256 is legitimately available here at zero
150
- * dependency cost. Python and Ruby cannot do RS256 without a third-party
151
- * package, so RS256 is a documented PHP/Node-only EXTRA, not a parity
152
- * requirement — the HMAC family is the cross-framework contract.
153
- */
173
+ /** RSA algorithms → their node:crypto sign/verify algorithm name. The opt-in extra. */
154
174
  const RSA_SIGN_ALGORITHMS = new Map<string, string>([["RS256", "RSA-SHA256"]]);
155
175
 
156
- /** Every algorithm Tina4 for Node can sign and verify, in the order we advertise them. */
157
- const SUPPORTED_ALGORITHMS: readonly string[] = [
176
+ /** Every algorithm Tina4 for Node KNOWS, available in this runtime or not. */
177
+ const KNOWN_ALGORITHMS: readonly string[] = [
158
178
  ...HMAC_DIGESTS.keys(),
159
179
  ...RSA_SIGN_ALGORITHMS.keys(),
160
180
  ];
161
181
 
182
+ /**
183
+ * How to get an algorithm back when this runtime cannot provide it. Only the
184
+ * remedy differs between frameworks; the sentence around it is identical, so
185
+ * "RS256 is not available" reads the same everywhere.
186
+ */
187
+ const ALGORITHM_REMEDY = new Map<string, string>([
188
+ [
189
+ "RS256",
190
+ "RS256 comes from builtin node:crypto, so no package installs it — this build's " +
191
+ 'crypto provider refused RSA-SHA256 (check `node -p "process.versions.openssl"`)',
192
+ ],
193
+ ]);
194
+
195
+ /** Probe results, memoised: algorithm → null when usable here, else the runtime's own reason. */
196
+ const capabilityCache = new Map<string, string | null>();
197
+
198
+ /**
199
+ * Ask the RUNTIME — not a lookup table — whether it can actually sign and
200
+ * verify with `algorithm`, and remember the answer.
201
+ *
202
+ * Only meaningful for a KNOWN algorithm; every caller checks membership first.
203
+ *
204
+ * @returns `null` when the algorithm works here, else the runtime's own reason.
205
+ */
206
+ function capabilityFailure(algorithm: string): string | null {
207
+ const cached = capabilityCache.get(algorithm);
208
+ if (cached !== undefined) return cached;
209
+
210
+ let failure: string | null = null;
211
+ try {
212
+ const digest = HMAC_DIGESTS.get(algorithm);
213
+ if (digest !== undefined) {
214
+ createHmac(digest, "tina4-capability-probe").update("").digest();
215
+ } else {
216
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm) as string;
217
+ createSign(rsaAlgorithm).update("");
218
+ createVerify(rsaAlgorithm).update("");
219
+ }
220
+ } catch (error) {
221
+ failure = error instanceof Error ? error.message : String(error);
222
+ }
223
+ capabilityCache.set(algorithm, failure);
224
+ return failure;
225
+ }
226
+
227
+ /**
228
+ * Can this runtime actually sign and verify `algorithm` right now?
229
+ *
230
+ * The cross-framework capability check — same question, same answer shape, in
231
+ * all four frameworks. HMAC answers `true` everywhere. RS256 answers `true`
232
+ * only where the runtime ships asymmetric crypto natively (node:crypto here,
233
+ * core ext-openssl in PHP, the stdlib openssl gem in Ruby) and `false` in
234
+ * tina4-python. An algorithm Tina4 does not know at all answers `false`.
235
+ */
236
+ export function algorithmAvailable(algorithm: string): boolean {
237
+ const chosen = String(algorithm ?? "").trim();
238
+ return KNOWN_ALGORITHMS.includes(chosen) && capabilityFailure(chosen) === null;
239
+ }
240
+
241
+ /** Every algorithm this runtime can sign and verify right now, in advertised order. */
242
+ export function availableAlgorithms(): string[] {
243
+ return KNOWN_ALGORITHMS.filter((algorithm) => capabilityFailure(algorithm) === null);
244
+ }
245
+
162
246
  /**
163
247
  * Seconds of clock skew tolerated on the "nbf" (not-before) claim.
164
248
  *
@@ -168,28 +252,77 @@ const SUPPORTED_ALGORITHMS: readonly string[] = [
168
252
  */
169
253
  export const JWT_LEEWAY_SECONDS = 60;
170
254
 
171
- /** The loud, actionable failure for an algorithm we cannot sign — names the supported set. */
255
+ /**
256
+ * Coerce an RFC 7519 NumericDate claim to integer seconds, else `null`.
257
+ *
258
+ * RFC 7519 s2 defines `exp`/`nbf`/`iat` as a NumericDate — a JSON numeric
259
+ * value. A claim that is PRESENT but not a number is malformed, and a malformed
260
+ * constraint must never read as "no constraint": treating a non-numeric `exp`
261
+ * as absent turns a broken token into one that never expires.
262
+ *
263
+ * `typeof value === "number"` already excludes boolean, string, null, array and
264
+ * object; the finite check additionally rejects NaN and Infinity, which would
265
+ * otherwise make every comparison silently false. Truncation is toward the
266
+ * earlier second, so a fractional expiry can only expire sooner, never later.
267
+ */
268
+ function numericDate(value: unknown): number | null {
269
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
270
+ return Math.floor(value);
271
+ }
272
+
273
+ /** The loud, actionable failure for an algorithm Tina4 does not know at all. */
172
274
  function unsupportedAlgorithmError(algorithm: string): Error {
173
275
  return new Error(
174
- `Unsupported JWT algorithm "${algorithm}". Tina4 signs with ` +
175
- `${SUPPORTED_ALGORITHMS.join(", ")} (HMAC via node:crypto; RS256 needs a PEM key pair). ` +
176
- `Set TINA4_JWT_ALGORITHM to one of those.`,
276
+ `Unsupported JWT algorithm "${algorithm}". Tina4 knows ${KNOWN_ALGORITHMS.join(", ")} ` +
277
+ `(HMAC via node:crypto; RS256 needs a PEM key pair); available in this runtime: ` +
278
+ `${availableAlgorithms().join(", ") || "(none)"}. ` +
279
+ `Set TINA4_JWT_ALGORITHM to one of the available ones.`,
280
+ );
281
+ }
282
+
283
+ /**
284
+ * The loud, actionable failure for an algorithm Tina4 knows but this RUNTIME
285
+ * cannot provide.
286
+ *
287
+ * Never a silent fallback to HMAC and never a bare `false`: the message names
288
+ * WHAT is missing and HOW to get it. The sentence skeleton is identical in
289
+ * tina4-python / tina4-php / tina4-ruby — only `reason` and the remedy differ —
290
+ * so "RS256 is not available" is one recognisable experience in all four.
291
+ */
292
+ function unavailableAlgorithmError(algorithm: string, reason: string): Error {
293
+ const remedy =
294
+ ALGORITHM_REMEDY.get(algorithm) ??
295
+ `${algorithm} comes from builtin node:crypto, so no package installs it — this ` +
296
+ `build's crypto provider refused it`;
297
+ return new Error(
298
+ `JWT algorithm "${algorithm}" is not available in this runtime: ${reason}. ${remedy}. ` +
299
+ `Available right now: ${availableAlgorithms().join(", ") || "(none)"}. ` +
300
+ `HMAC (HS256/HS384/HS512) is the Tina4 standard in all four frameworks and ` +
301
+ `needs no extra dependency.`,
177
302
  );
178
303
  }
179
304
 
180
305
  /**
181
306
  * Pick the JWT algorithm: explicit argument, else TINA4_JWT_ALGORITHM, else HS256.
182
307
  *
183
- * Throws (naming the supported set and the env var) when asked for an algorithm
184
- * we cannot sign a silent downgrade to HS256 is the whole bug in python#106.
308
+ * Throws when asked for an algorithm Tina4 does not know (naming the known set,
309
+ * what is available here, and the env var), and throws again with the runtime's
310
+ * own reason and a remedy — when it knows the algorithm but this build cannot
311
+ * provide it. A silent downgrade to HS256 is the whole bug in python#106, and a
312
+ * silent downgrade from RS256 would be worse: it would quietly turn asymmetric
313
+ * verification into a shared secret.
185
314
  *
186
315
  * @param algorithm - Explicit algorithm; wins over the environment when given.
187
316
  */
188
317
  export function resolveAlgorithm(algorithm?: string): string {
189
318
  const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
190
- if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
319
+ if (!KNOWN_ALGORITHMS.includes(chosen)) {
191
320
  throw unsupportedAlgorithmError(chosen);
192
321
  }
322
+ const failure = capabilityFailure(chosen);
323
+ if (failure !== null) {
324
+ throw unavailableAlgorithmError(chosen, failure);
325
+ }
193
326
  return chosen;
194
327
  }
195
328
 
@@ -212,8 +345,11 @@ function base64urlDecode(str: string): Buffer {
212
345
  * Create a signed JWT token.
213
346
  *
214
347
  * Secret is always read from `process.env.TINA4_SECRET`.
215
- * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256");
216
- * HS256 / HS384 / HS512 / RS256 are supported and anything else throws.
348
+ * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
349
+ * HS256 / HS384 / HS512 is the cross-framework standard; RS256 is an opt-in
350
+ * extra that Node provides from builtin node:crypto (pass the PEM private key
351
+ * as the secret). An unknown algorithm, or one this runtime cannot provide,
352
+ * throws — see `resolveAlgorithm`.
217
353
  *
218
354
  * The header's `alg` is always the algorithm that actually signed the token.
219
355
  *
@@ -318,21 +454,33 @@ export function validToken(token: string, secret?: string, algorithm?: string):
318
454
 
319
455
  const payload = JSON.parse(base64urlDecode(p).toString()) as Record<string, unknown>;
320
456
 
321
- const now = Date.now() / 1000;
322
- if (typeof payload.exp === "number" && now > payload.exp) {
323
- return null;
457
+ // Integer seconds, so all four frameworks compare the SAME number. PHP/Ruby
458
+ // read an integer clock and Python/Node a float one; with a float clock
459
+ // `now > exp` and `now >= exp` differ only on an exactly integral instant,
460
+ // so truncating here is what makes the boundary identical everywhere rather
461
+ // than merely close.
462
+ const now = Math.floor(Date.now() / 1000);
463
+
464
+ // RFC 7519 s4.1.4: "The processing of the 'exp' claim requires that the
465
+ // current date/time MUST be before the expiration date/time". now == exp is
466
+ // therefore ALREADY expired, so the test is >=. A PRESENT but malformed exp
467
+ // is rejected: the old `typeof payload.exp === "number"` SKIPPED the whole
468
+ // check for `exp: "abc"` / `exp: null`, so a malformed claim read as a token
469
+ // that never expires. No exp key at all stays unconstrained (non-breaking).
470
+ if (Object.hasOwn(payload, "exp")) {
471
+ const expires = numericDate(payload.exp);
472
+ if (expires === null || now >= expires) return null;
324
473
  }
325
474
 
326
475
  // "nbf" (not-before): a post-dated token is not valid YET. Was honoured only
327
476
  // by Ruby, so Python/PHP/Node accepted tokens their issuer had explicitly
328
- // marked as not-yet-usable (nodejs#39 / python#107). A token with no nbf is
329
- // unaffected that is what keeps this non-breaking. A PRESENT but
330
- // non-numeric nbf is rejected (Python raises a TypeError there and returns
331
- // None; a malformed not-before must never read as "no constraint").
477
+ // marked as not-yet-usable (nodejs#39 / python#107). Tolerates
478
+ // JWT_LEEWAY_SECONDS of clock skew, which RFC 7519 s4.1.5 permits. Same
479
+ // malformed-is-rejected rule as exp, through the same helper so the two
480
+ // claims can never drift apart.
332
481
  if (Object.hasOwn(payload, "nbf")) {
333
- const notBefore = payload.nbf;
334
- if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
335
- if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
482
+ const notBefore = numericDate(payload.nbf);
483
+ if (notBefore === null || now + JWT_LEEWAY_SECONDS < notBefore) return null;
336
484
  }
337
485
 
338
486
  return payload;
@@ -504,7 +652,8 @@ export function refreshToken(
504
652
  *
505
653
  * @param headers - Object with header keys (e.g. `{ authorization: "Bearer ..." }`)
506
654
  * @param secret - HMAC secret or PEM public key
507
- * @param algorithm - "HS256" or "RS256" (default "HS256")
655
+ * @param algorithm - Omit it to honour TINA4_JWT_ALGORITHM (then HS256). HS256 /
656
+ * HS384 / HS512 everywhere; RS256 where the runtime provides it (it does here).
508
657
  * @returns Decoded payload, or null if missing/invalid
509
658
  */
510
659
  export function authenticateRequest(
@@ -573,6 +722,9 @@ export function validateApiKey(
573
722
  export class Auth {
574
723
  static getToken = getToken;
575
724
  static validToken = validToken;
725
+ static resolveAlgorithm = resolveAlgorithm;
726
+ static algorithmAvailable = algorithmAvailable;
727
+ static availableAlgorithms = availableAlgorithms;
576
728
  static getPayload = getPayload;
577
729
  static hashPassword = hashPassword;
578
730
  static checkPassword = checkPassword;
@@ -27,21 +27,23 @@ interface BackgroundTask {
27
27
  }
28
28
 
29
29
  const _tasks: BackgroundTask[] = [];
30
- let _signalsBound = false;
31
30
 
32
- /**
33
- * Register signal handlers exactly once so SIGTERM/SIGINT during a long-running
34
- * process clears all background timers before the runtime exits. The handler
35
- * is additive it does not call `process.exit()` or interfere with other
36
- * shutdown logic registered by the CLI or user code.
37
- */
38
- function _bindSignalsOnce(): void {
39
- if (_signalsBound) return;
40
- _signalsBound = true;
41
- const cleanup = () => stopAllBackgroundTasks();
42
- process.on("SIGTERM", cleanup);
43
- process.on("SIGINT", cleanup);
44
- }
31
+ // This module deliberately installs NO signal handlers.
32
+ //
33
+ // It used to bind `process.on("SIGTERM"/"SIGINT", stopAllBackgroundTasks)`,
34
+ // described as "additive - it does not call process.exit()". That description
35
+ // was the bug. Registering ANY listener for SIGTERM REPLACES Node's default
36
+ // disposition, so a handler that does not exit does not "add" to the default,
37
+ // it CANCELS it: measured against a real signal, a server with one registered
38
+ // background task ignored SIGTERM entirely and ran forever, still answering
39
+ // 200s, until SIGKILL. Under Kubernetes that burns the whole
40
+ // terminationGracePeriodSeconds on every rolling deploy.
41
+ //
42
+ // It also bought nothing: `_arm()` unrefs every timer, so a background task
43
+ // never holds the event loop open and never needed clearing to let the process
44
+ // exit. The server's own graceful shutdown (server.ts) calls
45
+ // stopAllBackgroundTasks() as its first step, and a process using background()
46
+ // without a server keeps Node's correct default (terminate on SIGTERM).
45
47
 
46
48
  /**
47
49
  * Register a callback to run periodically alongside the HTTP server.
@@ -63,8 +65,6 @@ export function background(
63
65
  );
64
66
  }
65
67
 
66
- _bindSignalsOnce();
67
-
68
68
  const ms = Math.max(1, Math.round(intervalSeconds * 1000));
69
69
 
70
70
  // A task must NEVER overlap itself. setInterval fires on a fixed schedule and
@@ -109,9 +109,9 @@ export function background(
109
109
  }
110
110
 
111
111
  /**
112
- * Clear every registered background task. Called automatically on SIGTERM/SIGINT;
113
- * also called from the server's `close()` so a manual server shutdown stops
114
- * the timer wheel along with HTTP listeners.
112
+ * Clear every registered background task. Called by the server's graceful
113
+ * shutdown (its first step on SIGTERM/SIGINT) and by its `close()`, so both a
114
+ * signal and a manual shutdown stop the timer wheel along with the listeners.
115
115
  */
116
116
  export function stopAllBackgroundTasks(): void {
117
117
  while (_tasks.length > 0) {