tina4-nodejs 3.13.90 → 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.
@@ -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)) {
@@ -646,7 +646,12 @@ function extractFunctions(source: string, filePath: string, root: string = "."):
646
646
 
647
647
  // Extract function body by brace matching (on cleaned lines).
648
648
  const funcBody = extractFunctionBody(lines, i);
649
- const funcLoc = funcBody.split("\n").length;
649
+ // Code lines, by the exact same counter the file level uses. This was
650
+ // `funcBody.split("\n").length` - a raw line span - while file LOC excluded
651
+ // blanks and comments, so `loc` meant two different things in one payload
652
+ // and the dashboard sized bubbles in one unit while printing the function
653
+ // table in the other. Floor of 1: a one-line body must never report 0.
654
+ const funcLoc = Math.max(1, countLines(funcBody).loc);
650
655
  const complexity = cycloMaticComplexity(funcBody);
651
656
 
652
657
  // Parse args
@@ -675,6 +680,53 @@ function extractFunctions(source: string, filePath: string, root: string = "."):
675
680
  }
676
681
  }
677
682
 
683
+ return chargeNestedComplexityToTheNestedFunction(functions);
684
+ }
685
+
686
+ /**
687
+ * Stop a function being charged for the complexity of the functions nested
688
+ * inside it.
689
+ *
690
+ * Each function's raw score is measured over its whole span, so a branch inside
691
+ * a nested function landed on BOTH that function and every function enclosing
692
+ * it. The over-count compounded with depth: an IIFE wrapper or a registrar
693
+ * defining twenty inner handlers absorbed the entire file's complexity and
694
+ * topped the offenders list, hiding the genuine hot spots.
695
+ *
696
+ * The correction is exact. A raw score is 1 + every decision in the span, so
697
+ * (raw - 1) is the total decision count of a function's whole subtree.
698
+ * Subtracting that for each DIRECT child leaves the function's own branches:
699
+ *
700
+ * own(F) = raw(F) - sum over direct children C of (raw(C) - 1)
701
+ *
702
+ * Anything the extractor does NOT list is deliberately unaffected: nothing
703
+ * subtracts it, so its decisions stay with the function that contains it -
704
+ * moved, never lost.
705
+ */
706
+ export function chargeNestedComplexityToTheNestedFunction(
707
+ functions: FunctionInfo[],
708
+ ): FunctionInfo[] {
709
+ if (functions.length < 2) return functions;
710
+
711
+ const lastLine = (f: FunctionInfo) => f.line + Math.max(1, f.loc) - 1;
712
+ const contains = (outer: FunctionInfo, inner: FunctionInfo) =>
713
+ inner.line > outer.line && lastLine(inner) <= lastLine(outer);
714
+
715
+ const raw = functions.map((f) => f.complexity);
716
+ functions.forEach((outer, i) => {
717
+ let subtract = 0;
718
+ functions.forEach((inner, j) => {
719
+ if (i === j || !contains(outer, inner)) return;
720
+ // Direct child only: skip it if another function sits between the two,
721
+ // or its complexity would be subtracted twice.
722
+ const nestedDeeper = functions.some(
723
+ (mid, k) => k !== i && k !== j && contains(outer, mid) && contains(mid, inner),
724
+ );
725
+ if (!nestedDeeper) subtract += raw[j] - 1;
726
+ });
727
+ outer.complexity = Math.max(1, raw[i] - subtract);
728
+ });
729
+
678
730
  return functions;
679
731
  }
680
732
 
@@ -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;
@@ -8573,7 +8607,7 @@ function extractFunctions(source, filePath, root = ".") {
8573
8607
  if (funcName !== null && !NON_FUNCTION_WORDS.has(funcName)) {
8574
8608
  const displayName = funcName === "constructor" && currentClass ? `${currentClass}.constructor` : currentClass !== null && !isTopLevelDecl ? `${currentClass}.${funcName}` : funcName;
8575
8609
  const funcBody = extractFunctionBody(lines, i);
8576
- const funcLoc = funcBody.split("\n").length;
8610
+ const funcLoc = Math.max(1, countLines(funcBody).loc);
8577
8611
  const complexity = cycloMaticComplexity(funcBody);
8578
8612
  const args = argsStr.split(",").map((a) => a.trim().split(":")[0].split("=")[0].replace("?", "").trim()).filter((a) => a && a !== "this");
8579
8613
  functions.push({
@@ -8589,6 +8623,24 @@ function extractFunctions(source, filePath, root = ".") {
8589
8623
  currentClass = null;
8590
8624
  }
8591
8625
  }
8626
+ return chargeNestedComplexityToTheNestedFunction(functions);
8627
+ }
8628
+ function chargeNestedComplexityToTheNestedFunction(functions) {
8629
+ if (functions.length < 2) return functions;
8630
+ const lastLine = (f) => f.line + Math.max(1, f.loc) - 1;
8631
+ const contains = (outer, inner) => inner.line > outer.line && lastLine(inner) <= lastLine(outer);
8632
+ const raw = functions.map((f) => f.complexity);
8633
+ functions.forEach((outer, i) => {
8634
+ let subtract = 0;
8635
+ functions.forEach((inner, j) => {
8636
+ if (i === j || !contains(outer, inner)) return;
8637
+ const nestedDeeper = functions.some(
8638
+ (mid, k) => k !== i && k !== j && contains(outer, mid) && contains(mid, inner)
8639
+ );
8640
+ if (!nestedDeeper) subtract += raw[j] - 1;
8641
+ });
8642
+ outer.complexity = Math.max(1, raw[i] - subtract);
8643
+ });
8592
8644
  return functions;
8593
8645
  }
8594
8646
  function extractFunctionBody(lines, startLine) {