tina4-nodejs 3.13.91 → 3.13.92

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.
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.91)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.92)
2
2
 
3
3
  > This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
4
4
 
5
5
  ## What This Project Is
6
6
 
7
- Tina4 for Node.js/TypeScript v3.13.91 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.92 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
8
8
 
9
9
  The philosophy: zero ceremony, batteries included, file system as source of truth.
10
10
 
@@ -1244,7 +1244,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
1244
1244
  ## v3 Features Summary
1245
1245
 
1246
1246
  - **98 built-in features**, zero third-party dependencies
1247
- - **5,932 tests** passing across 190 files (build + typecheck green)
1247
+ - **6,005 tests** passing across 191 files (build + typecheck green)
1248
1248
  - **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
1249
1249
  - **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
1250
1250
  - **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.91",
3
+ "version": "3.13.92",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
6
6
  "keywords": [
@@ -12122,6 +12122,7 @@ var init_logger = __esm({
12122
12122
  var auth_exports = {};
12123
12123
  __export(auth_exports, {
12124
12124
  Auth: () => Auth,
12125
+ JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
12125
12126
  authMiddleware: () => authMiddleware,
12126
12127
  authenticateRequest: () => authenticateRequest,
12127
12128
  checkPassword: () => checkPassword,
@@ -12130,6 +12131,7 @@ __export(auth_exports, {
12130
12131
  getToken: () => getToken,
12131
12132
  hashPassword: () => hashPassword,
12132
12133
  refreshToken: () => refreshToken,
12134
+ resolveAlgorithm: () => resolveAlgorithm,
12133
12135
  validToken: () => validToken,
12134
12136
  validateApiKey: () => validateApiKey
12135
12137
  });
@@ -12190,6 +12192,18 @@ function ensureDevSecret(cwd) {
12190
12192
  }
12191
12193
  return newSecret;
12192
12194
  }
12195
+ function unsupportedAlgorithmError(algorithm) {
12196
+ return new Error(
12197
+ `Unsupported JWT algorithm "${algorithm}". Tina4 signs with ${SUPPORTED_ALGORITHMS.join(", ")} (HMAC via node:crypto; RS256 needs a PEM key pair). Set TINA4_JWT_ALGORITHM to one of those.`
12198
+ );
12199
+ }
12200
+ function resolveAlgorithm(algorithm) {
12201
+ const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
12202
+ if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
12203
+ throw unsupportedAlgorithmError(chosen);
12204
+ }
12205
+ return chosen;
12206
+ }
12193
12207
  function base64urlEncode(data) {
12194
12208
  return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
12195
12209
  }
@@ -12212,7 +12226,7 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
12212
12226
  if (!resolvedSecret) {
12213
12227
  _warnBlankSecret();
12214
12228
  }
12215
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
12229
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
12216
12230
  const header = { alg: resolvedAlgorithm, typ: "JWT" };
12217
12231
  const now = Math.floor(Date.now() / 1e3);
12218
12232
  const claims = { ...payload, iat: now };
@@ -12230,19 +12244,27 @@ function validToken(token, secret, algorithm) {
12230
12244
  if (!resolvedSecret) {
12231
12245
  _warnBlankSecret();
12232
12246
  }
12233
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
12247
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
12234
12248
  try {
12235
12249
  const parts = token.split(".");
12236
12250
  if (parts.length !== 3) return null;
12237
12251
  const [h, p, sig] = parts;
12252
+ const header = JSON.parse(base64urlDecode(h).toString());
12253
+ if (header.alg !== resolvedAlgorithm) return null;
12238
12254
  const signingInput = `${h}.${p}`;
12239
12255
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
12240
12256
  return null;
12241
12257
  }
12242
12258
  const payload = JSON.parse(base64urlDecode(p).toString());
12243
- if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp) {
12259
+ const now = Date.now() / 1e3;
12260
+ if (typeof payload.exp === "number" && now > payload.exp) {
12244
12261
  return null;
12245
12262
  }
12263
+ if (Object.hasOwn(payload, "nbf")) {
12264
+ const notBefore = payload.nbf;
12265
+ if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
12266
+ if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
12267
+ }
12246
12268
  return payload;
12247
12269
  } catch {
12248
12270
  return null;
@@ -12258,32 +12280,33 @@ function getPayload(token) {
12258
12280
  }
12259
12281
  }
12260
12282
  function sign(input, secret, algorithm) {
12261
- if (algorithm === "HS256") {
12262
- const sig = createHmac2("sha256", secret).update(input).digest();
12263
- return base64urlEncode(sig);
12283
+ const digest = HMAC_DIGESTS.get(algorithm);
12284
+ if (digest) {
12285
+ return base64urlEncode(createHmac2(digest, secret).update(input).digest());
12264
12286
  }
12265
- if (algorithm === "RS256") {
12266
- const signer = createSign("RSA-SHA256");
12287
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
12288
+ if (rsaAlgorithm) {
12289
+ const signer = createSign(rsaAlgorithm);
12267
12290
  signer.update(input);
12268
- const sig = signer.sign(secret);
12269
- return base64urlEncode(sig);
12291
+ return base64urlEncode(signer.sign(secret));
12270
12292
  }
12271
- throw new Error(`Unsupported algorithm: ${algorithm}`);
12293
+ throw unsupportedAlgorithmError(algorithm);
12272
12294
  }
12273
12295
  function verifySignature(input, sig, secret, algorithm) {
12274
- if (algorithm === "HS256") {
12296
+ if (HMAC_DIGESTS.has(algorithm)) {
12275
12297
  const expected = sign(input, secret, algorithm);
12276
12298
  const a = Buffer.from(sig);
12277
12299
  const b = Buffer.from(expected);
12278
12300
  if (a.length !== b.length) return false;
12279
12301
  return timingSafeEqual(a, b);
12280
12302
  }
12281
- if (algorithm === "RS256") {
12282
- const verifier = createVerify("RSA-SHA256");
12303
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
12304
+ if (rsaAlgorithm) {
12305
+ const verifier = createVerify(rsaAlgorithm);
12283
12306
  verifier.update(input);
12284
12307
  return verifier.verify(secret, base64urlDecode(sig));
12285
12308
  }
12286
- throw new Error(`Unsupported algorithm: ${algorithm}`);
12309
+ throw unsupportedAlgorithmError(algorithm);
12287
12310
  }
12288
12311
  function hashPassword(password, salt, iterations = 26e4) {
12289
12312
  const actualSalt = salt ?? randomBytes3(16).toString("hex");
@@ -12308,7 +12331,7 @@ function checkPassword(password, hash) {
12308
12331
  return false;
12309
12332
  }
12310
12333
  }
12311
- function authMiddleware(secret, algorithm = "HS256") {
12334
+ function authMiddleware(secret, algorithm) {
12312
12335
  return (req2, res, next) => {
12313
12336
  const authHeader = req2.headers.authorization ?? "";
12314
12337
  if (!authHeader.startsWith("Bearer ")) {
@@ -12331,11 +12354,11 @@ function refreshToken(token, expiresIn = 60) {
12331
12354
  const { iat: _iat, exp: _exp, ...claims } = payload;
12332
12355
  return getToken(claims, expiresIn);
12333
12356
  }
12334
- function authenticateRequest(headers, secret, algorithm = "HS256") {
12357
+ function authenticateRequest(headers, secret, algorithm) {
12335
12358
  const authHeader = headers.authorization ?? headers.Authorization ?? "";
12336
12359
  if (!authHeader.startsWith("Bearer ")) return null;
12337
12360
  const token = authHeader.slice(7);
12338
- if (validToken(token)) return getPayload(token);
12361
+ if (validToken(token, secret, algorithm)) return getPayload(token);
12339
12362
  if (validateApiKey(token)) {
12340
12363
  return { _auth: "api_key" };
12341
12364
  }
@@ -12349,12 +12372,23 @@ function validateApiKey(provided, expected) {
12349
12372
  if (a.length !== b.length) return false;
12350
12373
  return timingSafeEqual(a, b);
12351
12374
  }
12352
- var BLANK_SECRET_WARNING, Auth;
12375
+ var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, SUPPORTED_ALGORITHMS, JWT_LEEWAY_SECONDS, Auth;
12353
12376
  var init_auth = __esm({
12354
12377
  "../core/src/auth.ts"() {
12355
12378
  "use strict";
12356
12379
  init_dotenv();
12357
12380
  BLANK_SECRET_WARNING = "Auth: TINA4_SECRET is not set \u2014 JWT signing is insecure. Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic. For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated automatically into .env.local (gitignored). Seeing this warning means the run was NOT detected as dev \u2014 typically a container or CI without TINA4_DEBUG set, or TINA4_ENV=production.";
12381
+ HMAC_DIGESTS = /* @__PURE__ */ new Map([
12382
+ ["HS256", "sha256"],
12383
+ ["HS384", "sha384"],
12384
+ ["HS512", "sha512"]
12385
+ ]);
12386
+ RSA_SIGN_ALGORITHMS = /* @__PURE__ */ new Map([["RS256", "RSA-SHA256"]]);
12387
+ SUPPORTED_ALGORITHMS = [
12388
+ ...HMAC_DIGESTS.keys(),
12389
+ ...RSA_SIGN_ALGORITHMS.keys()
12390
+ ];
12391
+ JWT_LEEWAY_SECONDS = 60;
12358
12392
  Auth = class {
12359
12393
  static getToken = getToken;
12360
12394
  static validToken = validToken;
@@ -12121,6 +12121,7 @@ var init_logger = __esm({
12121
12121
  var auth_exports = {};
12122
12122
  __export(auth_exports, {
12123
12123
  Auth: () => Auth,
12124
+ JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
12124
12125
  authMiddleware: () => authMiddleware,
12125
12126
  authenticateRequest: () => authenticateRequest,
12126
12127
  checkPassword: () => checkPassword,
@@ -12129,6 +12130,7 @@ __export(auth_exports, {
12129
12130
  getToken: () => getToken,
12130
12131
  hashPassword: () => hashPassword,
12131
12132
  refreshToken: () => refreshToken,
12133
+ resolveAlgorithm: () => resolveAlgorithm,
12132
12134
  validToken: () => validToken,
12133
12135
  validateApiKey: () => validateApiKey
12134
12136
  });
@@ -12189,6 +12191,18 @@ function ensureDevSecret(cwd) {
12189
12191
  }
12190
12192
  return newSecret;
12191
12193
  }
12194
+ function unsupportedAlgorithmError(algorithm) {
12195
+ return new Error(
12196
+ `Unsupported JWT algorithm "${algorithm}". Tina4 signs with ${SUPPORTED_ALGORITHMS.join(", ")} (HMAC via node:crypto; RS256 needs a PEM key pair). Set TINA4_JWT_ALGORITHM to one of those.`
12197
+ );
12198
+ }
12199
+ function resolveAlgorithm(algorithm) {
12200
+ const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
12201
+ if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
12202
+ throw unsupportedAlgorithmError(chosen);
12203
+ }
12204
+ return chosen;
12205
+ }
12192
12206
  function base64urlEncode(data) {
12193
12207
  return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
12194
12208
  }
@@ -12211,7 +12225,7 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
12211
12225
  if (!resolvedSecret) {
12212
12226
  _warnBlankSecret();
12213
12227
  }
12214
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
12228
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
12215
12229
  const header = { alg: resolvedAlgorithm, typ: "JWT" };
12216
12230
  const now = Math.floor(Date.now() / 1e3);
12217
12231
  const claims = { ...payload, iat: now };
@@ -12229,19 +12243,27 @@ function validToken(token, secret, algorithm) {
12229
12243
  if (!resolvedSecret) {
12230
12244
  _warnBlankSecret();
12231
12245
  }
12232
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
12246
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
12233
12247
  try {
12234
12248
  const parts = token.split(".");
12235
12249
  if (parts.length !== 3) return null;
12236
12250
  const [h, p, sig] = parts;
12251
+ const header = JSON.parse(base64urlDecode(h).toString());
12252
+ if (header.alg !== resolvedAlgorithm) return null;
12237
12253
  const signingInput = `${h}.${p}`;
12238
12254
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
12239
12255
  return null;
12240
12256
  }
12241
12257
  const payload = JSON.parse(base64urlDecode(p).toString());
12242
- if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp) {
12258
+ const now = Date.now() / 1e3;
12259
+ if (typeof payload.exp === "number" && now > payload.exp) {
12243
12260
  return null;
12244
12261
  }
12262
+ if (Object.hasOwn(payload, "nbf")) {
12263
+ const notBefore = payload.nbf;
12264
+ if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
12265
+ if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
12266
+ }
12245
12267
  return payload;
12246
12268
  } catch {
12247
12269
  return null;
@@ -12257,32 +12279,33 @@ function getPayload(token) {
12257
12279
  }
12258
12280
  }
12259
12281
  function sign(input, secret, algorithm) {
12260
- if (algorithm === "HS256") {
12261
- const sig = createHmac2("sha256", secret).update(input).digest();
12262
- return base64urlEncode(sig);
12282
+ const digest = HMAC_DIGESTS.get(algorithm);
12283
+ if (digest) {
12284
+ return base64urlEncode(createHmac2(digest, secret).update(input).digest());
12263
12285
  }
12264
- if (algorithm === "RS256") {
12265
- const signer = createSign("RSA-SHA256");
12286
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
12287
+ if (rsaAlgorithm) {
12288
+ const signer = createSign(rsaAlgorithm);
12266
12289
  signer.update(input);
12267
- const sig = signer.sign(secret);
12268
- return base64urlEncode(sig);
12290
+ return base64urlEncode(signer.sign(secret));
12269
12291
  }
12270
- throw new Error(`Unsupported algorithm: ${algorithm}`);
12292
+ throw unsupportedAlgorithmError(algorithm);
12271
12293
  }
12272
12294
  function verifySignature(input, sig, secret, algorithm) {
12273
- if (algorithm === "HS256") {
12295
+ if (HMAC_DIGESTS.has(algorithm)) {
12274
12296
  const expected = sign(input, secret, algorithm);
12275
12297
  const a = Buffer.from(sig);
12276
12298
  const b = Buffer.from(expected);
12277
12299
  if (a.length !== b.length) return false;
12278
12300
  return timingSafeEqual(a, b);
12279
12301
  }
12280
- if (algorithm === "RS256") {
12281
- const verifier = createVerify("RSA-SHA256");
12302
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
12303
+ if (rsaAlgorithm) {
12304
+ const verifier = createVerify(rsaAlgorithm);
12282
12305
  verifier.update(input);
12283
12306
  return verifier.verify(secret, base64urlDecode(sig));
12284
12307
  }
12285
- throw new Error(`Unsupported algorithm: ${algorithm}`);
12308
+ throw unsupportedAlgorithmError(algorithm);
12286
12309
  }
12287
12310
  function hashPassword(password, salt, iterations = 26e4) {
12288
12311
  const actualSalt = salt ?? randomBytes3(16).toString("hex");
@@ -12307,7 +12330,7 @@ function checkPassword(password, hash) {
12307
12330
  return false;
12308
12331
  }
12309
12332
  }
12310
- function authMiddleware(secret, algorithm = "HS256") {
12333
+ function authMiddleware(secret, algorithm) {
12311
12334
  return (req2, res, next) => {
12312
12335
  const authHeader = req2.headers.authorization ?? "";
12313
12336
  if (!authHeader.startsWith("Bearer ")) {
@@ -12330,11 +12353,11 @@ function refreshToken(token, expiresIn = 60) {
12330
12353
  const { iat: _iat, exp: _exp, ...claims } = payload;
12331
12354
  return getToken(claims, expiresIn);
12332
12355
  }
12333
- function authenticateRequest(headers, secret, algorithm = "HS256") {
12356
+ function authenticateRequest(headers, secret, algorithm) {
12334
12357
  const authHeader = headers.authorization ?? headers.Authorization ?? "";
12335
12358
  if (!authHeader.startsWith("Bearer ")) return null;
12336
12359
  const token = authHeader.slice(7);
12337
- if (validToken(token)) return getPayload(token);
12360
+ if (validToken(token, secret, algorithm)) return getPayload(token);
12338
12361
  if (validateApiKey(token)) {
12339
12362
  return { _auth: "api_key" };
12340
12363
  }
@@ -12348,12 +12371,23 @@ function validateApiKey(provided, expected) {
12348
12371
  if (a.length !== b.length) return false;
12349
12372
  return timingSafeEqual(a, b);
12350
12373
  }
12351
- var BLANK_SECRET_WARNING, Auth;
12374
+ var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, SUPPORTED_ALGORITHMS, JWT_LEEWAY_SECONDS, Auth;
12352
12375
  var init_auth = __esm({
12353
12376
  "src/auth.ts"() {
12354
12377
  "use strict";
12355
12378
  init_dotenv();
12356
12379
  BLANK_SECRET_WARNING = "Auth: TINA4_SECRET is not set \u2014 JWT signing is insecure. Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic. For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated automatically into .env.local (gitignored). Seeing this warning means the run was NOT detected as dev \u2014 typically a container or CI without TINA4_DEBUG set, or TINA4_ENV=production.";
12380
+ HMAC_DIGESTS = /* @__PURE__ */ new Map([
12381
+ ["HS256", "sha256"],
12382
+ ["HS384", "sha384"],
12383
+ ["HS512", "sha512"]
12384
+ ]);
12385
+ RSA_SIGN_ALGORITHMS = /* @__PURE__ */ new Map([["RS256", "RSA-SHA256"]]);
12386
+ SUPPORTED_ALGORITHMS = [
12387
+ ...HMAC_DIGESTS.keys(),
12388
+ ...RSA_SIGN_ALGORITHMS.keys()
12389
+ ];
12390
+ JWT_LEEWAY_SECONDS = 60;
12357
12391
  Auth = class {
12358
12392
  static getToken = getToken;
12359
12393
  static validToken = validToken;
@@ -127,6 +127,72 @@ export function ensureDevSecret(cwd?: string): string | null {
127
127
  return newSecret;
128
128
  }
129
129
 
130
+ // ── JWT algorithms ────────────────────────────────────────────────
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. */
140
+ const HMAC_DIGESTS = new Map<string, string>([
141
+ ["HS256", "sha256"],
142
+ ["HS384", "sha384"],
143
+ ["HS512", "sha512"],
144
+ ]);
145
+
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
+ */
154
+ const RSA_SIGN_ALGORITHMS = new Map<string, string>([["RS256", "RSA-SHA256"]]);
155
+
156
+ /** Every algorithm Tina4 for Node can sign and verify, in the order we advertise them. */
157
+ const SUPPORTED_ALGORITHMS: readonly string[] = [
158
+ ...HMAC_DIGESTS.keys(),
159
+ ...RSA_SIGN_ALGORITHMS.keys(),
160
+ ];
161
+
162
+ /**
163
+ * Seconds of clock skew tolerated on the "nbf" (not-before) claim.
164
+ *
165
+ * Without this, a token minted on one host and validated on another a second
166
+ * behind is rejected for no real reason; RFC 7519 explicitly allows "a small
167
+ * leeway". Same value as the Python master's `_JWT_LEEWAY_SECONDS`.
168
+ */
169
+ export const JWT_LEEWAY_SECONDS = 60;
170
+
171
+ /** The loud, actionable failure for an algorithm we cannot sign — names the supported set. */
172
+ function unsupportedAlgorithmError(algorithm: string): Error {
173
+ 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.`,
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Pick the JWT algorithm: explicit argument, else TINA4_JWT_ALGORITHM, else HS256.
182
+ *
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.
185
+ *
186
+ * @param algorithm - Explicit algorithm; wins over the environment when given.
187
+ */
188
+ export function resolveAlgorithm(algorithm?: string): string {
189
+ const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
190
+ if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
191
+ throw unsupportedAlgorithmError(chosen);
192
+ }
193
+ return chosen;
194
+ }
195
+
130
196
  // ── Base64url helpers (RFC 7515) ──────────────────────────────────
131
197
 
132
198
  function base64urlEncode(data: Buffer): string {
@@ -146,12 +212,20 @@ function base64urlDecode(str: string): Buffer {
146
212
  * Create a signed JWT token.
147
213
  *
148
214
  * Secret is always read from `process.env.TINA4_SECRET`.
149
- * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
215
+ * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256");
216
+ * HS256 / HS384 / HS512 / RS256 are supported and anything else throws.
217
+ *
218
+ * The header's `alg` is always the algorithm that actually signed the token.
219
+ *
220
+ * No `nbf` (not-before) claim is stamped — parity with Python and PHP. Pass your
221
+ * own `nbf` in the payload to post-date a token; `validToken` enforces it.
150
222
  *
151
223
  * @param payload - Claims to encode (e.g. `{ userId: 1, role: "admin" }`)
152
224
  * @param secretOrExpiresIn - Signing secret string, OR expiresIn number in MINUTES (back-compat with old 2-arg form)
153
225
  * @param expiresIn - Lifetime in MINUTES (default 60). `0` ⇒ no `exp` claim (non-expiring). Only used when secret is a string.
226
+ * @param algorithm - Overrides TINA4_JWT_ALGORITHM for this call.
154
227
  * @returns Signed JWT string: header.payload.signature
228
+ * @throws When the resolved algorithm is not one Tina4 can sign.
155
229
  */
156
230
  export function getToken(
157
231
  payload: Record<string, unknown>,
@@ -173,7 +247,8 @@ export function getToken(
173
247
  if (!resolvedSecret) {
174
248
  _warnBlankSecret();
175
249
  }
176
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
250
+ // Throws on an algorithm we cannot sign — never silently downgrades to HS256.
251
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
177
252
 
178
253
  const header = { alg: resolvedAlgorithm, typ: "JWT" };
179
254
  const now = Math.floor(Date.now() / 1000);
@@ -204,30 +279,62 @@ export function getToken(
204
279
  *
205
280
  * Secret is read from `process.env.TINA4_SECRET` when not passed explicitly.
206
281
  * Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
282
+ *
283
+ * Checks, in order: the header's `alg` must BE the expected algorithm (blocks alg
284
+ * substitution, including `alg: "none"`, before any signature work), then the
285
+ * signature, then `exp`, then `nbf` (with `JWT_LEEWAY_SECONDS` of clock skew).
207
286
  */
208
287
  export function validToken(token: string, secret?: string, algorithm?: string): Record<string, unknown> | null {
209
288
  const resolvedSecret = secret ?? process.env.TINA4_SECRET ?? "";
210
289
  if (!resolvedSecret) {
211
290
  _warnBlankSecret();
212
291
  }
213
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
292
+ // Resolved OUTSIDE the try so a bad TINA4_JWT_ALGORITHM throws rather than
293
+ // being swallowed into a null. A misconfigured algorithm is a deployment
294
+ // error, not a bad token: swallowing it turns one actionable message into a
295
+ // silent 401 on every request, which is far harder to diagnose. It also could
296
+ // not hide the fault anyway - getToken() already throws on the same value, so
297
+ // a typo surfaces at login regardless; swallowing here only made the two paths
298
+ // disagree. Python (master, raises in the constructor) and PHP both throw.
299
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
300
+
214
301
  try {
215
302
  const parts = token.split(".");
216
303
  if (parts.length !== 3) return null;
217
304
 
218
305
  const [h, p, sig] = parts;
219
- const signingInput = `${h}.${p}`;
220
306
 
307
+ // Pin the algorithm to OUR configured one instead of trusting the token's
308
+ // header. A token asking to be verified as anything else — "none", a weaker
309
+ // HMAC, or an RSA alg when we sign HMAC — is rejected BEFORE any signature
310
+ // work, which is what blocks alg substitution. Matches the Python master.
311
+ const header = JSON.parse(base64urlDecode(h).toString()) as Record<string, unknown>;
312
+ if (header.alg !== resolvedAlgorithm) return null;
313
+
314
+ const signingInput = `${h}.${p}`;
221
315
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
222
316
  return null;
223
317
  }
224
318
 
225
319
  const payload = JSON.parse(base64urlDecode(p).toString()) as Record<string, unknown>;
226
320
 
227
- if (typeof payload.exp === "number" && Date.now() / 1000 > payload.exp) {
321
+ const now = Date.now() / 1000;
322
+ if (typeof payload.exp === "number" && now > payload.exp) {
228
323
  return null;
229
324
  }
230
325
 
326
+ // "nbf" (not-before): a post-dated token is not valid YET. Was honoured only
327
+ // 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").
332
+ 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;
336
+ }
337
+
231
338
  return payload;
232
339
  } catch {
233
340
  return null;
@@ -250,21 +357,23 @@ export function getPayload(token: string): Record<string, unknown> | null {
250
357
  // ── Signing helpers ───────────────────────────────────────────────
251
358
 
252
359
  function sign(input: string, secret: string, algorithm: string): string {
253
- if (algorithm === "HS256") {
254
- const sig = createHmac("sha256", secret).update(input).digest();
255
- return base64urlEncode(sig);
360
+ // The digest comes from the configured algorithm, so the "alg" we advertise in
361
+ // the header is the one that actually produced this signature.
362
+ const digest = HMAC_DIGESTS.get(algorithm);
363
+ if (digest) {
364
+ return base64urlEncode(createHmac(digest, secret).update(input).digest());
256
365
  }
257
- if (algorithm === "RS256") {
258
- const signer = createSign("RSA-SHA256");
366
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
367
+ if (rsaAlgorithm) {
368
+ const signer = createSign(rsaAlgorithm);
259
369
  signer.update(input);
260
- const sig = signer.sign(secret);
261
- return base64urlEncode(sig);
370
+ return base64urlEncode(signer.sign(secret));
262
371
  }
263
- throw new Error(`Unsupported algorithm: ${algorithm}`);
372
+ throw unsupportedAlgorithmError(algorithm);
264
373
  }
265
374
 
266
375
  function verifySignature(input: string, sig: string, secret: string, algorithm: string): boolean {
267
- if (algorithm === "HS256") {
376
+ if (HMAC_DIGESTS.has(algorithm)) {
268
377
  const expected = sign(input, secret, algorithm);
269
378
  // Constant-time comparison
270
379
  const a = Buffer.from(sig);
@@ -272,12 +381,13 @@ function verifySignature(input: string, sig: string, secret: string, algorithm:
272
381
  if (a.length !== b.length) return false;
273
382
  return timingSafeEqual(a, b);
274
383
  }
275
- if (algorithm === "RS256") {
276
- const verifier = createVerify("RSA-SHA256");
384
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
385
+ if (rsaAlgorithm) {
386
+ const verifier = createVerify(rsaAlgorithm);
277
387
  verifier.update(input);
278
388
  return verifier.verify(secret, base64urlDecode(sig));
279
389
  }
280
- throw new Error(`Unsupported algorithm: ${algorithm}`);
390
+ throw unsupportedAlgorithmError(algorithm);
281
391
  }
282
392
 
283
393
  // ── Password Hashing (PBKDF2) ────────────────────────────────────
@@ -334,8 +444,14 @@ export function checkPassword(password: string, hash: string): boolean {
334
444
  * Auth middleware that extracts and verifies a Bearer JWT from the
335
445
  * Authorization header. On success, attaches the decoded payload to
336
446
  * `(request as any).auth`. On failure, sends a 401 JSON response.
447
+ *
448
+ * @param secret - Signing secret / PEM public key (default: TINA4_SECRET env var).
449
+ * @param algorithm - JWT algorithm. Omit it to honour TINA4_JWT_ALGORITHM (then
450
+ * HS256). It used to default to the literal "HS256", which SHADOWED the env
451
+ * var: an app on TINA4_JWT_ALGORITHM=HS512 minted HS512 tokens and this
452
+ * middleware verified them as HS256, rejecting every valid token.
337
453
  */
338
- export function authMiddleware(secret?: string, algorithm: string = "HS256"): Middleware {
454
+ export function authMiddleware(secret?: string, algorithm?: string): Middleware {
339
455
  return (req: Tina4Request, res: Tina4Response, next: () => void): void => {
340
456
  const authHeader = req.headers.authorization ?? "";
341
457
 
@@ -394,7 +510,7 @@ export function refreshToken(
394
510
  export function authenticateRequest(
395
511
  headers: Record<string, string | string[] | undefined>,
396
512
  secret?: string,
397
- algorithm: string = "HS256",
513
+ algorithm?: string,
398
514
  ): Record<string, unknown> | null {
399
515
  const authHeader =
400
516
  (headers.authorization ?? headers.Authorization ?? "") as string;
@@ -403,8 +519,12 @@ export function authenticateRequest(
403
519
 
404
520
  const token = authHeader.slice(7);
405
521
 
406
- // Try JWT first (secret/algorithm params kept for backward compat but validToken reads from env)
407
- if (validToken(token)) return getPayload(token);
522
+ // Both overrides are FORWARDED. They used to be accepted and dropped - the
523
+ // body called bare validToken(token), so a caller passing secret= or
524
+ // algorithm= silently got the env values instead, and `algorithm` defaulted to
525
+ // the literal "HS256" which additionally shadowed TINA4_JWT_ALGORITHM. Python,
526
+ // PHP and Ruby all honour these; Node was the last one that did not.
527
+ if (validToken(token, secret, algorithm)) return getPayload(token);
408
528
 
409
529
  // Fallback: treat Bearer value as API key
410
530
  if (validateApiKey(token)) {
@@ -2757,6 +2757,7 @@ var init_logger = __esm({
2757
2757
  var auth_exports = {};
2758
2758
  __export(auth_exports, {
2759
2759
  Auth: () => Auth,
2760
+ JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
2760
2761
  authMiddleware: () => authMiddleware,
2761
2762
  authenticateRequest: () => authenticateRequest,
2762
2763
  checkPassword: () => checkPassword,
@@ -2765,6 +2766,7 @@ __export(auth_exports, {
2765
2766
  getToken: () => getToken,
2766
2767
  hashPassword: () => hashPassword,
2767
2768
  refreshToken: () => refreshToken,
2769
+ resolveAlgorithm: () => resolveAlgorithm,
2768
2770
  validToken: () => validToken,
2769
2771
  validateApiKey: () => validateApiKey
2770
2772
  });
@@ -2825,6 +2827,18 @@ function ensureDevSecret(cwd) {
2825
2827
  }
2826
2828
  return newSecret;
2827
2829
  }
2830
+ function unsupportedAlgorithmError(algorithm) {
2831
+ return new Error(
2832
+ `Unsupported JWT algorithm "${algorithm}". Tina4 signs with ${SUPPORTED_ALGORITHMS.join(", ")} (HMAC via node:crypto; RS256 needs a PEM key pair). Set TINA4_JWT_ALGORITHM to one of those.`
2833
+ );
2834
+ }
2835
+ function resolveAlgorithm(algorithm) {
2836
+ const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
2837
+ if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
2838
+ throw unsupportedAlgorithmError(chosen);
2839
+ }
2840
+ return chosen;
2841
+ }
2828
2842
  function base64urlEncode(data) {
2829
2843
  return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2830
2844
  }
@@ -2847,7 +2861,7 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
2847
2861
  if (!resolvedSecret) {
2848
2862
  _warnBlankSecret();
2849
2863
  }
2850
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
2864
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
2851
2865
  const header = { alg: resolvedAlgorithm, typ: "JWT" };
2852
2866
  const now = Math.floor(Date.now() / 1e3);
2853
2867
  const claims = { ...payload, iat: now };
@@ -2865,19 +2879,27 @@ function validToken(token, secret, algorithm) {
2865
2879
  if (!resolvedSecret) {
2866
2880
  _warnBlankSecret();
2867
2881
  }
2868
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
2882
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
2869
2883
  try {
2870
2884
  const parts = token.split(".");
2871
2885
  if (parts.length !== 3) return null;
2872
2886
  const [h, p, sig] = parts;
2887
+ const header = JSON.parse(base64urlDecode(h).toString());
2888
+ if (header.alg !== resolvedAlgorithm) return null;
2873
2889
  const signingInput = `${h}.${p}`;
2874
2890
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
2875
2891
  return null;
2876
2892
  }
2877
2893
  const payload = JSON.parse(base64urlDecode(p).toString());
2878
- if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp) {
2894
+ const now = Date.now() / 1e3;
2895
+ if (typeof payload.exp === "number" && now > payload.exp) {
2879
2896
  return null;
2880
2897
  }
2898
+ if (Object.hasOwn(payload, "nbf")) {
2899
+ const notBefore = payload.nbf;
2900
+ if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
2901
+ if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
2902
+ }
2881
2903
  return payload;
2882
2904
  } catch {
2883
2905
  return null;
@@ -2893,32 +2915,33 @@ function getPayload(token) {
2893
2915
  }
2894
2916
  }
2895
2917
  function sign(input, secret, algorithm) {
2896
- if (algorithm === "HS256") {
2897
- const sig = createHmac("sha256", secret).update(input).digest();
2898
- return base64urlEncode(sig);
2918
+ const digest = HMAC_DIGESTS.get(algorithm);
2919
+ if (digest) {
2920
+ return base64urlEncode(createHmac(digest, secret).update(input).digest());
2899
2921
  }
2900
- if (algorithm === "RS256") {
2901
- const signer = createSign("RSA-SHA256");
2922
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
2923
+ if (rsaAlgorithm) {
2924
+ const signer = createSign(rsaAlgorithm);
2902
2925
  signer.update(input);
2903
- const sig = signer.sign(secret);
2904
- return base64urlEncode(sig);
2926
+ return base64urlEncode(signer.sign(secret));
2905
2927
  }
2906
- throw new Error(`Unsupported algorithm: ${algorithm}`);
2928
+ throw unsupportedAlgorithmError(algorithm);
2907
2929
  }
2908
2930
  function verifySignature(input, sig, secret, algorithm) {
2909
- if (algorithm === "HS256") {
2931
+ if (HMAC_DIGESTS.has(algorithm)) {
2910
2932
  const expected = sign(input, secret, algorithm);
2911
2933
  const a = Buffer.from(sig);
2912
2934
  const b = Buffer.from(expected);
2913
2935
  if (a.length !== b.length) return false;
2914
2936
  return timingSafeEqual(a, b);
2915
2937
  }
2916
- if (algorithm === "RS256") {
2917
- const verifier = createVerify("RSA-SHA256");
2938
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
2939
+ if (rsaAlgorithm) {
2940
+ const verifier = createVerify(rsaAlgorithm);
2918
2941
  verifier.update(input);
2919
2942
  return verifier.verify(secret, base64urlDecode(sig));
2920
2943
  }
2921
- throw new Error(`Unsupported algorithm: ${algorithm}`);
2944
+ throw unsupportedAlgorithmError(algorithm);
2922
2945
  }
2923
2946
  function hashPassword(password, salt, iterations = 26e4) {
2924
2947
  const actualSalt = salt ?? randomBytes(16).toString("hex");
@@ -2943,7 +2966,7 @@ function checkPassword(password, hash) {
2943
2966
  return false;
2944
2967
  }
2945
2968
  }
2946
- function authMiddleware(secret, algorithm = "HS256") {
2969
+ function authMiddleware(secret, algorithm) {
2947
2970
  return (req2, res, next) => {
2948
2971
  const authHeader = req2.headers.authorization ?? "";
2949
2972
  if (!authHeader.startsWith("Bearer ")) {
@@ -2966,11 +2989,11 @@ function refreshToken(token, expiresIn = 60) {
2966
2989
  const { iat: _iat, exp: _exp, ...claims } = payload;
2967
2990
  return getToken(claims, expiresIn);
2968
2991
  }
2969
- function authenticateRequest(headers, secret, algorithm = "HS256") {
2992
+ function authenticateRequest(headers, secret, algorithm) {
2970
2993
  const authHeader = headers.authorization ?? headers.Authorization ?? "";
2971
2994
  if (!authHeader.startsWith("Bearer ")) return null;
2972
2995
  const token = authHeader.slice(7);
2973
- if (validToken(token)) return getPayload(token);
2996
+ if (validToken(token, secret, algorithm)) return getPayload(token);
2974
2997
  if (validateApiKey(token)) {
2975
2998
  return { _auth: "api_key" };
2976
2999
  }
@@ -2984,12 +3007,23 @@ function validateApiKey(provided, expected) {
2984
3007
  if (a.length !== b.length) return false;
2985
3008
  return timingSafeEqual(a, b);
2986
3009
  }
2987
- var BLANK_SECRET_WARNING, Auth;
3010
+ var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, SUPPORTED_ALGORITHMS, JWT_LEEWAY_SECONDS, Auth;
2988
3011
  var init_auth = __esm({
2989
3012
  "../core/src/auth.ts"() {
2990
3013
  "use strict";
2991
3014
  init_dotenv();
2992
3015
  BLANK_SECRET_WARNING = "Auth: TINA4_SECRET is not set \u2014 JWT signing is insecure. Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic. For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated automatically into .env.local (gitignored). Seeing this warning means the run was NOT detected as dev \u2014 typically a container or CI without TINA4_DEBUG set, or TINA4_ENV=production.";
3016
+ HMAC_DIGESTS = /* @__PURE__ */ new Map([
3017
+ ["HS256", "sha256"],
3018
+ ["HS384", "sha384"],
3019
+ ["HS512", "sha512"]
3020
+ ]);
3021
+ RSA_SIGN_ALGORITHMS = /* @__PURE__ */ new Map([["RS256", "RSA-SHA256"]]);
3022
+ SUPPORTED_ALGORITHMS = [
3023
+ ...HMAC_DIGESTS.keys(),
3024
+ ...RSA_SIGN_ALGORITHMS.keys()
3025
+ ];
3026
+ JWT_LEEWAY_SECONDS = 60;
2993
3027
  Auth = class {
2994
3028
  static getToken = getToken;
2995
3029
  static validToken = validToken;