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.
- package/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +72 -20
- package/packages/core/dist/index.js +72 -20
- package/packages/core/public/js/tina4-dev-admin.min.js +179 -179
- package/packages/core/src/auth.ts +141 -21
- package/packages/core/src/metrics.ts +53 -1
- package/packages/orm/dist/index.js +72 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
258
|
-
|
|
366
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
367
|
+
if (rsaAlgorithm) {
|
|
368
|
+
const signer = createSign(rsaAlgorithm);
|
|
259
369
|
signer.update(input);
|
|
260
|
-
|
|
261
|
-
return base64urlEncode(sig);
|
|
370
|
+
return base64urlEncode(signer.sign(secret));
|
|
262
371
|
}
|
|
263
|
-
throw
|
|
372
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
264
373
|
}
|
|
265
374
|
|
|
266
375
|
function verifySignature(input: string, sig: string, secret: string, algorithm: string): boolean {
|
|
267
|
-
if (algorithm
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
407
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2897
|
-
|
|
2898
|
-
return base64urlEncode(
|
|
2918
|
+
const digest = HMAC_DIGESTS.get(algorithm);
|
|
2919
|
+
if (digest) {
|
|
2920
|
+
return base64urlEncode(createHmac(digest, secret).update(input).digest());
|
|
2899
2921
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2922
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
2923
|
+
if (rsaAlgorithm) {
|
|
2924
|
+
const signer = createSign(rsaAlgorithm);
|
|
2902
2925
|
signer.update(input);
|
|
2903
|
-
|
|
2904
|
-
return base64urlEncode(sig);
|
|
2926
|
+
return base64urlEncode(signer.sign(secret));
|
|
2905
2927
|
}
|
|
2906
|
-
throw
|
|
2928
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
2907
2929
|
}
|
|
2908
2930
|
function verifySignature(input, sig, secret, algorithm) {
|
|
2909
|
-
if (algorithm
|
|
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
|
-
|
|
2917
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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) {
|