tina4-nodejs 3.13.138 → 3.13.139
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 +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +529 -70
- package/packages/core/dist/index.js +534 -70
- package/packages/core/src/api.ts +33 -4
- package/packages/core/src/auth.ts +73 -6
- package/packages/core/src/authGate.ts +27 -20
- package/packages/core/src/graphql.ts +92 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/job.ts +8 -1
- package/packages/core/src/mcp.ts +3 -1
- package/packages/core/src/middleware.ts +4 -2
- package/packages/core/src/push.ts +16 -0
- package/packages/core/src/queue.ts +15 -2
- package/packages/core/src/queueBackends/liteBackend.ts +15 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +40 -6
- package/packages/core/src/server.ts +6 -1
- package/packages/core/src/sessionHandlers/databaseHandler.ts +60 -3
- package/packages/core/src/sso.ts +12 -4
- package/packages/core/src/ssoIdentity.ts +19 -0
- package/packages/core/src/ssrf.ts +221 -0
- package/packages/core/src/testClient.ts +3 -0
- package/packages/core/src/websocket.ts +4 -2
- package/packages/frond/dist/index.js +6 -0
- package/packages/frond/src/engine.ts +9 -0
- package/packages/orm/dist/index.js +529 -70
- package/types/core/src/api.d.ts +6 -0
- package/types/core/src/auth.d.ts +30 -0
- package/types/core/src/graphql.d.ts +25 -0
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/job.d.ts +1 -0
- package/types/core/src/push.d.ts +5 -0
- package/types/core/src/queue.d.ts +7 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +8 -1
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
- package/types/core/src/sso.d.ts +1 -0
- package/types/core/src/ssoIdentity.d.ts +10 -0
- package/types/core/src/ssrf.d.ts +19 -0
|
@@ -2612,6 +2612,8 @@ var auth_exports = {};
|
|
|
2612
2612
|
__export(auth_exports, {
|
|
2613
2613
|
Auth: () => Auth,
|
|
2614
2614
|
JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
|
|
2615
|
+
MIN_SECRET_BYTES: () => MIN_SECRET_BYTES,
|
|
2616
|
+
NON_IDENTITY_TOKEN_TYPES: () => NON_IDENTITY_TOKEN_TYPES,
|
|
2615
2617
|
algorithmAvailable: () => algorithmAvailable,
|
|
2616
2618
|
authMiddleware: () => authMiddleware,
|
|
2617
2619
|
authenticateRequest: () => authenticateRequest,
|
|
@@ -2621,7 +2623,10 @@ __export(auth_exports, {
|
|
|
2621
2623
|
getPayload: () => getPayload,
|
|
2622
2624
|
getToken: () => getToken,
|
|
2623
2625
|
hashPassword: () => hashPassword,
|
|
2626
|
+
insecureSecretMessage: () => insecureSecretMessage,
|
|
2627
|
+
isIdentityPayload: () => isIdentityPayload,
|
|
2624
2628
|
refreshToken: () => refreshToken,
|
|
2629
|
+
requireBootSecret: () => requireBootSecret,
|
|
2625
2630
|
resolveAlgorithm: () => resolveAlgorithm,
|
|
2626
2631
|
validToken: () => validToken,
|
|
2627
2632
|
validateApiKey: () => validateApiKey
|
|
@@ -2657,6 +2662,23 @@ async function _logWarning(message) {
|
|
|
2657
2662
|
function _warnBlankSecret() {
|
|
2658
2663
|
void _logWarning(BLANK_SECRET_WARNING);
|
|
2659
2664
|
}
|
|
2665
|
+
function isIdentityPayload(payload) {
|
|
2666
|
+
return payload != null && !NON_IDENTITY_TOKEN_TYPES.includes(payload.type);
|
|
2667
|
+
}
|
|
2668
|
+
function insecureSecretMessage(secret) {
|
|
2669
|
+
const length = Buffer.byteLength(secret ?? "", "utf8");
|
|
2670
|
+
if (length >= MIN_SECRET_BYTES) return null;
|
|
2671
|
+
const what = length === 0 ? "is not set" : `is ${length} bytes`;
|
|
2672
|
+
return `Auth: TINA4_SECRET ${what}; an HMAC JWT secret must be at least ${MIN_SECRET_BYTES} bytes. Generate one with \`openssl rand -hex 32\` and set TINA4_SECRET in your environment or .env.`;
|
|
2673
|
+
}
|
|
2674
|
+
function requireBootSecret() {
|
|
2675
|
+
const algorithm = (process.env.TINA4_JWT_ALGORITHM ?? "").trim() || "HS256";
|
|
2676
|
+
if (RSA_SIGN_ALGORITHMS.has(algorithm)) return;
|
|
2677
|
+
const secret = process.env.TINA4_SECRET ?? "";
|
|
2678
|
+
if (secret === "" && _isDev()) return;
|
|
2679
|
+
const message = insecureSecretMessage(secret);
|
|
2680
|
+
if (message) throw new Error(message);
|
|
2681
|
+
}
|
|
2660
2682
|
function ensureDevSecret(cwd) {
|
|
2661
2683
|
if (process.env.TINA4_SECRET) return null;
|
|
2662
2684
|
if (!_isDev() || _isCi() || _isProduction()) {
|
|
@@ -2787,6 +2809,13 @@ function validToken(token, secret, algorithm) {
|
|
|
2787
2809
|
const [h, p, sig] = parts;
|
|
2788
2810
|
const header = JSON.parse(base64urlDecode(h).toString());
|
|
2789
2811
|
if (header.alg !== resolvedAlgorithm) return null;
|
|
2812
|
+
if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
|
|
2813
|
+
const weak = insecureSecretMessage(resolvedSecret);
|
|
2814
|
+
if (weak) {
|
|
2815
|
+
void _logWarning(weak);
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2790
2819
|
const signingInput = `${h}.${p}`;
|
|
2791
2820
|
if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
|
|
2792
2821
|
return null;
|
|
@@ -2818,6 +2847,8 @@ function getPayload(token) {
|
|
|
2818
2847
|
function sign(input, secret, algorithm) {
|
|
2819
2848
|
const digest = HMAC_DIGESTS.get(algorithm);
|
|
2820
2849
|
if (digest) {
|
|
2850
|
+
const weak = insecureSecretMessage(secret);
|
|
2851
|
+
if (weak) throw new Error(weak);
|
|
2821
2852
|
return base64urlEncode(createHmac(digest, secret).update(input).digest());
|
|
2822
2853
|
}
|
|
2823
2854
|
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
@@ -2875,17 +2906,17 @@ function authMiddleware(secret, algorithm) {
|
|
|
2875
2906
|
return;
|
|
2876
2907
|
}
|
|
2877
2908
|
const token = authHeader.slice(7);
|
|
2878
|
-
|
|
2909
|
+
const payload = validToken(token, secret, algorithm);
|
|
2910
|
+
if (!isIdentityPayload(payload)) {
|
|
2879
2911
|
res({ error: "Unauthorized" }, 401);
|
|
2880
2912
|
return;
|
|
2881
2913
|
}
|
|
2882
|
-
req2.auth =
|
|
2914
|
+
req2.auth = payload;
|
|
2883
2915
|
next();
|
|
2884
2916
|
};
|
|
2885
2917
|
}
|
|
2886
2918
|
function refreshToken(token, expiresIn = 60) {
|
|
2887
|
-
|
|
2888
|
-
const payload = getPayload(token);
|
|
2919
|
+
const payload = validToken(token);
|
|
2889
2920
|
if (!payload) return null;
|
|
2890
2921
|
const { iat: _iat, exp: _exp, ...claims } = payload;
|
|
2891
2922
|
return getToken(claims, expiresIn);
|
|
@@ -2894,7 +2925,8 @@ function authenticateRequest(headers, secret, algorithm) {
|
|
|
2894
2925
|
const authHeader = headers.authorization ?? headers.Authorization ?? "";
|
|
2895
2926
|
if (!authHeader.startsWith("Bearer ")) return null;
|
|
2896
2927
|
const token = authHeader.slice(7);
|
|
2897
|
-
|
|
2928
|
+
const payload = validToken(token, secret, algorithm);
|
|
2929
|
+
if (payload) return isIdentityPayload(payload) ? payload : null;
|
|
2898
2930
|
if (validateApiKey(token)) {
|
|
2899
2931
|
return { _auth: "api_key" };
|
|
2900
2932
|
}
|
|
@@ -2908,12 +2940,14 @@ function validateApiKey(provided, expected) {
|
|
|
2908
2940
|
if (a.length !== b.length) return false;
|
|
2909
2941
|
return timingSafeEqual(a, b);
|
|
2910
2942
|
}
|
|
2911
|
-
var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, KNOWN_ALGORITHMS, ALGORITHM_REMEDY, capabilityCache, JWT_LEEWAY_SECONDS, Auth;
|
|
2943
|
+
var BLANK_SECRET_WARNING, NON_IDENTITY_TOKEN_TYPES, MIN_SECRET_BYTES, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, KNOWN_ALGORITHMS, ALGORITHM_REMEDY, capabilityCache, JWT_LEEWAY_SECONDS, Auth;
|
|
2912
2944
|
var init_auth = __esm({
|
|
2913
2945
|
"../core/src/auth.ts"() {
|
|
2914
2946
|
"use strict";
|
|
2915
2947
|
init_dotenv();
|
|
2916
2948
|
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.";
|
|
2949
|
+
NON_IDENTITY_TOKEN_TYPES = ["form"];
|
|
2950
|
+
MIN_SECRET_BYTES = 32;
|
|
2917
2951
|
HMAC_DIGESTS = /* @__PURE__ */ new Map([
|
|
2918
2952
|
["HS256", "sha256"],
|
|
2919
2953
|
["HS384", "sha384"],
|
|
@@ -3296,9 +3330,9 @@ var init_cache = __esm({
|
|
|
3296
3330
|
return entry.value;
|
|
3297
3331
|
}
|
|
3298
3332
|
async set(key, value, ttl) {
|
|
3299
|
-
const
|
|
3333
|
+
const expiresAt2 = ttl > 0 ? Date.now() + ttl * 1e3 : 0;
|
|
3300
3334
|
this.store.delete(key);
|
|
3301
|
-
this.store.set(key, { value, expiresAt });
|
|
3335
|
+
this.store.set(key, { value, expiresAt: expiresAt2 });
|
|
3302
3336
|
while (this.store.size > this.maxEntries) {
|
|
3303
3337
|
const firstKey = this.store.keys().next().value;
|
|
3304
3338
|
if (firstKey !== void 0) this.store.delete(firstKey);
|
|
@@ -3731,8 +3765,8 @@ ${s}\r
|
|
|
3731
3765
|
const oldest = files.shift();
|
|
3732
3766
|
if (oldest) fs.unlinkSync(path.join(this.dir, oldest.name));
|
|
3733
3767
|
}
|
|
3734
|
-
const
|
|
3735
|
-
const entry = { key, value, expiresAt };
|
|
3768
|
+
const expiresAt2 = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
|
|
3769
|
+
const entry = { key, value, expiresAt: expiresAt2 };
|
|
3736
3770
|
fs.writeFileSync(this.keyPath(key), JSON.stringify(entry));
|
|
3737
3771
|
} catch {
|
|
3738
3772
|
}
|
|
@@ -5479,8 +5513,8 @@ function capCache(cache, maxEntries) {
|
|
|
5479
5513
|
}
|
|
5480
5514
|
function sweepExpiredCache(cache) {
|
|
5481
5515
|
const now = Date.now();
|
|
5482
|
-
for (const [key, [,
|
|
5483
|
-
if (
|
|
5516
|
+
for (const [key, [, expiresAt2]] of cache) {
|
|
5517
|
+
if (expiresAt2 <= now) cache.delete(key);
|
|
5484
5518
|
}
|
|
5485
5519
|
}
|
|
5486
5520
|
function tokenize(source) {
|
|
@@ -6452,6 +6486,12 @@ function setFormTokenSessionId(sessionId) {
|
|
|
6452
6486
|
}
|
|
6453
6487
|
function _buildFormTokenJwt(descriptor = "") {
|
|
6454
6488
|
const secret = process.env.TINA4_SECRET ?? "";
|
|
6489
|
+
const secretBytes = Buffer.byteLength(secret, "utf8");
|
|
6490
|
+
if (secretBytes < 32) {
|
|
6491
|
+
throw new Error(
|
|
6492
|
+
`Auth: TINA4_SECRET ${secretBytes === 0 ? "is not set" : `is ${secretBytes} bytes`}; an HMAC JWT secret must be at least 32 bytes. Generate one with \`openssl rand -hex 32\` and set TINA4_SECRET in your environment or .env.`
|
|
6493
|
+
);
|
|
6494
|
+
}
|
|
6455
6495
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
6456
6496
|
const header = { alg: "HS256", typ: "JWT" };
|
|
6457
6497
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -7993,8 +8033,8 @@ var init_engine = __esm({
|
|
|
7993
8033
|
sweepExpiredCache(this.fragmentCache);
|
|
7994
8034
|
const cached = this.fragmentCache.get(cacheKey);
|
|
7995
8035
|
if (cached) {
|
|
7996
|
-
const [htmlContent,
|
|
7997
|
-
if (Date.now() <
|
|
8036
|
+
const [htmlContent, expiresAt2] = cached;
|
|
8037
|
+
if (Date.now() < expiresAt2) {
|
|
7998
8038
|
let i2 = start2 + 1;
|
|
7999
8039
|
let depth = 0;
|
|
8000
8040
|
while (i2 < tokens.length) {
|
|
@@ -8787,7 +8827,7 @@ function csrfBearerIsValid(req2, secret) {
|
|
|
8787
8827
|
const authHeader = req2.headers.authorization ?? "";
|
|
8788
8828
|
if (!authHeader.startsWith("Bearer ")) return false;
|
|
8789
8829
|
const bearerToken = authHeader.slice(7).trim();
|
|
8790
|
-
return Boolean(bearerToken && validToken(bearerToken, secret));
|
|
8830
|
+
return Boolean(bearerToken && isIdentityPayload(validToken(bearerToken, secret)));
|
|
8791
8831
|
}
|
|
8792
8832
|
function csrfRequestToken(req2) {
|
|
8793
8833
|
const query = req2.query ?? {};
|
|
@@ -9924,6 +9964,22 @@ var init_transport = __esm({
|
|
|
9924
9964
|
}
|
|
9925
9965
|
});
|
|
9926
9966
|
|
|
9967
|
+
// ../core/src/ssoIdentity.ts
|
|
9968
|
+
function liveSessionIdentity(stored) {
|
|
9969
|
+
if (!stored || typeof stored !== "object") return null;
|
|
9970
|
+
const identity = stored.identity;
|
|
9971
|
+
if (!identity || typeof identity !== "object" || !identity.issuer || !identity.subject) return null;
|
|
9972
|
+
const expiresAt2 = stored.expires_at ?? 0;
|
|
9973
|
+
if (typeof expiresAt2 !== "number") return null;
|
|
9974
|
+
if (expiresAt2 > 0 && Math.floor(Date.now() / 1e3) >= expiresAt2) return null;
|
|
9975
|
+
return identity;
|
|
9976
|
+
}
|
|
9977
|
+
var init_ssoIdentity = __esm({
|
|
9978
|
+
"../core/src/ssoIdentity.ts"() {
|
|
9979
|
+
"use strict";
|
|
9980
|
+
}
|
|
9981
|
+
});
|
|
9982
|
+
|
|
9927
9983
|
// ../core/src/authGate.ts
|
|
9928
9984
|
function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
9929
9985
|
if (!(match.secure === true && match.noAuth !== true && !isDevAdmin)) {
|
|
@@ -9931,38 +9987,50 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
9931
9987
|
}
|
|
9932
9988
|
const authHeader = req2.headers.authorization ?? "";
|
|
9933
9989
|
const headerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
9934
|
-
let
|
|
9990
|
+
let payload = null;
|
|
9935
9991
|
let tokenSource = "";
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9992
|
+
let resolvedToken = "";
|
|
9993
|
+
if (headerToken) {
|
|
9994
|
+
const candidate = validToken(headerToken);
|
|
9995
|
+
if (isIdentityPayload(candidate)) {
|
|
9996
|
+
payload = candidate;
|
|
9997
|
+
tokenSource = "header";
|
|
9998
|
+
resolvedToken = headerToken;
|
|
9999
|
+
}
|
|
9939
10000
|
}
|
|
9940
|
-
if (!
|
|
10001
|
+
if (!payload) {
|
|
9941
10002
|
const bodyToken = req2.body?.formToken;
|
|
9942
|
-
if (bodyToken
|
|
9943
|
-
|
|
9944
|
-
|
|
10003
|
+
if (bodyToken) {
|
|
10004
|
+
const candidate = validToken(bodyToken);
|
|
10005
|
+
if (isIdentityPayload(candidate)) {
|
|
10006
|
+
payload = candidate;
|
|
10007
|
+
tokenSource = "body";
|
|
10008
|
+
resolvedToken = bodyToken;
|
|
10009
|
+
}
|
|
9945
10010
|
}
|
|
9946
10011
|
}
|
|
9947
|
-
if (!
|
|
9948
|
-
const
|
|
9949
|
-
|
|
9950
|
-
if (identity?.issuer && identity?.subject) {
|
|
10012
|
+
if (!payload) {
|
|
10013
|
+
const identity = liveSessionIdentity(req2.session?.get?.("_tina4_sso"));
|
|
10014
|
+
if (identity) {
|
|
9951
10015
|
req2.user = identity;
|
|
9952
10016
|
return rbacForbidden(match, identity, res);
|
|
9953
10017
|
}
|
|
9954
10018
|
const sessionToken = req2.session?.get?.("token");
|
|
9955
|
-
if (sessionToken
|
|
9956
|
-
|
|
9957
|
-
|
|
10019
|
+
if (sessionToken) {
|
|
10020
|
+
const candidate = validToken(sessionToken);
|
|
10021
|
+
if (isIdentityPayload(candidate)) {
|
|
10022
|
+
payload = candidate;
|
|
10023
|
+
tokenSource = "session";
|
|
10024
|
+
resolvedToken = sessionToken;
|
|
10025
|
+
}
|
|
9958
10026
|
}
|
|
9959
10027
|
}
|
|
9960
|
-
if (!
|
|
10028
|
+
if (!payload) {
|
|
9961
10029
|
res.raw.writeHead(401, { "Content-Type": "application/json" });
|
|
9962
10030
|
res.raw.end(JSON.stringify({ error: "Unauthorized" }));
|
|
9963
10031
|
return true;
|
|
9964
10032
|
}
|
|
9965
|
-
req2.user =
|
|
10033
|
+
req2.user = payload;
|
|
9966
10034
|
if (tokenSource === "body") {
|
|
9967
10035
|
const fresh = refreshToken(resolvedToken);
|
|
9968
10036
|
if (fresh) {
|
|
@@ -10012,6 +10080,7 @@ var init_authGate = __esm({
|
|
|
10012
10080
|
"../core/src/authGate.ts"() {
|
|
10013
10081
|
"use strict";
|
|
10014
10082
|
init_auth();
|
|
10083
|
+
init_ssoIdentity();
|
|
10015
10084
|
}
|
|
10016
10085
|
});
|
|
10017
10086
|
|
|
@@ -11085,8 +11154,8 @@ var init_mongoHandler = __esm({
|
|
|
11085
11154
|
if (!result || result === "__EMPTY__") return null;
|
|
11086
11155
|
try {
|
|
11087
11156
|
const doc = JSON.parse(result);
|
|
11088
|
-
const
|
|
11089
|
-
if (
|
|
11157
|
+
const expiresAt2 = Number(doc?.expires_at ?? 0);
|
|
11158
|
+
if (expiresAt2 > 0 && Date.now() / 1e3 > expiresAt2) {
|
|
11090
11159
|
this.destroy(sessionId);
|
|
11091
11160
|
return null;
|
|
11092
11161
|
}
|
|
@@ -11312,6 +11381,9 @@ parentPort.on("message", (message) => {
|
|
|
11312
11381
|
|
|
11313
11382
|
// ../core/src/sessionHandlers/databaseHandler.ts
|
|
11314
11383
|
import { DatabaseSync } from "node:sqlite";
|
|
11384
|
+
function sleepSync2(ms) {
|
|
11385
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
11386
|
+
}
|
|
11315
11387
|
function column(row, name) {
|
|
11316
11388
|
if (name in row) return row[name];
|
|
11317
11389
|
return row[name.toUpperCase()];
|
|
@@ -11320,7 +11392,7 @@ function schemeOf2(url) {
|
|
|
11320
11392
|
const match = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
11321
11393
|
return match ? match[1].toLowerCase() : "unknown";
|
|
11322
11394
|
}
|
|
11323
|
-
var CREATE_TABLE, DatabaseSessionHandler;
|
|
11395
|
+
var CREATE_TABLE, CREATE_ATTEMPTS, CREATE_RETRY_DELAY_MS, DatabaseSessionHandler;
|
|
11324
11396
|
var init_databaseHandler = __esm({
|
|
11325
11397
|
"../core/src/sessionHandlers/databaseHandler.ts"() {
|
|
11326
11398
|
"use strict";
|
|
@@ -11400,6 +11472,8 @@ var init_databaseHandler = __esm({
|
|
|
11400
11472
|
END
|
|
11401
11473
|
`
|
|
11402
11474
|
};
|
|
11475
|
+
CREATE_ATTEMPTS = 5;
|
|
11476
|
+
CREATE_RETRY_DELAY_MS = 50;
|
|
11403
11477
|
DatabaseSessionHandler = class {
|
|
11404
11478
|
sqliteHandle = null;
|
|
11405
11479
|
/** Set for SQLite. Null when this handler talks to a networked engine. */
|
|
@@ -11440,6 +11514,7 @@ var init_databaseHandler = __esm({
|
|
|
11440
11514
|
get sqlite() {
|
|
11441
11515
|
if (this.sqliteHandle === null) {
|
|
11442
11516
|
this.sqliteHandle = new DatabaseSync(this.dbPath);
|
|
11517
|
+
this.sqliteHandle.exec("PRAGMA busy_timeout = 5000");
|
|
11443
11518
|
this.sqliteHandle.exec("PRAGMA journal_mode = WAL");
|
|
11444
11519
|
}
|
|
11445
11520
|
return this.sqliteHandle;
|
|
@@ -11527,15 +11602,55 @@ var init_databaseHandler = __esm({
|
|
|
11527
11602
|
sqlCommandSync(this.target, sql, params);
|
|
11528
11603
|
}
|
|
11529
11604
|
/**
|
|
11530
|
-
* Ensure the session table exists (called once on first use)
|
|
11605
|
+
* Ensure the session table exists (called once on first use) - safely when
|
|
11606
|
+
* several processes race to create it.
|
|
11607
|
+
*
|
|
11608
|
+
* Every per-engine statement above is check-then-act, so two processes can
|
|
11609
|
+
* both find the table absent and both CREATE; the loser errors, and every
|
|
11610
|
+
* engine spells that differently (a pg_type unique violation on PostgreSQL,
|
|
11611
|
+
* Msg 2714 on SQL Server, an RDB$RELATIONS unique violation on Firebird). So
|
|
11612
|
+
* a failure is judged by RE-CHECKING whether the table now exists, never by
|
|
11613
|
+
* parsing the message - the rescue the Firebird note above has always
|
|
11614
|
+
* assumed, and which was not here. MEASURED on the lab before it: six real
|
|
11615
|
+
* processes racing lost one to five of them on PostgreSQL, SQLite and
|
|
11616
|
+
* Firebird.
|
|
11617
|
+
*
|
|
11618
|
+
* The re-check is RETRIED because the loser can fail BEFORE the winner has
|
|
11619
|
+
* committed. On MySQL, CREATE TABLE IF NOT EXISTS takes a shared metadata
|
|
11620
|
+
* lock, checks, then upgrades it to exclusive; two upgrades deadlock, and when
|
|
11621
|
+
* MySQL cannot back the victim off silently it answers 1213 "Deadlock found"
|
|
11622
|
+
* while the winner's table is still invisible (tina4-php CI run 35972320442).
|
|
11623
|
+
* Bounded, so a genuine failure (no CREATE grant) still throws. Same attempts
|
|
11624
|
+
* and delays in all four frameworks.
|
|
11531
11625
|
*/
|
|
11532
11626
|
ensureTable() {
|
|
11533
11627
|
if (this.initialized) return;
|
|
11534
11628
|
const ddl = CREATE_TABLE[this.engine];
|
|
11535
|
-
|
|
11536
|
-
|
|
11629
|
+
for (let attempt = 1; ; attempt++) {
|
|
11630
|
+
try {
|
|
11631
|
+
if (this.target === null) this.sqlite.exec(ddl);
|
|
11632
|
+
else sqlCommandSync(this.target, ddl, []);
|
|
11633
|
+
break;
|
|
11634
|
+
} catch (error2) {
|
|
11635
|
+
if (this.tableExists()) break;
|
|
11636
|
+
if (attempt >= CREATE_ATTEMPTS) throw error2;
|
|
11637
|
+
sleepSync2(CREATE_RETRY_DELAY_MS * attempt);
|
|
11638
|
+
}
|
|
11639
|
+
}
|
|
11537
11640
|
this.initialized = true;
|
|
11538
11641
|
}
|
|
11642
|
+
/**
|
|
11643
|
+
* Whether tina4_session exists, asked the one way every engine answers alike:
|
|
11644
|
+
* a query against it that returns no rows succeeds only if it is there.
|
|
11645
|
+
*/
|
|
11646
|
+
tableExists() {
|
|
11647
|
+
try {
|
|
11648
|
+
this.query("SELECT 1 FROM tina4_session WHERE 1 = 0", []);
|
|
11649
|
+
return true;
|
|
11650
|
+
} catch {
|
|
11651
|
+
return false;
|
|
11652
|
+
}
|
|
11653
|
+
}
|
|
11539
11654
|
read(sessionId) {
|
|
11540
11655
|
this.ensureTable();
|
|
11541
11656
|
const rows = this.query(
|
|
@@ -11545,8 +11660,8 @@ var init_databaseHandler = __esm({
|
|
|
11545
11660
|
const row = rows[0];
|
|
11546
11661
|
if (!row) return null;
|
|
11547
11662
|
const now = Date.now() / 1e3;
|
|
11548
|
-
const
|
|
11549
|
-
if (
|
|
11663
|
+
const expiresAt2 = Number(column(row, "expires_at") ?? 0);
|
|
11664
|
+
if (expiresAt2 > 0 && expiresAt2 < now) {
|
|
11550
11665
|
this.destroy(sessionId);
|
|
11551
11666
|
return null;
|
|
11552
11667
|
}
|
|
@@ -11559,7 +11674,7 @@ var init_databaseHandler = __esm({
|
|
|
11559
11674
|
write(sessionId, data, ttl) {
|
|
11560
11675
|
this.ensureTable();
|
|
11561
11676
|
const json = JSON.stringify(data);
|
|
11562
|
-
const
|
|
11677
|
+
const expiresAt2 = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
|
|
11563
11678
|
const existing = this.query(
|
|
11564
11679
|
"SELECT session_id FROM tina4_session WHERE session_id = ?",
|
|
11565
11680
|
[sessionId]
|
|
@@ -11567,12 +11682,12 @@ var init_databaseHandler = __esm({
|
|
|
11567
11682
|
if (existing.length > 0) {
|
|
11568
11683
|
this.exec(
|
|
11569
11684
|
"UPDATE tina4_session SET data = ?, expires_at = ? WHERE session_id = ?",
|
|
11570
|
-
[json,
|
|
11685
|
+
[json, expiresAt2, sessionId]
|
|
11571
11686
|
);
|
|
11572
11687
|
} else {
|
|
11573
11688
|
this.exec(
|
|
11574
11689
|
"INSERT INTO tina4_session (session_id, data, expires_at) VALUES (?, ?, ?)",
|
|
11575
|
-
[sessionId, json,
|
|
11690
|
+
[sessionId, json, expiresAt2]
|
|
11576
11691
|
);
|
|
11577
11692
|
}
|
|
11578
11693
|
}
|
|
@@ -15567,7 +15682,7 @@ function envTruthy(val) {
|
|
|
15567
15682
|
return ["true", "1", "yes", "on"].includes(val.trim().toLowerCase());
|
|
15568
15683
|
}
|
|
15569
15684
|
function isLoopback(ip) {
|
|
15570
|
-
if (ip == null || ip === "") return
|
|
15685
|
+
if (ip == null || ip === "") return false;
|
|
15571
15686
|
let addr = ip.trim().toLowerCase();
|
|
15572
15687
|
if (addr.startsWith("::ffff:")) addr = addr.slice(7);
|
|
15573
15688
|
return addr === "::1" || addr === "localhost" || addr.startsWith("127.");
|
|
@@ -17158,6 +17273,7 @@ __export(graphql_exports, {
|
|
|
17158
17273
|
graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
|
|
17159
17274
|
graphqlEndpoint: () => graphqlEndpoint,
|
|
17160
17275
|
graphqlMaxDepth: () => graphqlMaxDepth,
|
|
17276
|
+
graphqlMaxNodes: () => graphqlMaxNodes,
|
|
17161
17277
|
tokenize: () => tokenize2
|
|
17162
17278
|
});
|
|
17163
17279
|
function tokenize2(source) {
|
|
@@ -17197,7 +17313,12 @@ function graphqlMaxDepth() {
|
|
|
17197
17313
|
const n = parseInt(raw, 10);
|
|
17198
17314
|
return Number.isNaN(n) ? 50 : n;
|
|
17199
17315
|
}
|
|
17200
|
-
|
|
17316
|
+
function graphqlMaxNodes() {
|
|
17317
|
+
const raw = (process.env.TINA4_GRAPHQL_MAX_NODES ?? "1000").trim();
|
|
17318
|
+
const n = parseInt(raw, 10);
|
|
17319
|
+
return Number.isNaN(n) ? 1e3 : n;
|
|
17320
|
+
}
|
|
17321
|
+
var TOKEN_PATTERNS, ParseError, Parser, ComplexityError, GraphQL, GraphQLType;
|
|
17201
17322
|
var init_graphql = __esm({
|
|
17202
17323
|
"../core/src/graphql.ts"() {
|
|
17203
17324
|
"use strict";
|
|
@@ -17236,9 +17357,16 @@ var init_graphql = __esm({
|
|
|
17236
17357
|
Parser = class {
|
|
17237
17358
|
tokens;
|
|
17238
17359
|
pos;
|
|
17360
|
+
// F2: bound the parser's own recursion. A deeply nested query
|
|
17361
|
+
// ("{a{a{a...}}}") otherwise recurses in parseSelectionSet until the call
|
|
17362
|
+
// stack overflows — BEFORE the execution-time depth guard can run. Reusing
|
|
17363
|
+
// TINA4_GRAPHQL_MAX_DEPTH keeps parse and execution on one bound.
|
|
17364
|
+
depth = 0;
|
|
17365
|
+
maxDepth;
|
|
17239
17366
|
constructor(tokens) {
|
|
17240
17367
|
this.tokens = tokens;
|
|
17241
17368
|
this.pos = 0;
|
|
17369
|
+
this.maxDepth = graphqlMaxDepth();
|
|
17242
17370
|
}
|
|
17243
17371
|
peek() {
|
|
17244
17372
|
return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
|
|
@@ -17320,6 +17448,17 @@ var init_graphql = __esm({
|
|
|
17320
17448
|
};
|
|
17321
17449
|
}
|
|
17322
17450
|
parseSelectionSet() {
|
|
17451
|
+
this.depth++;
|
|
17452
|
+
if (this.maxDepth > 0 && this.depth > this.maxDepth) {
|
|
17453
|
+
throw new Error(`Query exceeds maximum depth of ${this.maxDepth}`);
|
|
17454
|
+
}
|
|
17455
|
+
try {
|
|
17456
|
+
return this.parseSelectionSetInner();
|
|
17457
|
+
} finally {
|
|
17458
|
+
this.depth--;
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
17461
|
+
parseSelectionSetInner() {
|
|
17323
17462
|
this.expect("LBRACE");
|
|
17324
17463
|
const selections = [];
|
|
17325
17464
|
while (!this.match("RBRACE")) {
|
|
@@ -17474,6 +17613,8 @@ var init_graphql = __esm({
|
|
|
17474
17613
|
return t;
|
|
17475
17614
|
}
|
|
17476
17615
|
};
|
|
17616
|
+
ComplexityError = class extends Error {
|
|
17617
|
+
};
|
|
17477
17618
|
GraphQL = class _GraphQL {
|
|
17478
17619
|
types = /* @__PURE__ */ new Map();
|
|
17479
17620
|
queries = /* @__PURE__ */ new Map();
|
|
@@ -17496,6 +17637,12 @@ var init_graphql = __esm({
|
|
|
17496
17637
|
* spread / inline fragment, so circular fragments are caught too.
|
|
17497
17638
|
*/
|
|
17498
17639
|
maxDepth = graphqlMaxDepth();
|
|
17640
|
+
/**
|
|
17641
|
+
* Maximum expanded selection nodes per query (from `TINA4_GRAPHQL_MAX_NODES`,
|
|
17642
|
+
* default 1000; `<= 0` disables). Bounds query WIDTH — fragment bombs and
|
|
17643
|
+
* alias explosions — which the depth guard does not. See {@link graphqlMaxNodes}.
|
|
17644
|
+
*/
|
|
17645
|
+
maxNodes = graphqlMaxNodes();
|
|
17499
17646
|
/**
|
|
17500
17647
|
* Decorator-style resolver registration.
|
|
17501
17648
|
*
|
|
@@ -17643,6 +17790,16 @@ var init_graphql = __esm({
|
|
|
17643
17790
|
return { data: null, errors: [{ message: "No operation found" }] };
|
|
17644
17791
|
}
|
|
17645
17792
|
const op = operations[0];
|
|
17793
|
+
if (this.maxNodes > 0) {
|
|
17794
|
+
try {
|
|
17795
|
+
this.assertComplexity(op.selections, fragments);
|
|
17796
|
+
} catch (e) {
|
|
17797
|
+
if (e instanceof ComplexityError) {
|
|
17798
|
+
return { data: null, errors: [{ message: e.message }] };
|
|
17799
|
+
}
|
|
17800
|
+
throw e;
|
|
17801
|
+
}
|
|
17802
|
+
}
|
|
17646
17803
|
const resolvers = op.operation === "query" ? this.queries : this.mutations;
|
|
17647
17804
|
for (const vdef of op.variables) {
|
|
17648
17805
|
if (!(vdef.name in vars) && vdef.default !== void 0) {
|
|
@@ -17668,6 +17825,36 @@ var init_graphql = __esm({
|
|
|
17668
17825
|
* instead of recursing until the interpreter stack overflows. Top-level
|
|
17669
17826
|
* starts at depth 1; `maxDepth <= 0` disables the guard.
|
|
17670
17827
|
*/
|
|
17828
|
+
/**
|
|
17829
|
+
* Count the expanded selection nodes, throwing {@link ComplexityError} past
|
|
17830
|
+
* `maxNodes` (F2). Fragment spreads are expanded (so a fragment bomb is
|
|
17831
|
+
* counted, not the small unexpanded document) and every field — including
|
|
17832
|
+
* each alias — counts once. The count is capped by `maxNodes` and the walk by
|
|
17833
|
+
* `maxDepth`, so a circular fragment trips the budget instead of looping and
|
|
17834
|
+
* the check itself can never cost more than the budget.
|
|
17835
|
+
*/
|
|
17836
|
+
assertComplexity(selections, fragments) {
|
|
17837
|
+
const depthCap = this.maxDepth > 0 ? this.maxDepth : 1e3;
|
|
17838
|
+
let count = 0;
|
|
17839
|
+
const walk2 = (sels, depth) => {
|
|
17840
|
+
if (depth > depthCap) return;
|
|
17841
|
+
for (const sel of sels) {
|
|
17842
|
+
count++;
|
|
17843
|
+
if (count > this.maxNodes) {
|
|
17844
|
+
throw new ComplexityError(`Query exceeds maximum complexity of ${this.maxNodes} nodes`);
|
|
17845
|
+
}
|
|
17846
|
+
if (sel.kind === "fragment_spread") {
|
|
17847
|
+
const frag = fragments.get(sel.name);
|
|
17848
|
+
if (frag) walk2(frag.selections, depth + 1);
|
|
17849
|
+
} else if (sel.kind === "inline_fragment") {
|
|
17850
|
+
walk2(sel.selections, depth + 1);
|
|
17851
|
+
} else if (sel.kind === "field" && sel.selections) {
|
|
17852
|
+
walk2(sel.selections, depth + 1);
|
|
17853
|
+
}
|
|
17854
|
+
}
|
|
17855
|
+
};
|
|
17856
|
+
walk2(selections, 1);
|
|
17857
|
+
}
|
|
17671
17858
|
async resolveSelectionsInto(selections, resolvers, parent, variables, context, fragments, target, depth) {
|
|
17672
17859
|
const errors = [];
|
|
17673
17860
|
if (this.maxDepth > 0 && depth > this.maxDepth) {
|
|
@@ -18870,7 +19057,8 @@ function wsAuthorized(route, headers, queryString = "", subprotocol = "") {
|
|
|
18870
19057
|
const token = wsToken(headers, queryString, subprotocol);
|
|
18871
19058
|
if (!token) return [null, false];
|
|
18872
19059
|
const payload = validToken(token);
|
|
18873
|
-
return [
|
|
19060
|
+
if (!isIdentityPayload(payload)) return [null, false];
|
|
19061
|
+
return [payload, true];
|
|
18874
19062
|
}
|
|
18875
19063
|
function offeredBearerSubprotocol(subprotocol) {
|
|
18876
19064
|
return subprotocol.split(",").some((p) => p.trim().toLowerCase() === "bearer");
|
|
@@ -20065,12 +20253,36 @@ var init_mongoBackend = __esm({
|
|
|
20065
20253
|
}
|
|
20066
20254
|
}
|
|
20067
20255
|
else if (operation === "size") {
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20256
|
+
// ADR-0022 dec 7: size(status) must not answer a different question.
|
|
20257
|
+
// The dead aliases count the .dead_letter store (== deadLetters().length);
|
|
20258
|
+
// reserved/completed count by status; else pending. data = status.
|
|
20259
|
+
const status = data || "pending";
|
|
20260
|
+
let count: number;
|
|
20261
|
+
if (status === "dead" || status === "failed" || status === "dead_letter") {
|
|
20262
|
+
count = await col.countDocuments({ queue: queueName + ".dead_letter" });
|
|
20263
|
+
} else if (status === "reserved" || status === "completed") {
|
|
20264
|
+
count = await col.countDocuments({ queue: queueName, status });
|
|
20265
|
+
} else {
|
|
20266
|
+
count = await col.countDocuments({ queue: queueName, status: "pending" });
|
|
20267
|
+
}
|
|
20072
20268
|
process.stdout.write(String(count));
|
|
20073
20269
|
}
|
|
20270
|
+
else if (operation === "reject") {
|
|
20271
|
+
// Dead-letter NOW, no retry (ADR-0023). Floor attempts at maxRetries
|
|
20272
|
+
// so it reads as exhausted (parity with fail()'s dead-letter branch
|
|
20273
|
+
// and size("dead")). data = JSON { id, reason, maxRetries }.
|
|
20274
|
+
const info = JSON.parse(data);
|
|
20275
|
+
const doc = await col.findOne({ queue: queueName, id: info.id });
|
|
20276
|
+
if (doc) {
|
|
20277
|
+
const attempts = Math.max((doc.attempts || 0) + 1, info.maxRetries);
|
|
20278
|
+
await col.insertOne({
|
|
20279
|
+
...doc, _id: undefined, attempts,
|
|
20280
|
+
status: "dead", queue: queueName + ".dead_letter", error: info.reason,
|
|
20281
|
+
});
|
|
20282
|
+
await col.deleteOne({ _id: doc._id, queue: queueName });
|
|
20283
|
+
}
|
|
20284
|
+
process.stdout.write("__OK__");
|
|
20285
|
+
}
|
|
20074
20286
|
else if (operation === "clear") {
|
|
20075
20287
|
await col.deleteMany({ queue: queueName });
|
|
20076
20288
|
process.stdout.write("__CLEARED__");
|
|
@@ -20306,11 +20518,20 @@ var init_mongoBackend = __esm({
|
|
|
20306
20518
|
return null;
|
|
20307
20519
|
}
|
|
20308
20520
|
}
|
|
20309
|
-
size(queue) {
|
|
20310
|
-
const result = this.execSync("size", queue);
|
|
20521
|
+
size(queue, status2 = "pending") {
|
|
20522
|
+
const result = this.execSync("size", queue, status2);
|
|
20311
20523
|
const num = parseInt(result, 10);
|
|
20312
20524
|
return isNaN(num) ? 0 : num;
|
|
20313
20525
|
}
|
|
20526
|
+
/**
|
|
20527
|
+
* Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
|
|
20528
|
+
* fail(): fail() requeues until maxRetries is spent. attempts is floored at
|
|
20529
|
+
* maxRetries so deadLetters()/size("dead") (which filter/scope to the
|
|
20530
|
+
* dead-letter store) see it.
|
|
20531
|
+
*/
|
|
20532
|
+
reject(queue, id, reason, maxRetries) {
|
|
20533
|
+
this.execSync("reject", queue, JSON.stringify({ id, reason, maxRetries }));
|
|
20534
|
+
}
|
|
20314
20535
|
clear(queue) {
|
|
20315
20536
|
this.execSync("clear", queue);
|
|
20316
20537
|
}
|
|
@@ -20405,7 +20626,9 @@ function createJob(data, queue) {
|
|
|
20405
20626
|
queue._failJob(job.topic, job, reason, queue.getMaxRetries());
|
|
20406
20627
|
},
|
|
20407
20628
|
reject(reason = "") {
|
|
20408
|
-
job.
|
|
20629
|
+
job.status = "dead";
|
|
20630
|
+
job.error = reason;
|
|
20631
|
+
queue._rejectJob(job.topic, job, reason, queue.getMaxRetries());
|
|
20409
20632
|
},
|
|
20410
20633
|
retry(delaySeconds) {
|
|
20411
20634
|
queue._retryJob(job.topic, job, delaySeconds);
|
|
@@ -21014,6 +21237,20 @@ var init_liteBackend = __esm({
|
|
|
21014
21237
|
* dead-letter file -- two spellings of the same intent that previously
|
|
21015
21238
|
* diverged.
|
|
21016
21239
|
*/
|
|
21240
|
+
/**
|
|
21241
|
+
* Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
|
|
21242
|
+
*
|
|
21243
|
+
* Unlike failJob(), which requeues until maxRetries is spent, rejectJob()
|
|
21244
|
+
* moves a known-poison job straight to the dead-letter store. attempts is
|
|
21245
|
+
* floored at maxRetries so deadLetters() (attempts >= maxRetries) returns it
|
|
21246
|
+
* and size("dead") agrees.
|
|
21247
|
+
*/
|
|
21248
|
+
rejectJob(queue, job, reason, maxRetries) {
|
|
21249
|
+
this.clearReservation(queue, job.id);
|
|
21250
|
+
job.attempts = Math.max((job.attempts || 0) + 1, maxRetries);
|
|
21251
|
+
job.error = reason;
|
|
21252
|
+
this.deadLetter(queue, job, reason);
|
|
21253
|
+
}
|
|
21017
21254
|
retryJob(queue, job, delaySeconds) {
|
|
21018
21255
|
this.clearReservation(queue, job.id);
|
|
21019
21256
|
try {
|
|
@@ -21235,7 +21472,7 @@ var init_queue = __esm({
|
|
|
21235
21472
|
size(status2 = "pending") {
|
|
21236
21473
|
const q = this.topic;
|
|
21237
21474
|
if (this.externalBackend) {
|
|
21238
|
-
return this.externalBackend.size(q);
|
|
21475
|
+
return this.externalBackend.size(q, status2);
|
|
21239
21476
|
}
|
|
21240
21477
|
return this.liteBackend.size(q, status2);
|
|
21241
21478
|
}
|
|
@@ -21503,6 +21740,17 @@ var init_queue = __esm({
|
|
|
21503
21740
|
}
|
|
21504
21741
|
this.liteBackend.failJob(queue, job, error2, maxRetries, this._retryBackoff);
|
|
21505
21742
|
}
|
|
21743
|
+
/**
|
|
21744
|
+
* Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
|
|
21745
|
+
* _failJob, which retries until maxRetries is spent.
|
|
21746
|
+
*/
|
|
21747
|
+
_rejectJob(queue, job, reason, maxRetries) {
|
|
21748
|
+
if (this.externalBackend?.reject) {
|
|
21749
|
+
this.externalBackend.reject(queue, job.id, reason, maxRetries);
|
|
21750
|
+
return;
|
|
21751
|
+
}
|
|
21752
|
+
this.liteBackend.rejectJob(queue, job, reason, maxRetries);
|
|
21753
|
+
}
|
|
21506
21754
|
/**
|
|
21507
21755
|
* Re-queue a job back to the main queue directory with incremented attempts.
|
|
21508
21756
|
*/
|
|
@@ -27214,13 +27462,20 @@ var sso_exports = {};
|
|
|
27214
27462
|
__export(sso_exports, {
|
|
27215
27463
|
SSO: () => Sso,
|
|
27216
27464
|
Sso: () => Sso,
|
|
27217
|
-
SsoError: () => SsoError
|
|
27465
|
+
SsoError: () => SsoError,
|
|
27466
|
+
liveSessionIdentity: () => liveSessionIdentity
|
|
27218
27467
|
});
|
|
27219
27468
|
import { createHash as createHash9, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
27469
|
+
function expiresAt(tokens) {
|
|
27470
|
+
const lifetime = Number(tokens.expires_in ?? 0);
|
|
27471
|
+
return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
|
|
27472
|
+
}
|
|
27220
27473
|
var SsoError, Sso;
|
|
27221
27474
|
var init_sso = __esm({
|
|
27222
27475
|
"../core/src/sso.ts"() {
|
|
27223
27476
|
"use strict";
|
|
27477
|
+
init_ssoIdentity();
|
|
27478
|
+
init_ssoIdentity();
|
|
27224
27479
|
SsoError = class extends Error {
|
|
27225
27480
|
};
|
|
27226
27481
|
Sso = class _Sso {
|
|
@@ -27426,13 +27681,12 @@ var init_sso = __esm({
|
|
|
27426
27681
|
access_token: tokens.access_token,
|
|
27427
27682
|
refresh_token: tokens.refresh_token,
|
|
27428
27683
|
id_token: tokens.id_token,
|
|
27429
|
-
expires_at:
|
|
27684
|
+
expires_at: expiresAt(tokens)
|
|
27430
27685
|
});
|
|
27431
27686
|
return { identity, return_to: _Sso.safeReturn(pending.return_to) };
|
|
27432
27687
|
}
|
|
27433
27688
|
identity(requestOrSession) {
|
|
27434
|
-
const
|
|
27435
|
-
const identity = stored?.identity ?? null;
|
|
27689
|
+
const identity = liveSessionIdentity(this.session(requestOrSession)?.get(_Sso.SESSION_KEY));
|
|
27436
27690
|
if (identity && requestOrSession?.session) requestOrSession.user = identity;
|
|
27437
27691
|
return identity;
|
|
27438
27692
|
}
|
|
@@ -27459,7 +27713,7 @@ var init_sso = __esm({
|
|
|
27459
27713
|
access_token: tokens.access_token,
|
|
27460
27714
|
refresh_token: tokens.refresh_token ?? stored.refresh_token,
|
|
27461
27715
|
id_token: tokens.id_token ?? stored.id_token,
|
|
27462
|
-
expires_at:
|
|
27716
|
+
expires_at: expiresAt(tokens)
|
|
27463
27717
|
});
|
|
27464
27718
|
return identity;
|
|
27465
27719
|
} catch (error2) {
|
|
@@ -27601,12 +27855,12 @@ function bannerSurfaceLines(port, opts) {
|
|
|
27601
27855
|
];
|
|
27602
27856
|
}
|
|
27603
27857
|
function swaggerAdvertised() {
|
|
27604
|
-
const
|
|
27858
|
+
const TRUTHY3 = ["true", "1", "yes", "on"];
|
|
27605
27859
|
const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
|
|
27606
27860
|
if (raw === "") {
|
|
27607
|
-
return
|
|
27861
|
+
return TRUTHY3.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
|
|
27608
27862
|
}
|
|
27609
|
-
return
|
|
27863
|
+
return TRUTHY3.includes(raw);
|
|
27610
27864
|
}
|
|
27611
27865
|
async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
|
|
27612
27866
|
const dir = resolve15(base, migrationDir);
|
|
@@ -28525,8 +28779,9 @@ function wireLocaleGlobal(frondEngine, base) {
|
|
|
28525
28779
|
async function startServer(config) {
|
|
28526
28780
|
loadEnv(".env.local");
|
|
28527
28781
|
loadEnv();
|
|
28528
|
-
const { ensureDevSecret: ensureDevSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
28782
|
+
const { ensureDevSecret: ensureDevSecret2, requireBootSecret: requireBootSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
28529
28783
|
ensureDevSecret2();
|
|
28784
|
+
requireBootSecret2();
|
|
28530
28785
|
_checkLegacyEnvVars();
|
|
28531
28786
|
const resolved = resolvePortAndHost(config);
|
|
28532
28787
|
const host = resolved.host;
|
|
@@ -30722,6 +30977,178 @@ var init_service = __esm({
|
|
|
30722
30977
|
}
|
|
30723
30978
|
});
|
|
30724
30979
|
|
|
30980
|
+
// ../core/src/ssrf.ts
|
|
30981
|
+
import { isIP } from "node:net";
|
|
30982
|
+
import { lookup } from "node:dns/promises";
|
|
30983
|
+
function v4ToBytes(ip) {
|
|
30984
|
+
const parts = ip.split(".");
|
|
30985
|
+
if (parts.length !== 4) return null;
|
|
30986
|
+
const bytes = new Uint8Array(4);
|
|
30987
|
+
for (let i = 0; i < 4; i++) {
|
|
30988
|
+
const n = Number(parts[i]);
|
|
30989
|
+
if (!Number.isInteger(n) || n < 0 || n > 255 || !/^\d+$/.test(parts[i])) return null;
|
|
30990
|
+
bytes[i] = n;
|
|
30991
|
+
}
|
|
30992
|
+
return bytes;
|
|
30993
|
+
}
|
|
30994
|
+
function v6ToBytes(input) {
|
|
30995
|
+
let ip = input.split("%")[0];
|
|
30996
|
+
const lastColon = ip.lastIndexOf(":");
|
|
30997
|
+
const tail = ip.slice(lastColon + 1);
|
|
30998
|
+
if (tail.includes(".")) {
|
|
30999
|
+
const v4 = v4ToBytes(tail);
|
|
31000
|
+
if (!v4) return null;
|
|
31001
|
+
const hextetA = (v4[0] << 8 | v4[1]).toString(16);
|
|
31002
|
+
const hextetB = (v4[2] << 8 | v4[3]).toString(16);
|
|
31003
|
+
ip = ip.slice(0, lastColon + 1) + hextetA + ":" + hextetB;
|
|
31004
|
+
}
|
|
31005
|
+
const halves = ip.split("::");
|
|
31006
|
+
if (halves.length > 2) return null;
|
|
31007
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
31008
|
+
const back = halves.length === 2 ? halves[1] ? halves[1].split(":") : [] : [];
|
|
31009
|
+
let groups;
|
|
31010
|
+
if (halves.length === 1) {
|
|
31011
|
+
if (head.length !== 8) return null;
|
|
31012
|
+
groups = head;
|
|
31013
|
+
} else {
|
|
31014
|
+
const missing = 8 - (head.length + back.length);
|
|
31015
|
+
if (missing < 0) return null;
|
|
31016
|
+
groups = [...head, ...Array(missing).fill("0"), ...back];
|
|
31017
|
+
}
|
|
31018
|
+
if (groups.length !== 8) return null;
|
|
31019
|
+
const bytes = new Uint8Array(16);
|
|
31020
|
+
for (let i = 0; i < 8; i++) {
|
|
31021
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(groups[i])) return null;
|
|
31022
|
+
const g = parseInt(groups[i], 16);
|
|
31023
|
+
bytes[i * 2] = g >> 8 & 255;
|
|
31024
|
+
bytes[i * 2 + 1] = g & 255;
|
|
31025
|
+
}
|
|
31026
|
+
return bytes;
|
|
31027
|
+
}
|
|
31028
|
+
function ipToBytes(ip) {
|
|
31029
|
+
const cleaned = ip.split("%")[0];
|
|
31030
|
+
const family = isIP(cleaned);
|
|
31031
|
+
if (family === 4) return v4ToBytes(cleaned);
|
|
31032
|
+
if (family === 6) {
|
|
31033
|
+
const bytes = v6ToBytes(cleaned);
|
|
31034
|
+
if (!bytes) return null;
|
|
31035
|
+
const isMapped = bytes.subarray(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
|
|
31036
|
+
return isMapped ? bytes.subarray(12) : bytes;
|
|
31037
|
+
}
|
|
31038
|
+
return null;
|
|
31039
|
+
}
|
|
31040
|
+
function parseCidr(cidr) {
|
|
31041
|
+
const [netStr, bitsStr] = cidr.split("/");
|
|
31042
|
+
const net4 = ipToBytes(netStr);
|
|
31043
|
+
if (!net4) return null;
|
|
31044
|
+
return { net: net4, bits: Number(bitsStr) };
|
|
31045
|
+
}
|
|
31046
|
+
function inCidr(packed, cidr) {
|
|
31047
|
+
if (packed.length !== cidr.net.length) return false;
|
|
31048
|
+
const whole = Math.floor(cidr.bits / 8);
|
|
31049
|
+
const remainder = cidr.bits % 8;
|
|
31050
|
+
for (let i = 0; i < whole; i++) {
|
|
31051
|
+
if (packed[i] !== cidr.net[i]) return false;
|
|
31052
|
+
}
|
|
31053
|
+
if (remainder === 0) return true;
|
|
31054
|
+
const mask = 255 << 8 - remainder & 255;
|
|
31055
|
+
return (packed[whole] & mask) === (cidr.net[whole] & mask);
|
|
31056
|
+
}
|
|
31057
|
+
function allowPrivateRequests() {
|
|
31058
|
+
return TRUTHY2.has((process.env[ALLOW_PRIVATE_ENV] ?? "").trim().toLowerCase());
|
|
31059
|
+
}
|
|
31060
|
+
function isBlockedAddress(ip) {
|
|
31061
|
+
const packed = ipToBytes(ip);
|
|
31062
|
+
if (!packed) return true;
|
|
31063
|
+
return BLOCKED.some((cidr) => inCidr(packed, cidr));
|
|
31064
|
+
}
|
|
31065
|
+
function matchesAllowList(host, port, resolved, allowHosts) {
|
|
31066
|
+
const hostLower = host.toLowerCase();
|
|
31067
|
+
for (const raw of allowHosts ?? []) {
|
|
31068
|
+
const entry = String(raw).trim().toLowerCase();
|
|
31069
|
+
if (!entry) continue;
|
|
31070
|
+
if (entry === hostLower || entry === `${hostLower}:${port}`) return true;
|
|
31071
|
+
if (entry.includes("/")) {
|
|
31072
|
+
const cidr = parseCidr(entry);
|
|
31073
|
+
if (cidr && resolved.some((ip) => {
|
|
31074
|
+
const packed = ipToBytes(ip);
|
|
31075
|
+
return packed !== null && inCidr(packed, cidr);
|
|
31076
|
+
})) {
|
|
31077
|
+
return true;
|
|
31078
|
+
}
|
|
31079
|
+
}
|
|
31080
|
+
}
|
|
31081
|
+
return false;
|
|
31082
|
+
}
|
|
31083
|
+
function logBlock(host, ip) {
|
|
31084
|
+
void Promise.resolve().then(() => (init_logger(), logger_exports)).then((mod) => mod.Log?.warning?.(`SSRF guard blocked outbound request to ${host} (${ip})`)).catch(() => void 0);
|
|
31085
|
+
}
|
|
31086
|
+
async function guardUrl(url, allowHosts) {
|
|
31087
|
+
let parsed;
|
|
31088
|
+
try {
|
|
31089
|
+
parsed = new URL(url);
|
|
31090
|
+
} catch {
|
|
31091
|
+
throw new SsrfError(`Blocked request: '${url}' is not a valid URL`);
|
|
31092
|
+
}
|
|
31093
|
+
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
31094
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
31095
|
+
throw new SsrfError(`Blocked request: URL scheme '${scheme || "(none)"}' is not http or https`);
|
|
31096
|
+
}
|
|
31097
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
31098
|
+
if (!host) {
|
|
31099
|
+
throw new SsrfError("Blocked request: URL has no host");
|
|
31100
|
+
}
|
|
31101
|
+
const port = parsed.port ? Number(parsed.port) : scheme === "https" ? 443 : 80;
|
|
31102
|
+
if (allowPrivateRequests()) return;
|
|
31103
|
+
let resolved;
|
|
31104
|
+
try {
|
|
31105
|
+
const results = await lookup(host, { all: true });
|
|
31106
|
+
resolved = results.map((r) => r.address);
|
|
31107
|
+
} catch {
|
|
31108
|
+
throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
|
|
31109
|
+
}
|
|
31110
|
+
if (resolved.length === 0) {
|
|
31111
|
+
throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
|
|
31112
|
+
}
|
|
31113
|
+
if (matchesAllowList(host, port, resolved, allowHosts)) return;
|
|
31114
|
+
for (const ip of resolved) {
|
|
31115
|
+
if (isBlockedAddress(ip)) {
|
|
31116
|
+
logBlock(host, ip);
|
|
31117
|
+
throw new SsrfError(
|
|
31118
|
+
`Blocked request to private/internal address ${ip} (host ${host}): set ${ALLOW_PRIVATE_ENV}=true to allow, or pass an allow-list.`
|
|
31119
|
+
);
|
|
31120
|
+
}
|
|
31121
|
+
}
|
|
31122
|
+
}
|
|
31123
|
+
var ALLOW_PRIVATE_ENV, TRUTHY2, SsrfError, BLOCKED_CIDRS, BLOCKED;
|
|
31124
|
+
var init_ssrf = __esm({
|
|
31125
|
+
"../core/src/ssrf.ts"() {
|
|
31126
|
+
"use strict";
|
|
31127
|
+
ALLOW_PRIVATE_ENV = "TINA4_ALLOW_PRIVATE_REQUESTS";
|
|
31128
|
+
TRUTHY2 = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
31129
|
+
SsrfError = class extends Error {
|
|
31130
|
+
constructor(message) {
|
|
31131
|
+
super(message);
|
|
31132
|
+
this.name = "SsrfError";
|
|
31133
|
+
}
|
|
31134
|
+
};
|
|
31135
|
+
BLOCKED_CIDRS = [
|
|
31136
|
+
"0.0.0.0/8",
|
|
31137
|
+
"10.0.0.0/8",
|
|
31138
|
+
"127.0.0.0/8",
|
|
31139
|
+
"169.254.0.0/16",
|
|
31140
|
+
"172.16.0.0/12",
|
|
31141
|
+
"192.168.0.0/16",
|
|
31142
|
+
"100.64.0.0/10",
|
|
31143
|
+
"::1/128",
|
|
31144
|
+
"::/128",
|
|
31145
|
+
"fc00::/7",
|
|
31146
|
+
"fe80::/10"
|
|
31147
|
+
];
|
|
31148
|
+
BLOCKED = BLOCKED_CIDRS.map(parseCidr).filter((c) => c !== null);
|
|
31149
|
+
}
|
|
31150
|
+
});
|
|
31151
|
+
|
|
30725
31152
|
// ../core/src/api.ts
|
|
30726
31153
|
import http from "node:http";
|
|
30727
31154
|
import https from "node:https";
|
|
@@ -30878,6 +31305,7 @@ var init_api = __esm({
|
|
|
30878
31305
|
"../core/src/api.ts"() {
|
|
30879
31306
|
"use strict";
|
|
30880
31307
|
init_version();
|
|
31308
|
+
init_ssrf();
|
|
30881
31309
|
ApiStreamError = class extends Error {
|
|
30882
31310
|
constructor(message, status2 = null) {
|
|
30883
31311
|
super(message);
|
|
@@ -30932,6 +31360,7 @@ var init_api = __esm({
|
|
|
30932
31360
|
transportFn;
|
|
30933
31361
|
cookiesEnabled;
|
|
30934
31362
|
cookies;
|
|
31363
|
+
allowHosts;
|
|
30935
31364
|
/**
|
|
30936
31365
|
* Construct an Api client.
|
|
30937
31366
|
*
|
|
@@ -30971,6 +31400,7 @@ var init_api = __esm({
|
|
|
30971
31400
|
this.transportFn = void 0;
|
|
30972
31401
|
this.cookiesEnabled = false;
|
|
30973
31402
|
this.cookies = {};
|
|
31403
|
+
this.allowHosts = [];
|
|
30974
31404
|
if (typeof authHeaderOrOptions === "object" && authHeaderOrOptions !== null) {
|
|
30975
31405
|
const opts = authHeaderOrOptions;
|
|
30976
31406
|
this.authHeader = opts.authHeader ?? "";
|
|
@@ -30980,6 +31410,7 @@ var init_api = __esm({
|
|
|
30980
31410
|
this.retryBackoff = opts.retryBackoff ?? 0.5;
|
|
30981
31411
|
this.transportFn = opts.transport;
|
|
30982
31412
|
this.cookiesEnabled = opts.cookies ?? false;
|
|
31413
|
+
this.allowHosts = opts.allowHosts ?? [];
|
|
30983
31414
|
if (opts.bearerToken != null) {
|
|
30984
31415
|
this.setBearerToken(opts.bearerToken);
|
|
30985
31416
|
} else if (opts.username != null && opts.password != null) {
|
|
@@ -31293,7 +31724,12 @@ var init_api = __esm({
|
|
|
31293
31724
|
* body-phase timeout is applied by the caller (streamBytes) via
|
|
31294
31725
|
* `res.destroy()`.
|
|
31295
31726
|
*/
|
|
31296
|
-
openStreamRequest(method, url, headers, data, connectSec) {
|
|
31727
|
+
async openStreamRequest(method, url, headers, data, connectSec) {
|
|
31728
|
+
try {
|
|
31729
|
+
await guardUrl(url, this.allowHosts);
|
|
31730
|
+
} catch (err) {
|
|
31731
|
+
throw new ApiStreamError(err instanceof Error ? err.message : String(err), null);
|
|
31732
|
+
}
|
|
31297
31733
|
return new Promise((resolve21, reject) => {
|
|
31298
31734
|
let parsed;
|
|
31299
31735
|
try {
|
|
@@ -31352,10 +31788,11 @@ var init_api = __esm({
|
|
|
31352
31788
|
*/
|
|
31353
31789
|
buildRequest(method, contentType, body, extraHeaders, targetUrl) {
|
|
31354
31790
|
const headers = { "User-Agent": `Tina4/${TINA4_VERSION}`, ...this.headers };
|
|
31355
|
-
|
|
31791
|
+
const sameOriginAsBase = targetUrl === void 0 || sameOrigin(targetUrl, this.baseUrl);
|
|
31792
|
+
if (this.authHeader && sameOriginAsBase) {
|
|
31356
31793
|
headers["Authorization"] = this.authHeader;
|
|
31357
31794
|
}
|
|
31358
|
-
if (this.cookiesEnabled) {
|
|
31795
|
+
if (this.cookiesEnabled && sameOriginAsBase) {
|
|
31359
31796
|
const cookieHeader = this.cookieHeader();
|
|
31360
31797
|
if (cookieHeader) {
|
|
31361
31798
|
headers["Cookie"] = cookieHeader;
|
|
@@ -31456,7 +31893,12 @@ var init_api = __esm({
|
|
|
31456
31893
|
* bearer token / session cookie never leaks to a host you didn't
|
|
31457
31894
|
* authenticate to.
|
|
31458
31895
|
*/
|
|
31459
|
-
performRequest(method, url, headers, data, redirectsLeft) {
|
|
31896
|
+
async performRequest(method, url, headers, data, redirectsLeft) {
|
|
31897
|
+
try {
|
|
31898
|
+
await guardUrl(url, this.allowHosts);
|
|
31899
|
+
} catch (err) {
|
|
31900
|
+
return { kind: "error", error: err instanceof Error ? err.message : String(err) };
|
|
31901
|
+
}
|
|
31460
31902
|
return new Promise((resolve21) => {
|
|
31461
31903
|
let parsed;
|
|
31462
31904
|
try {
|
|
@@ -34196,6 +34638,7 @@ var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
|
|
|
34196
34638
|
var init_push = __esm({
|
|
34197
34639
|
"../core/src/push.ts"() {
|
|
34198
34640
|
"use strict";
|
|
34641
|
+
init_ssrf();
|
|
34199
34642
|
CURVE = "prime256v1";
|
|
34200
34643
|
RECORD_SIZE = 4096;
|
|
34201
34644
|
MAX_PAYLOAD = RECORD_SIZE - 17;
|
|
@@ -34270,10 +34713,19 @@ var init_push = __esm({
|
|
|
34270
34713
|
return { rawPublic, rawPrivate };
|
|
34271
34714
|
}
|
|
34272
34715
|
async deliver(endpoint, config, keys, body) {
|
|
34716
|
+
try {
|
|
34717
|
+
await guardUrl(endpoint.toString(), this.options.allowHosts);
|
|
34718
|
+
} catch (error2) {
|
|
34719
|
+
if (error2 instanceof SsrfError) throw new PushError(error2.message);
|
|
34720
|
+
throw error2;
|
|
34721
|
+
}
|
|
34273
34722
|
let response;
|
|
34274
34723
|
try {
|
|
34275
34724
|
response = await fetch(endpoint, {
|
|
34276
34725
|
method: "POST",
|
|
34726
|
+
// A real push service answers the POST directly; a redirect from a push
|
|
34727
|
+
// endpoint is not followed to a private address (ADR-0084).
|
|
34728
|
+
redirect: "manual",
|
|
34277
34729
|
headers: {
|
|
34278
34730
|
Authorization: `vapid t=${vapidToken(endpoint.toString(), config.subject, keys.rawPrivate, keys.rawPublic)}, k=${config.publicKey}`,
|
|
34279
34731
|
"Content-Encoding": "aes128gcm",
|
|
@@ -36360,6 +36812,7 @@ var init_testClient = __esm({
|
|
|
36360
36812
|
reqHeaders["content-length"] = String(Buffer.byteLength(rawBody));
|
|
36361
36813
|
}
|
|
36362
36814
|
const socket = new Socket();
|
|
36815
|
+
Object.defineProperty(socket, "remoteAddress", { value: "127.0.0.1" });
|
|
36363
36816
|
const rawReq = new IncomingMessage2(socket);
|
|
36364
36817
|
rawReq.method = method.toUpperCase();
|
|
36365
36818
|
rawReq.url = path10;
|
|
@@ -36732,6 +37185,7 @@ var init_validator = __esm({
|
|
|
36732
37185
|
var src_exports2 = {};
|
|
36733
37186
|
__export(src_exports2, {
|
|
36734
37187
|
AI_TOOLS: () => AI_TOOLS,
|
|
37188
|
+
ALLOW_PRIVATE_ENV: () => ALLOW_PRIVATE_ENV,
|
|
36735
37189
|
APPLICATION_FORM: () => APPLICATION_FORM,
|
|
36736
37190
|
APPLICATION_JSON: () => APPLICATION_JSON,
|
|
36737
37191
|
APPLICATION_OCTET: () => APPLICATION_OCTET,
|
|
@@ -36839,6 +37293,7 @@ __export(src_exports2, {
|
|
|
36839
37293
|
Session: () => Session,
|
|
36840
37294
|
Sso: () => Sso,
|
|
36841
37295
|
SsoError: () => SsoError,
|
|
37296
|
+
SsrfError: () => SsrfError,
|
|
36842
37297
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
36843
37298
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
36844
37299
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -36872,6 +37327,7 @@ __export(src_exports2, {
|
|
|
36872
37327
|
addHtmlHelpers: () => addHtmlHelpers,
|
|
36873
37328
|
algorithmAvailable: () => algorithmAvailable,
|
|
36874
37329
|
allEnv: () => allEnv,
|
|
37330
|
+
allowPrivateRequests: () => allowPrivateRequests,
|
|
36875
37331
|
any: () => any,
|
|
36876
37332
|
attachCsrfFromEnv: () => attachCsrfFromEnv,
|
|
36877
37333
|
authMiddleware: () => authMiddleware,
|
|
@@ -36937,6 +37393,7 @@ __export(src_exports2, {
|
|
|
36937
37393
|
graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
|
|
36938
37394
|
graphqlEndpoint: () => graphqlEndpoint,
|
|
36939
37395
|
graphqlMaxDepth: () => graphqlMaxDepth,
|
|
37396
|
+
guardUrl: () => guardUrl,
|
|
36940
37397
|
handle: () => handle,
|
|
36941
37398
|
handleFeedbackTurn: () => handleFeedbackTurn,
|
|
36942
37399
|
handleFeedbackWidgetJs: () => handleFeedbackWidgetJs,
|
|
@@ -36950,6 +37407,7 @@ __export(src_exports2, {
|
|
|
36950
37407
|
installAll: () => installAll,
|
|
36951
37408
|
installSelected: () => installSelected,
|
|
36952
37409
|
isBannerSuppressed: () => isBannerSuppressed,
|
|
37410
|
+
isBlockedAddress: () => isBlockedAddress,
|
|
36953
37411
|
isDebugMode: () => isDebugMode,
|
|
36954
37412
|
isDev: () => isDev,
|
|
36955
37413
|
isInstalled: () => isInstalled,
|
|
@@ -37069,6 +37527,7 @@ var init_src2 = __esm({
|
|
|
37069
37527
|
init_service();
|
|
37070
37528
|
init_cache();
|
|
37071
37529
|
init_api();
|
|
37530
|
+
init_ssrf();
|
|
37072
37531
|
init_context();
|
|
37073
37532
|
init_events();
|
|
37074
37533
|
init_devAdmin();
|
|
@@ -44910,11 +45369,11 @@ async function eagerLoadBelongsToRelation(instances, model, relatedClass, relati
|
|
|
44910
45369
|
const relatedPkColumn = relatedClass.getDbColumn(relatedPk);
|
|
44911
45370
|
const related = await eagerQueryRelated(relatedClass, relatedPkColumn, values);
|
|
44912
45371
|
if (nested.length > 0 && related.length > 0) await relatedClass._eagerLoad(related, nested);
|
|
44913
|
-
const
|
|
44914
|
-
for (const record of related)
|
|
45372
|
+
const lookup2 = {};
|
|
45373
|
+
for (const record of related) lookup2[_joinKey(record[relatedPk])] = record;
|
|
44915
45374
|
for (const instance of instances) {
|
|
44916
45375
|
const value = instance[fkProp];
|
|
44917
|
-
relationshipCache(instance)[relationName] = value !== void 0 && value !== null ?
|
|
45376
|
+
relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup2[_joinKey(value)] ?? null : null;
|
|
44918
45377
|
}
|
|
44919
45378
|
}
|
|
44920
45379
|
async function eagerLoadRelation(instances, model, relationName, nested) {
|