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 +3 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +53 -19
- package/packages/core/dist/index.js +53 -19
- package/packages/core/src/auth.ts +141 -21
- package/packages/orm/dist/index.js +53 -19
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
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.
|
|
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
|
-
- **
|
|
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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12262
|
-
|
|
12263
|
-
return base64urlEncode(
|
|
12283
|
+
const digest = HMAC_DIGESTS.get(algorithm);
|
|
12284
|
+
if (digest) {
|
|
12285
|
+
return base64urlEncode(createHmac2(digest, secret).update(input).digest());
|
|
12264
12286
|
}
|
|
12265
|
-
|
|
12266
|
-
|
|
12287
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
12288
|
+
if (rsaAlgorithm) {
|
|
12289
|
+
const signer = createSign(rsaAlgorithm);
|
|
12267
12290
|
signer.update(input);
|
|
12268
|
-
|
|
12269
|
-
return base64urlEncode(sig);
|
|
12291
|
+
return base64urlEncode(signer.sign(secret));
|
|
12270
12292
|
}
|
|
12271
|
-
throw
|
|
12293
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
12272
12294
|
}
|
|
12273
12295
|
function verifySignature(input, sig, secret, algorithm) {
|
|
12274
|
-
if (algorithm
|
|
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
|
-
|
|
12282
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12261
|
-
|
|
12262
|
-
return base64urlEncode(
|
|
12282
|
+
const digest = HMAC_DIGESTS.get(algorithm);
|
|
12283
|
+
if (digest) {
|
|
12284
|
+
return base64urlEncode(createHmac2(digest, secret).update(input).digest());
|
|
12263
12285
|
}
|
|
12264
|
-
|
|
12265
|
-
|
|
12286
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
12287
|
+
if (rsaAlgorithm) {
|
|
12288
|
+
const signer = createSign(rsaAlgorithm);
|
|
12266
12289
|
signer.update(input);
|
|
12267
|
-
|
|
12268
|
-
return base64urlEncode(sig);
|
|
12290
|
+
return base64urlEncode(signer.sign(secret));
|
|
12269
12291
|
}
|
|
12270
|
-
throw
|
|
12292
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
12271
12293
|
}
|
|
12272
12294
|
function verifySignature(input, sig, secret, algorithm) {
|
|
12273
|
-
if (algorithm
|
|
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
|
-
|
|
12281
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)) {
|
|
@@ -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;
|