tina4-nodejs 3.13.138 → 3.13.140

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.
Files changed (51) hide show
  1. package/CLAUDE.md +2 -2
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +683 -329
  4. package/packages/cli/src/commands/generate.ts +7 -13
  5. package/packages/core/dist/index.js +688 -329
  6. package/packages/core/src/api.ts +33 -4
  7. package/packages/core/src/auth.ts +73 -6
  8. package/packages/core/src/authGate.ts +27 -20
  9. package/packages/core/src/graphql.ts +92 -0
  10. package/packages/core/src/index.ts +1 -0
  11. package/packages/core/src/job.ts +8 -1
  12. package/packages/core/src/mcp.ts +3 -1
  13. package/packages/core/src/middleware.ts +4 -2
  14. package/packages/core/src/push.ts +16 -0
  15. package/packages/core/src/queue.ts +15 -2
  16. package/packages/core/src/queueBackends/liteBackend.ts +15 -0
  17. package/packages/core/src/queueBackends/mongoBackend.ts +40 -6
  18. package/packages/core/src/server.ts +6 -1
  19. package/packages/core/src/sessionHandlers/databaseHandler.ts +60 -3
  20. package/packages/core/src/sso.ts +12 -4
  21. package/packages/core/src/ssoIdentity.ts +19 -0
  22. package/packages/core/src/ssrf.ts +221 -0
  23. package/packages/core/src/testClient.ts +3 -0
  24. package/packages/core/src/websocket.ts +4 -2
  25. package/packages/frond/dist/index.js +6 -0
  26. package/packages/frond/src/engine.ts +9 -0
  27. package/packages/orm/dist/index.js +675 -320
  28. package/packages/orm/src/adapters/firebird.ts +14 -45
  29. package/packages/orm/src/adapters/mssql.ts +21 -58
  30. package/packages/orm/src/adapters/mysql.ts +10 -49
  31. package/packages/orm/src/adapters/odbc.ts +11 -42
  32. package/packages/orm/src/adapters/postgres.ts +20 -60
  33. package/packages/orm/src/adapters/sqlCrud.ts +178 -0
  34. package/packages/orm/src/adapters/sqlite.ts +46 -66
  35. package/packages/orm/src/index.ts +1 -1
  36. package/types/core/src/api.d.ts +6 -0
  37. package/types/core/src/auth.d.ts +30 -0
  38. package/types/core/src/graphql.d.ts +25 -0
  39. package/types/core/src/index.d.ts +1 -0
  40. package/types/core/src/job.d.ts +1 -0
  41. package/types/core/src/push.d.ts +5 -0
  42. package/types/core/src/queue.d.ts +7 -1
  43. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  44. package/types/core/src/queueBackends/mongoBackend.d.ts +8 -1
  45. package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
  46. package/types/core/src/sso.d.ts +1 -0
  47. package/types/core/src/ssoIdentity.d.ts +10 -0
  48. package/types/core/src/ssrf.d.ts +19 -0
  49. package/types/orm/src/adapters/sqlCrud.d.ts +90 -0
  50. package/types/orm/src/adapters/sqlite.d.ts +17 -0
  51. package/types/orm/src/index.d.ts +1 -1
@@ -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
- if (!validToken(token, secret, algorithm)) {
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 = getPayload(token);
2914
+ req2.auth = payload;
2883
2915
  next();
2884
2916
  };
2885
2917
  }
2886
2918
  function refreshToken(token, expiresIn = 60) {
2887
- if (!validToken(token)) return null;
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
- if (validToken(token, secret, algorithm)) return getPayload(token);
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 expiresAt = ttl > 0 ? Date.now() + ttl * 1e3 : 0;
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 expiresAt = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
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, [, expiresAt]] of cache) {
5483
- if (expiresAt <= now) cache.delete(key);
5516
+ for (const [key, [, expiresAt2]] of cache) {
5517
+ if (expiresAt2 <= now) cache.delete(key);
5484
5518
  }
5485
5519
  }
5486
5520
  function tokenize(source) {
@@ -5726,15 +5760,15 @@ function findOutsideQuotes(expr, needle) {
5726
5760
  }
5727
5761
  return -1;
5728
5762
  }
5729
- function splitOutsideQuotes(expr, sep8) {
5730
- if (!expr.includes(sep8)) return [expr];
5763
+ function splitOutsideQuotes(expr, sep9) {
5764
+ if (!expr.includes(sep9)) return [expr];
5731
5765
  const parts = [];
5732
5766
  let currentStart = 0;
5733
5767
  let inQuote = null;
5734
5768
  let depth = 0;
5735
5769
  let bracketDepth = 0;
5736
5770
  let i = 0;
5737
- const sepLen = sep8.length;
5771
+ const sepLen = sep9.length;
5738
5772
  const lastStart = expr.length - sepLen;
5739
5773
  while (i <= lastStart) {
5740
5774
  const ch = expr[i];
@@ -5755,7 +5789,7 @@ function splitOutsideQuotes(expr, sep8) {
5755
5789
  else if (ch === ")") depth--;
5756
5790
  else if (ch === "[") bracketDepth++;
5757
5791
  else if (ch === "]") bracketDepth--;
5758
- if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep8, i)) {
5792
+ if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep9, i)) {
5759
5793
  parts.push(expr.slice(currentStart, i));
5760
5794
  i += sepLen;
5761
5795
  currentStart = i;
@@ -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);
@@ -6702,8 +6742,8 @@ var init_engine = __esm({
6702
6742
  },
6703
6743
  first: (v) => Array.isArray(v) ? v[0] ?? null : null,
6704
6744
  last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
6705
- join: (v, sep8) => Array.isArray(v) ? v.map(String).join(sep8 !== void 0 ? String(sep8) : ", ") : String(v),
6706
- split: (v, sep8) => String(v).split(sep8 !== void 0 ? String(sep8) : " "),
6745
+ join: (v, sep9) => Array.isArray(v) ? v.map(String).join(sep9 !== void 0 ? String(sep9) : ", ") : String(v),
6746
+ split: (v, sep9) => String(v).split(sep9 !== void 0 ? String(sep9) : " "),
6707
6747
  replace: (v, from, to) => {
6708
6748
  const s = String(v);
6709
6749
  if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
@@ -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, expiresAt] = cached;
7997
- if (Date.now() < expiresAt) {
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 resolvedToken = "";
9990
+ let payload = null;
9935
9991
  let tokenSource = "";
9936
- if (headerToken && validToken(headerToken)) {
9937
- resolvedToken = headerToken;
9938
- tokenSource = "header";
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 (!resolvedToken) {
10001
+ if (!payload) {
9941
10002
  const bodyToken = req2.body?.formToken;
9942
- if (bodyToken && validToken(bodyToken)) {
9943
- resolvedToken = bodyToken;
9944
- tokenSource = "body";
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 (!resolvedToken) {
9948
- const sso = req2.session?.get?.("_tina4_sso");
9949
- const identity = sso?.identity;
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 && validToken(sessionToken)) {
9956
- resolvedToken = sessionToken;
9957
- tokenSource = "session";
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 (!resolvedToken) {
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 = getPayload(resolvedToken) ?? {};
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 expiresAt = Number(doc?.expires_at ?? 0);
11089
- if (expiresAt > 0 && Date.now() / 1e3 > expiresAt) {
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
- if (this.target === null) this.sqlite.exec(ddl);
11536
- else sqlCommandSync(this.target, ddl, []);
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 expiresAt = Number(column(row, "expires_at") ?? 0);
11549
- if (expiresAt > 0 && expiresAt < now) {
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 expiresAt = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
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, expiresAt, sessionId]
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, expiresAt]
11690
+ [sessionId, json, expiresAt2]
11576
11691
  );
11577
11692
  }
11578
11693
  }
@@ -13615,12 +13730,6 @@ function printResolution() {
13615
13730
  lines.push("");
13616
13731
  process.stderr.write(lines.join("\n"));
13617
13732
  }
13618
- function toPlural(name) {
13619
- const lower = name.toLowerCase();
13620
- if (lower.endsWith("s")) return lower;
13621
- if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
13622
- return lower + "s";
13623
- }
13624
13733
  function toCamel(name) {
13625
13734
  return name.charAt(0).toLowerCase() + name.slice(1);
13626
13735
  }
@@ -14053,7 +14162,7 @@ ${aiFill(`delete_${singular}`, {
14053
14162
  }
14054
14163
  function generateCrud(name, flags) {
14055
14164
  const table2 = resolveTable(name, flags, { announce: false });
14056
- const routeName = toPlural(table2);
14165
+ const routeName = pluralizeReserved(toSnake(name));
14057
14166
  const isPublic = Boolean(flags.public);
14058
14167
  if (!__resolution.jsonMode) console.log(`
14059
14168
  Generating CRUD for ${name}...
@@ -14307,7 +14416,7 @@ void test${titleName};
14307
14416
  function generateForm(name, flags) {
14308
14417
  const fields = fieldsOrDefault(flags.fields || "");
14309
14418
  const table2 = resolveTable(name, flags, { announce: false });
14310
- const routeName = toPlural(table2);
14419
+ const routeName = pluralizeReserved(toSnake(name));
14311
14420
  const inputTypes = {
14312
14421
  string: "text",
14313
14422
  str: "text",
@@ -14373,7 +14482,7 @@ function generateForm(name, flags) {
14373
14482
  function generateView(name, flags) {
14374
14483
  const fields = fieldsOrDefault(flags.fields || "");
14375
14484
  const table2 = resolveTable(name, flags, { announce: false });
14376
- const routeName = toPlural(table2);
14485
+ const routeName = pluralizeReserved(toSnake(name));
14377
14486
  const cols = fields.map(([f]) => f);
14378
14487
  const dir = resolve6("src/templates/pages");
14379
14488
  ensureDir(dir);
@@ -15385,7 +15494,7 @@ var init_generate = __esm({
15385
15494
  crud: ({ name, table: table2 }) => [
15386
15495
  `Apply the migration: npx tina4nodejs migrate`,
15387
15496
  `Serve and try: npx tina4nodejs serve -> visit /swagger`,
15388
- `Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
15497
+ `Run the gate test: npx tsx tests/${pluralizeReserved(toSnake(name))}.test.ts`,
15389
15498
  `Change fields: edit src/models/${name}.ts then re-run generate crud`
15390
15499
  ],
15391
15500
  migration: () => [
@@ -15403,10 +15512,10 @@ var init_generate = __esm({
15403
15512
  ],
15404
15513
  form: ({ name, table: table2 }) => [
15405
15514
  `Render from a route: res.render("forms/${table2}.twig", { item })`,
15406
- `Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
15515
+ `Add the POST route: npx tina4nodejs generate route ${pluralizeReserved(toSnake(name))} --model ${name}`
15407
15516
  ],
15408
- view: ({ table: table2 }) => [
15409
- `Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
15517
+ view: ({ name, table: table2 }) => [
15518
+ `Wire routes to render list -> ${pluralizeReserved(toSnake(name))}.twig, detail -> ${table2}.twig`,
15410
15519
  `Customize the templates in src/templates/pages/`
15411
15520
  ],
15412
15521
  auth: () => [
@@ -15567,7 +15676,7 @@ function envTruthy(val) {
15567
15676
  return ["true", "1", "yes", "on"].includes(val.trim().toLowerCase());
15568
15677
  }
15569
15678
  function isLoopback(ip) {
15570
- if (ip == null || ip === "") return true;
15679
+ if (ip == null || ip === "") return false;
15571
15680
  let addr = ip.trim().toLowerCase();
15572
15681
  if (addr.startsWith("::ffff:")) addr = addr.slice(7);
15573
15682
  return addr === "::1" || addr === "localhost" || addr.startsWith("127.");
@@ -17158,6 +17267,7 @@ __export(graphql_exports, {
17158
17267
  graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
17159
17268
  graphqlEndpoint: () => graphqlEndpoint,
17160
17269
  graphqlMaxDepth: () => graphqlMaxDepth,
17270
+ graphqlMaxNodes: () => graphqlMaxNodes,
17161
17271
  tokenize: () => tokenize2
17162
17272
  });
17163
17273
  function tokenize2(source) {
@@ -17197,7 +17307,12 @@ function graphqlMaxDepth() {
17197
17307
  const n = parseInt(raw, 10);
17198
17308
  return Number.isNaN(n) ? 50 : n;
17199
17309
  }
17200
- var TOKEN_PATTERNS, ParseError, Parser, GraphQL, GraphQLType;
17310
+ function graphqlMaxNodes() {
17311
+ const raw = (process.env.TINA4_GRAPHQL_MAX_NODES ?? "1000").trim();
17312
+ const n = parseInt(raw, 10);
17313
+ return Number.isNaN(n) ? 1e3 : n;
17314
+ }
17315
+ var TOKEN_PATTERNS, ParseError, Parser, ComplexityError, GraphQL, GraphQLType;
17201
17316
  var init_graphql = __esm({
17202
17317
  "../core/src/graphql.ts"() {
17203
17318
  "use strict";
@@ -17236,9 +17351,16 @@ var init_graphql = __esm({
17236
17351
  Parser = class {
17237
17352
  tokens;
17238
17353
  pos;
17354
+ // F2: bound the parser's own recursion. A deeply nested query
17355
+ // ("{a{a{a...}}}") otherwise recurses in parseSelectionSet until the call
17356
+ // stack overflows — BEFORE the execution-time depth guard can run. Reusing
17357
+ // TINA4_GRAPHQL_MAX_DEPTH keeps parse and execution on one bound.
17358
+ depth = 0;
17359
+ maxDepth;
17239
17360
  constructor(tokens) {
17240
17361
  this.tokens = tokens;
17241
17362
  this.pos = 0;
17363
+ this.maxDepth = graphqlMaxDepth();
17242
17364
  }
17243
17365
  peek() {
17244
17366
  return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
@@ -17320,6 +17442,17 @@ var init_graphql = __esm({
17320
17442
  };
17321
17443
  }
17322
17444
  parseSelectionSet() {
17445
+ this.depth++;
17446
+ if (this.maxDepth > 0 && this.depth > this.maxDepth) {
17447
+ throw new Error(`Query exceeds maximum depth of ${this.maxDepth}`);
17448
+ }
17449
+ try {
17450
+ return this.parseSelectionSetInner();
17451
+ } finally {
17452
+ this.depth--;
17453
+ }
17454
+ }
17455
+ parseSelectionSetInner() {
17323
17456
  this.expect("LBRACE");
17324
17457
  const selections = [];
17325
17458
  while (!this.match("RBRACE")) {
@@ -17474,6 +17607,8 @@ var init_graphql = __esm({
17474
17607
  return t;
17475
17608
  }
17476
17609
  };
17610
+ ComplexityError = class extends Error {
17611
+ };
17477
17612
  GraphQL = class _GraphQL {
17478
17613
  types = /* @__PURE__ */ new Map();
17479
17614
  queries = /* @__PURE__ */ new Map();
@@ -17496,6 +17631,12 @@ var init_graphql = __esm({
17496
17631
  * spread / inline fragment, so circular fragments are caught too.
17497
17632
  */
17498
17633
  maxDepth = graphqlMaxDepth();
17634
+ /**
17635
+ * Maximum expanded selection nodes per query (from `TINA4_GRAPHQL_MAX_NODES`,
17636
+ * default 1000; `<= 0` disables). Bounds query WIDTH — fragment bombs and
17637
+ * alias explosions — which the depth guard does not. See {@link graphqlMaxNodes}.
17638
+ */
17639
+ maxNodes = graphqlMaxNodes();
17499
17640
  /**
17500
17641
  * Decorator-style resolver registration.
17501
17642
  *
@@ -17643,6 +17784,16 @@ var init_graphql = __esm({
17643
17784
  return { data: null, errors: [{ message: "No operation found" }] };
17644
17785
  }
17645
17786
  const op = operations[0];
17787
+ if (this.maxNodes > 0) {
17788
+ try {
17789
+ this.assertComplexity(op.selections, fragments);
17790
+ } catch (e) {
17791
+ if (e instanceof ComplexityError) {
17792
+ return { data: null, errors: [{ message: e.message }] };
17793
+ }
17794
+ throw e;
17795
+ }
17796
+ }
17646
17797
  const resolvers = op.operation === "query" ? this.queries : this.mutations;
17647
17798
  for (const vdef of op.variables) {
17648
17799
  if (!(vdef.name in vars) && vdef.default !== void 0) {
@@ -17668,6 +17819,36 @@ var init_graphql = __esm({
17668
17819
  * instead of recursing until the interpreter stack overflows. Top-level
17669
17820
  * starts at depth 1; `maxDepth <= 0` disables the guard.
17670
17821
  */
17822
+ /**
17823
+ * Count the expanded selection nodes, throwing {@link ComplexityError} past
17824
+ * `maxNodes` (F2). Fragment spreads are expanded (so a fragment bomb is
17825
+ * counted, not the small unexpanded document) and every field — including
17826
+ * each alias — counts once. The count is capped by `maxNodes` and the walk by
17827
+ * `maxDepth`, so a circular fragment trips the budget instead of looping and
17828
+ * the check itself can never cost more than the budget.
17829
+ */
17830
+ assertComplexity(selections, fragments) {
17831
+ const depthCap = this.maxDepth > 0 ? this.maxDepth : 1e3;
17832
+ let count = 0;
17833
+ const walk2 = (sels, depth) => {
17834
+ if (depth > depthCap) return;
17835
+ for (const sel of sels) {
17836
+ count++;
17837
+ if (count > this.maxNodes) {
17838
+ throw new ComplexityError(`Query exceeds maximum complexity of ${this.maxNodes} nodes`);
17839
+ }
17840
+ if (sel.kind === "fragment_spread") {
17841
+ const frag = fragments.get(sel.name);
17842
+ if (frag) walk2(frag.selections, depth + 1);
17843
+ } else if (sel.kind === "inline_fragment") {
17844
+ walk2(sel.selections, depth + 1);
17845
+ } else if (sel.kind === "field" && sel.selections) {
17846
+ walk2(sel.selections, depth + 1);
17847
+ }
17848
+ }
17849
+ };
17850
+ walk2(selections, 1);
17851
+ }
17671
17852
  async resolveSelectionsInto(selections, resolvers, parent, variables, context, fragments, target, depth) {
17672
17853
  const errors = [];
17673
17854
  if (this.maxDepth > 0 && depth > this.maxDepth) {
@@ -18870,7 +19051,8 @@ function wsAuthorized(route, headers, queryString = "", subprotocol = "") {
18870
19051
  const token = wsToken(headers, queryString, subprotocol);
18871
19052
  if (!token) return [null, false];
18872
19053
  const payload = validToken(token);
18873
- return [payload, payload !== null];
19054
+ if (!isIdentityPayload(payload)) return [null, false];
19055
+ return [payload, true];
18874
19056
  }
18875
19057
  function offeredBearerSubprotocol(subprotocol) {
18876
19058
  return subprotocol.split(",").some((p) => p.trim().toLowerCase() === "bearer");
@@ -20065,12 +20247,36 @@ var init_mongoBackend = __esm({
20065
20247
  }
20066
20248
  }
20067
20249
  else if (operation === "size") {
20068
- const count = await col.countDocuments({
20069
- queue: queueName,
20070
- status: "pending",
20071
- });
20250
+ // ADR-0022 dec 7: size(status) must not answer a different question.
20251
+ // The dead aliases count the .dead_letter store (== deadLetters().length);
20252
+ // reserved/completed count by status; else pending. data = status.
20253
+ const status = data || "pending";
20254
+ let count: number;
20255
+ if (status === "dead" || status === "failed" || status === "dead_letter") {
20256
+ count = await col.countDocuments({ queue: queueName + ".dead_letter" });
20257
+ } else if (status === "reserved" || status === "completed") {
20258
+ count = await col.countDocuments({ queue: queueName, status });
20259
+ } else {
20260
+ count = await col.countDocuments({ queue: queueName, status: "pending" });
20261
+ }
20072
20262
  process.stdout.write(String(count));
20073
20263
  }
20264
+ else if (operation === "reject") {
20265
+ // Dead-letter NOW, no retry (ADR-0023). Floor attempts at maxRetries
20266
+ // so it reads as exhausted (parity with fail()'s dead-letter branch
20267
+ // and size("dead")). data = JSON { id, reason, maxRetries }.
20268
+ const info = JSON.parse(data);
20269
+ const doc = await col.findOne({ queue: queueName, id: info.id });
20270
+ if (doc) {
20271
+ const attempts = Math.max((doc.attempts || 0) + 1, info.maxRetries);
20272
+ await col.insertOne({
20273
+ ...doc, _id: undefined, attempts,
20274
+ status: "dead", queue: queueName + ".dead_letter", error: info.reason,
20275
+ });
20276
+ await col.deleteOne({ _id: doc._id, queue: queueName });
20277
+ }
20278
+ process.stdout.write("__OK__");
20279
+ }
20074
20280
  else if (operation === "clear") {
20075
20281
  await col.deleteMany({ queue: queueName });
20076
20282
  process.stdout.write("__CLEARED__");
@@ -20306,11 +20512,20 @@ var init_mongoBackend = __esm({
20306
20512
  return null;
20307
20513
  }
20308
20514
  }
20309
- size(queue) {
20310
- const result = this.execSync("size", queue);
20515
+ size(queue, status2 = "pending") {
20516
+ const result = this.execSync("size", queue, status2);
20311
20517
  const num = parseInt(result, 10);
20312
20518
  return isNaN(num) ? 0 : num;
20313
20519
  }
20520
+ /**
20521
+ * Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
20522
+ * fail(): fail() requeues until maxRetries is spent. attempts is floored at
20523
+ * maxRetries so deadLetters()/size("dead") (which filter/scope to the
20524
+ * dead-letter store) see it.
20525
+ */
20526
+ reject(queue, id, reason, maxRetries) {
20527
+ this.execSync("reject", queue, JSON.stringify({ id, reason, maxRetries }));
20528
+ }
20314
20529
  clear(queue) {
20315
20530
  this.execSync("clear", queue);
20316
20531
  }
@@ -20405,7 +20620,9 @@ function createJob(data, queue) {
20405
20620
  queue._failJob(job.topic, job, reason, queue.getMaxRetries());
20406
20621
  },
20407
20622
  reject(reason = "") {
20408
- job.fail(reason);
20623
+ job.status = "dead";
20624
+ job.error = reason;
20625
+ queue._rejectJob(job.topic, job, reason, queue.getMaxRetries());
20409
20626
  },
20410
20627
  retry(delaySeconds) {
20411
20628
  queue._retryJob(job.topic, job, delaySeconds);
@@ -21014,6 +21231,20 @@ var init_liteBackend = __esm({
21014
21231
  * dead-letter file -- two spellings of the same intent that previously
21015
21232
  * diverged.
21016
21233
  */
21234
+ /**
21235
+ * Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
21236
+ *
21237
+ * Unlike failJob(), which requeues until maxRetries is spent, rejectJob()
21238
+ * moves a known-poison job straight to the dead-letter store. attempts is
21239
+ * floored at maxRetries so deadLetters() (attempts >= maxRetries) returns it
21240
+ * and size("dead") agrees.
21241
+ */
21242
+ rejectJob(queue, job, reason, maxRetries) {
21243
+ this.clearReservation(queue, job.id);
21244
+ job.attempts = Math.max((job.attempts || 0) + 1, maxRetries);
21245
+ job.error = reason;
21246
+ this.deadLetter(queue, job, reason);
21247
+ }
21017
21248
  retryJob(queue, job, delaySeconds) {
21018
21249
  this.clearReservation(queue, job.id);
21019
21250
  try {
@@ -21235,7 +21466,7 @@ var init_queue = __esm({
21235
21466
  size(status2 = "pending") {
21236
21467
  const q = this.topic;
21237
21468
  if (this.externalBackend) {
21238
- return this.externalBackend.size(q);
21469
+ return this.externalBackend.size(q, status2);
21239
21470
  }
21240
21471
  return this.liteBackend.size(q, status2);
21241
21472
  }
@@ -21503,6 +21734,17 @@ var init_queue = __esm({
21503
21734
  }
21504
21735
  this.liteBackend.failJob(queue, job, error2, maxRetries, this._retryBackoff);
21505
21736
  }
21737
+ /**
21738
+ * Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
21739
+ * _failJob, which retries until maxRetries is spent.
21740
+ */
21741
+ _rejectJob(queue, job, reason, maxRetries) {
21742
+ if (this.externalBackend?.reject) {
21743
+ this.externalBackend.reject(queue, job.id, reason, maxRetries);
21744
+ return;
21745
+ }
21746
+ this.liteBackend.rejectJob(queue, job, reason, maxRetries);
21747
+ }
21506
21748
  /**
21507
21749
  * Re-queue a job back to the main queue directory with incremented attempts.
21508
21750
  */
@@ -27214,13 +27456,20 @@ var sso_exports = {};
27214
27456
  __export(sso_exports, {
27215
27457
  SSO: () => Sso,
27216
27458
  Sso: () => Sso,
27217
- SsoError: () => SsoError
27459
+ SsoError: () => SsoError,
27460
+ liveSessionIdentity: () => liveSessionIdentity
27218
27461
  });
27219
27462
  import { createHash as createHash9, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
27463
+ function expiresAt(tokens) {
27464
+ const lifetime = Number(tokens.expires_in ?? 0);
27465
+ return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
27466
+ }
27220
27467
  var SsoError, Sso;
27221
27468
  var init_sso = __esm({
27222
27469
  "../core/src/sso.ts"() {
27223
27470
  "use strict";
27471
+ init_ssoIdentity();
27472
+ init_ssoIdentity();
27224
27473
  SsoError = class extends Error {
27225
27474
  };
27226
27475
  Sso = class _Sso {
@@ -27426,13 +27675,12 @@ var init_sso = __esm({
27426
27675
  access_token: tokens.access_token,
27427
27676
  refresh_token: tokens.refresh_token,
27428
27677
  id_token: tokens.id_token,
27429
- expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
27678
+ expires_at: expiresAt(tokens)
27430
27679
  });
27431
27680
  return { identity, return_to: _Sso.safeReturn(pending.return_to) };
27432
27681
  }
27433
27682
  identity(requestOrSession) {
27434
- const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
27435
- const identity = stored?.identity ?? null;
27683
+ const identity = liveSessionIdentity(this.session(requestOrSession)?.get(_Sso.SESSION_KEY));
27436
27684
  if (identity && requestOrSession?.session) requestOrSession.user = identity;
27437
27685
  return identity;
27438
27686
  }
@@ -27459,7 +27707,7 @@ var init_sso = __esm({
27459
27707
  access_token: tokens.access_token,
27460
27708
  refresh_token: tokens.refresh_token ?? stored.refresh_token,
27461
27709
  id_token: tokens.id_token ?? stored.id_token,
27462
- expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
27710
+ expires_at: expiresAt(tokens)
27463
27711
  });
27464
27712
  return identity;
27465
27713
  } catch (error2) {
@@ -27551,8 +27799,8 @@ function writeMcpDiscovery(projectRoot3, port) {
27551
27799
  const lines = contents.split(/\r?\n/);
27552
27800
  const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
27553
27801
  if (!already) {
27554
- const sep8 = contents.endsWith("\n") || contents === "" ? "" : "\n";
27555
- fs9.writeFileSync(gitignorePath, `${contents}${sep8}${GITIGNORE_LINE}
27802
+ const sep9 = contents.endsWith("\n") || contents === "" ? "" : "\n";
27803
+ fs9.writeFileSync(gitignorePath, `${contents}${sep9}${GITIGNORE_LINE}
27556
27804
  `, "utf-8");
27557
27805
  }
27558
27806
  }
@@ -27601,12 +27849,12 @@ function bannerSurfaceLines(port, opts) {
27601
27849
  ];
27602
27850
  }
27603
27851
  function swaggerAdvertised() {
27604
- const TRUTHY2 = ["true", "1", "yes", "on"];
27852
+ const TRUTHY3 = ["true", "1", "yes", "on"];
27605
27853
  const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
27606
27854
  if (raw === "") {
27607
- return TRUTHY2.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
27855
+ return TRUTHY3.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
27608
27856
  }
27609
- return TRUTHY2.includes(raw);
27857
+ return TRUTHY3.includes(raw);
27610
27858
  }
27611
27859
  async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
27612
27860
  const dir = resolve15(base, migrationDir);
@@ -28525,8 +28773,9 @@ function wireLocaleGlobal(frondEngine, base) {
28525
28773
  async function startServer(config) {
28526
28774
  loadEnv(".env.local");
28527
28775
  loadEnv();
28528
- const { ensureDevSecret: ensureDevSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
28776
+ const { ensureDevSecret: ensureDevSecret2, requireBootSecret: requireBootSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
28529
28777
  ensureDevSecret2();
28778
+ requireBootSecret2();
28530
28779
  _checkLegacyEnvVars();
28531
28780
  const resolved = resolvePortAndHost(config);
28532
28781
  const host = resolved.host;
@@ -30722,6 +30971,178 @@ var init_service = __esm({
30722
30971
  }
30723
30972
  });
30724
30973
 
30974
+ // ../core/src/ssrf.ts
30975
+ import { isIP } from "node:net";
30976
+ import { lookup } from "node:dns/promises";
30977
+ function v4ToBytes(ip) {
30978
+ const parts = ip.split(".");
30979
+ if (parts.length !== 4) return null;
30980
+ const bytes = new Uint8Array(4);
30981
+ for (let i = 0; i < 4; i++) {
30982
+ const n = Number(parts[i]);
30983
+ if (!Number.isInteger(n) || n < 0 || n > 255 || !/^\d+$/.test(parts[i])) return null;
30984
+ bytes[i] = n;
30985
+ }
30986
+ return bytes;
30987
+ }
30988
+ function v6ToBytes(input) {
30989
+ let ip = input.split("%")[0];
30990
+ const lastColon = ip.lastIndexOf(":");
30991
+ const tail = ip.slice(lastColon + 1);
30992
+ if (tail.includes(".")) {
30993
+ const v4 = v4ToBytes(tail);
30994
+ if (!v4) return null;
30995
+ const hextetA = (v4[0] << 8 | v4[1]).toString(16);
30996
+ const hextetB = (v4[2] << 8 | v4[3]).toString(16);
30997
+ ip = ip.slice(0, lastColon + 1) + hextetA + ":" + hextetB;
30998
+ }
30999
+ const halves = ip.split("::");
31000
+ if (halves.length > 2) return null;
31001
+ const head = halves[0] ? halves[0].split(":") : [];
31002
+ const back = halves.length === 2 ? halves[1] ? halves[1].split(":") : [] : [];
31003
+ let groups;
31004
+ if (halves.length === 1) {
31005
+ if (head.length !== 8) return null;
31006
+ groups = head;
31007
+ } else {
31008
+ const missing = 8 - (head.length + back.length);
31009
+ if (missing < 0) return null;
31010
+ groups = [...head, ...Array(missing).fill("0"), ...back];
31011
+ }
31012
+ if (groups.length !== 8) return null;
31013
+ const bytes = new Uint8Array(16);
31014
+ for (let i = 0; i < 8; i++) {
31015
+ if (!/^[0-9a-fA-F]{1,4}$/.test(groups[i])) return null;
31016
+ const g = parseInt(groups[i], 16);
31017
+ bytes[i * 2] = g >> 8 & 255;
31018
+ bytes[i * 2 + 1] = g & 255;
31019
+ }
31020
+ return bytes;
31021
+ }
31022
+ function ipToBytes(ip) {
31023
+ const cleaned = ip.split("%")[0];
31024
+ const family = isIP(cleaned);
31025
+ if (family === 4) return v4ToBytes(cleaned);
31026
+ if (family === 6) {
31027
+ const bytes = v6ToBytes(cleaned);
31028
+ if (!bytes) return null;
31029
+ const isMapped = bytes.subarray(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
31030
+ return isMapped ? bytes.subarray(12) : bytes;
31031
+ }
31032
+ return null;
31033
+ }
31034
+ function parseCidr(cidr) {
31035
+ const [netStr, bitsStr] = cidr.split("/");
31036
+ const net4 = ipToBytes(netStr);
31037
+ if (!net4) return null;
31038
+ return { net: net4, bits: Number(bitsStr) };
31039
+ }
31040
+ function inCidr(packed, cidr) {
31041
+ if (packed.length !== cidr.net.length) return false;
31042
+ const whole = Math.floor(cidr.bits / 8);
31043
+ const remainder = cidr.bits % 8;
31044
+ for (let i = 0; i < whole; i++) {
31045
+ if (packed[i] !== cidr.net[i]) return false;
31046
+ }
31047
+ if (remainder === 0) return true;
31048
+ const mask = 255 << 8 - remainder & 255;
31049
+ return (packed[whole] & mask) === (cidr.net[whole] & mask);
31050
+ }
31051
+ function allowPrivateRequests() {
31052
+ return TRUTHY2.has((process.env[ALLOW_PRIVATE_ENV] ?? "").trim().toLowerCase());
31053
+ }
31054
+ function isBlockedAddress(ip) {
31055
+ const packed = ipToBytes(ip);
31056
+ if (!packed) return true;
31057
+ return BLOCKED.some((cidr) => inCidr(packed, cidr));
31058
+ }
31059
+ function matchesAllowList(host, port, resolved, allowHosts) {
31060
+ const hostLower = host.toLowerCase();
31061
+ for (const raw of allowHosts ?? []) {
31062
+ const entry = String(raw).trim().toLowerCase();
31063
+ if (!entry) continue;
31064
+ if (entry === hostLower || entry === `${hostLower}:${port}`) return true;
31065
+ if (entry.includes("/")) {
31066
+ const cidr = parseCidr(entry);
31067
+ if (cidr && resolved.some((ip) => {
31068
+ const packed = ipToBytes(ip);
31069
+ return packed !== null && inCidr(packed, cidr);
31070
+ })) {
31071
+ return true;
31072
+ }
31073
+ }
31074
+ }
31075
+ return false;
31076
+ }
31077
+ function logBlock(host, ip) {
31078
+ void Promise.resolve().then(() => (init_logger(), logger_exports)).then((mod) => mod.Log?.warning?.(`SSRF guard blocked outbound request to ${host} (${ip})`)).catch(() => void 0);
31079
+ }
31080
+ async function guardUrl(url, allowHosts) {
31081
+ let parsed;
31082
+ try {
31083
+ parsed = new URL(url);
31084
+ } catch {
31085
+ throw new SsrfError(`Blocked request: '${url}' is not a valid URL`);
31086
+ }
31087
+ const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
31088
+ if (scheme !== "http" && scheme !== "https") {
31089
+ throw new SsrfError(`Blocked request: URL scheme '${scheme || "(none)"}' is not http or https`);
31090
+ }
31091
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
31092
+ if (!host) {
31093
+ throw new SsrfError("Blocked request: URL has no host");
31094
+ }
31095
+ const port = parsed.port ? Number(parsed.port) : scheme === "https" ? 443 : 80;
31096
+ if (allowPrivateRequests()) return;
31097
+ let resolved;
31098
+ try {
31099
+ const results = await lookup(host, { all: true });
31100
+ resolved = results.map((r) => r.address);
31101
+ } catch {
31102
+ throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
31103
+ }
31104
+ if (resolved.length === 0) {
31105
+ throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
31106
+ }
31107
+ if (matchesAllowList(host, port, resolved, allowHosts)) return;
31108
+ for (const ip of resolved) {
31109
+ if (isBlockedAddress(ip)) {
31110
+ logBlock(host, ip);
31111
+ throw new SsrfError(
31112
+ `Blocked request to private/internal address ${ip} (host ${host}): set ${ALLOW_PRIVATE_ENV}=true to allow, or pass an allow-list.`
31113
+ );
31114
+ }
31115
+ }
31116
+ }
31117
+ var ALLOW_PRIVATE_ENV, TRUTHY2, SsrfError, BLOCKED_CIDRS, BLOCKED;
31118
+ var init_ssrf = __esm({
31119
+ "../core/src/ssrf.ts"() {
31120
+ "use strict";
31121
+ ALLOW_PRIVATE_ENV = "TINA4_ALLOW_PRIVATE_REQUESTS";
31122
+ TRUTHY2 = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
31123
+ SsrfError = class extends Error {
31124
+ constructor(message) {
31125
+ super(message);
31126
+ this.name = "SsrfError";
31127
+ }
31128
+ };
31129
+ BLOCKED_CIDRS = [
31130
+ "0.0.0.0/8",
31131
+ "10.0.0.0/8",
31132
+ "127.0.0.0/8",
31133
+ "169.254.0.0/16",
31134
+ "172.16.0.0/12",
31135
+ "192.168.0.0/16",
31136
+ "100.64.0.0/10",
31137
+ "::1/128",
31138
+ "::/128",
31139
+ "fc00::/7",
31140
+ "fe80::/10"
31141
+ ];
31142
+ BLOCKED = BLOCKED_CIDRS.map(parseCidr).filter((c) => c !== null);
31143
+ }
31144
+ });
31145
+
30725
31146
  // ../core/src/api.ts
30726
31147
  import http from "node:http";
30727
31148
  import https from "node:https";
@@ -30878,6 +31299,7 @@ var init_api = __esm({
30878
31299
  "../core/src/api.ts"() {
30879
31300
  "use strict";
30880
31301
  init_version();
31302
+ init_ssrf();
30881
31303
  ApiStreamError = class extends Error {
30882
31304
  constructor(message, status2 = null) {
30883
31305
  super(message);
@@ -30932,6 +31354,7 @@ var init_api = __esm({
30932
31354
  transportFn;
30933
31355
  cookiesEnabled;
30934
31356
  cookies;
31357
+ allowHosts;
30935
31358
  /**
30936
31359
  * Construct an Api client.
30937
31360
  *
@@ -30971,6 +31394,7 @@ var init_api = __esm({
30971
31394
  this.transportFn = void 0;
30972
31395
  this.cookiesEnabled = false;
30973
31396
  this.cookies = {};
31397
+ this.allowHosts = [];
30974
31398
  if (typeof authHeaderOrOptions === "object" && authHeaderOrOptions !== null) {
30975
31399
  const opts = authHeaderOrOptions;
30976
31400
  this.authHeader = opts.authHeader ?? "";
@@ -30980,6 +31404,7 @@ var init_api = __esm({
30980
31404
  this.retryBackoff = opts.retryBackoff ?? 0.5;
30981
31405
  this.transportFn = opts.transport;
30982
31406
  this.cookiesEnabled = opts.cookies ?? false;
31407
+ this.allowHosts = opts.allowHosts ?? [];
30983
31408
  if (opts.bearerToken != null) {
30984
31409
  this.setBearerToken(opts.bearerToken);
30985
31410
  } else if (opts.username != null && opts.password != null) {
@@ -31293,7 +31718,12 @@ var init_api = __esm({
31293
31718
  * body-phase timeout is applied by the caller (streamBytes) via
31294
31719
  * `res.destroy()`.
31295
31720
  */
31296
- openStreamRequest(method, url, headers, data, connectSec) {
31721
+ async openStreamRequest(method, url, headers, data, connectSec) {
31722
+ try {
31723
+ await guardUrl(url, this.allowHosts);
31724
+ } catch (err) {
31725
+ throw new ApiStreamError(err instanceof Error ? err.message : String(err), null);
31726
+ }
31297
31727
  return new Promise((resolve21, reject) => {
31298
31728
  let parsed;
31299
31729
  try {
@@ -31352,10 +31782,11 @@ var init_api = __esm({
31352
31782
  */
31353
31783
  buildRequest(method, contentType, body, extraHeaders, targetUrl) {
31354
31784
  const headers = { "User-Agent": `Tina4/${TINA4_VERSION}`, ...this.headers };
31355
- if (this.authHeader) {
31785
+ const sameOriginAsBase = targetUrl === void 0 || sameOrigin(targetUrl, this.baseUrl);
31786
+ if (this.authHeader && sameOriginAsBase) {
31356
31787
  headers["Authorization"] = this.authHeader;
31357
31788
  }
31358
- if (this.cookiesEnabled) {
31789
+ if (this.cookiesEnabled && sameOriginAsBase) {
31359
31790
  const cookieHeader = this.cookieHeader();
31360
31791
  if (cookieHeader) {
31361
31792
  headers["Cookie"] = cookieHeader;
@@ -31456,7 +31887,12 @@ var init_api = __esm({
31456
31887
  * bearer token / session cookie never leaks to a host you didn't
31457
31888
  * authenticate to.
31458
31889
  */
31459
- performRequest(method, url, headers, data, redirectsLeft) {
31890
+ async performRequest(method, url, headers, data, redirectsLeft) {
31891
+ try {
31892
+ await guardUrl(url, this.allowHosts);
31893
+ } catch (err) {
31894
+ return { kind: "error", error: err instanceof Error ? err.message : String(err) };
31895
+ }
31460
31896
  return new Promise((resolve21) => {
31461
31897
  let parsed;
31462
31898
  try {
@@ -34196,6 +34632,7 @@ var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
34196
34632
  var init_push = __esm({
34197
34633
  "../core/src/push.ts"() {
34198
34634
  "use strict";
34635
+ init_ssrf();
34199
34636
  CURVE = "prime256v1";
34200
34637
  RECORD_SIZE = 4096;
34201
34638
  MAX_PAYLOAD = RECORD_SIZE - 17;
@@ -34270,10 +34707,19 @@ var init_push = __esm({
34270
34707
  return { rawPublic, rawPrivate };
34271
34708
  }
34272
34709
  async deliver(endpoint, config, keys, body) {
34710
+ try {
34711
+ await guardUrl(endpoint.toString(), this.options.allowHosts);
34712
+ } catch (error2) {
34713
+ if (error2 instanceof SsrfError) throw new PushError(error2.message);
34714
+ throw error2;
34715
+ }
34273
34716
  let response;
34274
34717
  try {
34275
34718
  response = await fetch(endpoint, {
34276
34719
  method: "POST",
34720
+ // A real push service answers the POST directly; a redirect from a push
34721
+ // endpoint is not followed to a private address (ADR-0084).
34722
+ redirect: "manual",
34277
34723
  headers: {
34278
34724
  Authorization: `vapid t=${vapidToken(endpoint.toString(), config.subject, keys.rawPrivate, keys.rawPublic)}, k=${config.publicKey}`,
34279
34725
  "Content-Encoding": "aes128gcm",
@@ -36360,6 +36806,7 @@ var init_testClient = __esm({
36360
36806
  reqHeaders["content-length"] = String(Buffer.byteLength(rawBody));
36361
36807
  }
36362
36808
  const socket = new Socket();
36809
+ Object.defineProperty(socket, "remoteAddress", { value: "127.0.0.1" });
36363
36810
  const rawReq = new IncomingMessage2(socket);
36364
36811
  rawReq.method = method.toUpperCase();
36365
36812
  rawReq.url = path10;
@@ -36732,6 +37179,7 @@ var init_validator = __esm({
36732
37179
  var src_exports2 = {};
36733
37180
  __export(src_exports2, {
36734
37181
  AI_TOOLS: () => AI_TOOLS,
37182
+ ALLOW_PRIVATE_ENV: () => ALLOW_PRIVATE_ENV,
36735
37183
  APPLICATION_FORM: () => APPLICATION_FORM,
36736
37184
  APPLICATION_JSON: () => APPLICATION_JSON,
36737
37185
  APPLICATION_OCTET: () => APPLICATION_OCTET,
@@ -36839,6 +37287,7 @@ __export(src_exports2, {
36839
37287
  Session: () => Session,
36840
37288
  Sso: () => Sso,
36841
37289
  SsoError: () => SsoError,
37290
+ SsrfError: () => SsrfError,
36842
37291
  TAKEOVER_KILLED: () => TAKEOVER_KILLED,
36843
37292
  TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
36844
37293
  TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
@@ -36872,6 +37321,7 @@ __export(src_exports2, {
36872
37321
  addHtmlHelpers: () => addHtmlHelpers,
36873
37322
  algorithmAvailable: () => algorithmAvailable,
36874
37323
  allEnv: () => allEnv,
37324
+ allowPrivateRequests: () => allowPrivateRequests,
36875
37325
  any: () => any,
36876
37326
  attachCsrfFromEnv: () => attachCsrfFromEnv,
36877
37327
  authMiddleware: () => authMiddleware,
@@ -36937,6 +37387,7 @@ __export(src_exports2, {
36937
37387
  graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
36938
37388
  graphqlEndpoint: () => graphqlEndpoint,
36939
37389
  graphqlMaxDepth: () => graphqlMaxDepth,
37390
+ guardUrl: () => guardUrl,
36940
37391
  handle: () => handle,
36941
37392
  handleFeedbackTurn: () => handleFeedbackTurn,
36942
37393
  handleFeedbackWidgetJs: () => handleFeedbackWidgetJs,
@@ -36950,6 +37401,7 @@ __export(src_exports2, {
36950
37401
  installAll: () => installAll,
36951
37402
  installSelected: () => installSelected,
36952
37403
  isBannerSuppressed: () => isBannerSuppressed,
37404
+ isBlockedAddress: () => isBlockedAddress,
36953
37405
  isDebugMode: () => isDebugMode,
36954
37406
  isDev: () => isDev,
36955
37407
  isInstalled: () => isInstalled,
@@ -37069,6 +37521,7 @@ var init_src2 = __esm({
37069
37521
  init_service();
37070
37522
  init_cache();
37071
37523
  init_api();
37524
+ init_ssrf();
37072
37525
  init_context();
37073
37526
  init_events();
37074
37527
  init_devAdmin();
@@ -37675,14 +38128,72 @@ var init_cachedDatabase = __esm({
37675
38128
  }
37676
38129
  });
37677
38130
 
38131
+ // src/adapters/sqlCrud.ts
38132
+ function buildInsertRow(dialect, table2, data, suffix = "", options = {}) {
38133
+ const keys = Object.keys(data);
38134
+ const sql = buildInsert(dialect, table2, keys, suffix, options.startAt ?? 1);
38135
+ return { sql, values: Object.values(data) };
38136
+ }
38137
+ function buildInsertRows(dialect, table2, rows) {
38138
+ if (rows.length === 0) return null;
38139
+ const keys = Object.keys(rows[0]);
38140
+ const sql = buildInsert(dialect, table2, keys);
38141
+ const paramsList = rows.map((row) => keys.map((k) => row[k]));
38142
+ return { sql, paramsList };
38143
+ }
38144
+ function buildUpdate(dialect, table2, data, filter, params, options = {}) {
38145
+ const startAt = options.startAt ?? 1;
38146
+ const convertFragment = options.convertFragment ?? identityFragment;
38147
+ const dataKeys = Object.keys(data);
38148
+ const setClauses = buildSetClause(dialect, dataKeys, startAt);
38149
+ const whereStart = startAt + dataKeys.length;
38150
+ if (typeof filter === "string") {
38151
+ const where = filter ? ` WHERE ${convertFragment(filter, whereStart)}` : "";
38152
+ return {
38153
+ sql: `UPDATE ${dialect.quote(table2)} SET ${setClauses}${where}`,
38154
+ values: [...Object.values(data), ...params ?? []]
38155
+ };
38156
+ }
38157
+ const whereClauses = buildWhereClause(dialect, Object.keys(filter), whereStart);
38158
+ return {
38159
+ sql: `UPDATE ${dialect.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`,
38160
+ values: [...Object.values(data), ...Object.values(filter)]
38161
+ };
38162
+ }
38163
+ function buildDelete(dialect, table2, filter, params, options = {}) {
38164
+ const startAt = options.startAt ?? 1;
38165
+ const convertFragment = options.convertFragment ?? identityFragment;
38166
+ if (typeof filter === "string") {
38167
+ const where = filter ? ` WHERE ${convertFragment(filter, startAt)}` : "";
38168
+ return {
38169
+ sql: `DELETE FROM ${dialect.quote(table2)}${where}`,
38170
+ values: params ?? []
38171
+ };
38172
+ }
38173
+ const whereClauses = buildWhereClause(dialect, Object.keys(filter), startAt);
38174
+ return {
38175
+ sql: `DELETE FROM ${dialect.quote(table2)} WHERE ${whereClauses}`,
38176
+ values: Object.values(filter)
38177
+ };
38178
+ }
38179
+ var identityFragment;
38180
+ var init_sqlCrud = __esm({
38181
+ "src/adapters/sqlCrud.ts"() {
38182
+ "use strict";
38183
+ init_sqlDialect();
38184
+ identityFragment = (fragment) => fragment;
38185
+ }
38186
+ });
38187
+
37678
38188
  // src/adapters/sqlite.ts
37679
38189
  var sqlite_exports = {};
37680
38190
  __export(sqlite_exports, {
37681
- SQLiteAdapter: () => SQLiteAdapter
38191
+ SQLiteAdapter: () => SQLiteAdapter,
38192
+ resolveSqlitePath: () => resolveSqlitePath
37682
38193
  });
37683
38194
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
37684
38195
  import { mkdirSync as mkdirSync18 } from "node:fs";
37685
- import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18 } from "node:path";
38196
+ import { dirname as dirname13, isAbsolute as isAbsolute5, join as join29, resolve as resolve18, sep as sep7 } from "node:path";
37686
38197
  function isIdentifier(str) {
37687
38198
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
37688
38199
  }
@@ -37713,17 +38224,20 @@ function sqliteVersionAtLeast(major, minor, patch2) {
37713
38224
  }
37714
38225
  function resolveSqlitePath(dbPath) {
37715
38226
  if (dbPath === ":memory:") return dbPath;
37716
- let path10 = dbPath;
37717
- if (!isAbsolute5(path10)) {
37718
- path10 = join29(process.cwd(), path10);
37719
- mkdirSync18(dirname13(path10), { recursive: true });
37720
- } else {
37721
- const cwd = resolve18(process.cwd());
37722
- const abs = resolve18(path10);
37723
- if (abs.startsWith(cwd + "/") || abs === cwd) {
37724
- mkdirSync18(dirname13(abs), { recursive: true });
37725
- }
38227
+ const isWindowsAbs = /^[A-Za-z]:[\\/]/.test(dbPath);
38228
+ if (isAbsolute5(dbPath) || isWindowsAbs) {
38229
+ return dbPath;
38230
+ }
38231
+ const cwd = resolve18(process.cwd());
38232
+ const path10 = join29(cwd, dbPath);
38233
+ const parent = dirname13(path10);
38234
+ const absParent = resolve18(parent);
38235
+ if (absParent !== cwd && !absParent.startsWith(cwd + sep7)) {
38236
+ throw new Error(
38237
+ `SQLite path "${dbPath}" resolves outside the project directory: "${absParent}" is not within "${cwd}". Tina4 refuses to create directories outside the project (ADR-0086). Use an absolute path for a database that lives outside the project.`
38238
+ );
37726
38239
  }
38240
+ mkdirSync18(parent, { recursive: true, mode: 509 });
37727
38241
  return path10;
37728
38242
  }
37729
38243
  function fieldTypeToSQLite(def) {
@@ -37759,6 +38273,7 @@ var init_sqlite = __esm({
37759
38273
  "src/adapters/sqlite.ts"() {
37760
38274
  "use strict";
37761
38275
  init_sqlDialect();
38276
+ init_sqlCrud();
37762
38277
  init_sqlTranslator();
37763
38278
  _sqliteVersionInfo = null;
37764
38279
  SQLiteAdapter = class {
@@ -37879,16 +38394,12 @@ var init_sqlite = __esm({
37879
38394
  }
37880
38395
  insert(table2, data) {
37881
38396
  if (Array.isArray(data)) {
37882
- if (data.length === 0) return { success: true, affectedRows: 0 };
37883
- const keys2 = Object.keys(data[0]);
37884
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
37885
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
37886
- const result = this.executeMany(sql2, paramsList);
38397
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
38398
+ if (!batch) return { success: true, affectedRows: 0 };
38399
+ const result = this.executeMany(batch.sql, batch.paramsList);
37887
38400
  return { success: true, affectedRows: result.affectedRows, lastId: result.lastId };
37888
38401
  }
37889
- const keys = Object.keys(data);
37890
- const sql = buildInsert(ANSI_DIALECT, table2, keys);
37891
- const values = Object.values(data);
38402
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table2, data);
37892
38403
  try {
37893
38404
  const result = this.db.prepare(sql).run(...toSqlParams(values));
37894
38405
  this._lastInsertId = result.lastInsertRowid;
@@ -37898,21 +38409,7 @@ var init_sqlite = __esm({
37898
38409
  }
37899
38410
  }
37900
38411
  update(table2, data, filter, params) {
37901
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
37902
- if (typeof filter === "string") {
37903
- const where = filter ? ` WHERE ${filter}` : "";
37904
- const sql2 = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses}${where}`;
37905
- const values2 = [...Object.values(data), ...params ?? []];
37906
- try {
37907
- const result = this.db.prepare(sql2).run(...toSqlParams(values2));
37908
- return { success: true, affectedRows: Number(result.changes) };
37909
- } catch (e) {
37910
- return { success: false, affectedRows: 0, error: e.message };
37911
- }
37912
- }
37913
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
37914
- const sql = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
37915
- const values = [...Object.values(data), ...Object.values(filter)];
38412
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table2, data, filter, params);
37916
38413
  try {
37917
38414
  const result = this.db.prepare(sql).run(...toSqlParams(values));
37918
38415
  return { success: true, affectedRows: Number(result.changes) };
@@ -37929,18 +38426,7 @@ var init_sqlite = __esm({
37929
38426
  }
37930
38427
  return { success: true, affectedRows: totalAffected };
37931
38428
  }
37932
- if (typeof filter === "string") {
37933
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${filter}` : `DELETE FROM "${table2}"`;
37934
- try {
37935
- const result = this.db.prepare(sql2).run(...toSqlParams(params ?? []));
37936
- return { success: true, affectedRows: Number(result.changes) };
37937
- } catch (e) {
37938
- return { success: false, affectedRows: 0, error: e.message };
37939
- }
37940
- }
37941
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
37942
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table2)} WHERE ${whereClauses}`;
37943
- const values = Object.values(filter);
38429
+ const { sql, values } = buildDelete(ANSI_DIALECT, table2, filter, params);
37944
38430
  try {
37945
38431
  const result = this.db.prepare(sql).run(...toSqlParams(values));
37946
38432
  return { success: true, affectedRows: Number(result.changes) };
@@ -38232,6 +38718,7 @@ var init_postgres = __esm({
38232
38718
  "src/adapters/postgres.ts"() {
38233
38719
  "use strict";
38234
38720
  init_sqlDialect();
38721
+ init_sqlCrud();
38235
38722
  init_sqlTranslator();
38236
38723
  init_connectTimeout();
38237
38724
  pg = null;
@@ -38433,21 +38920,17 @@ var init_postgres = __esm({
38433
38920
  async insertAsync(table2, data) {
38434
38921
  this.ensureConnected();
38435
38922
  if (Array.isArray(data)) {
38436
- if (data.length === 0) return { success: true, affectedRows: 0 };
38437
- const keys2 = Object.keys(data[0]);
38438
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
38439
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
38923
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
38924
+ if (!batch) return { success: true, affectedRows: 0 };
38440
38925
  try {
38441
- const result = await this.executeManyAsync(sql2, paramsList);
38926
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
38442
38927
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
38443
38928
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
38444
38929
  } catch (e) {
38445
38930
  return { success: false, affectedRows: 0, error: e.message };
38446
38931
  }
38447
38932
  }
38448
- const keys = Object.keys(data);
38449
- const sql = buildInsert(POSTGRES_DIALECT, table2, keys, " RETURNING *");
38450
- const values = Object.values(data);
38933
+ const { sql, values } = buildInsertRow(POSTGRES_DIALECT, table2, data, " RETURNING *");
38451
38934
  try {
38452
38935
  const result = await this.client.query(sql, values);
38453
38936
  const insertedRow = result.rows[0];
@@ -38467,26 +38950,10 @@ var init_postgres = __esm({
38467
38950
  }
38468
38951
  async updateAsync(table2, data, filter, params) {
38469
38952
  this.ensureConnected();
38470
- const dataKeys = Object.keys(data);
38471
- let paramIndex = 1;
38472
- const setClauses = buildSetClause(POSTGRES_DIALECT, dataKeys, paramIndex);
38473
- paramIndex += dataKeys.length;
38474
- if (typeof filter === "string") {
38475
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
38476
- const sql2 = `UPDATE ${POSTGRES_DIALECT.quote(table2)} SET ${setClauses}${where}`;
38477
- const values2 = [...Object.values(data), ...params ?? []];
38478
- try {
38479
- const result = await this.client.query(sql2, values2);
38480
- return { success: true, affectedRows: result.rowCount ?? 0 };
38481
- } catch (e) {
38482
- return { success: false, affectedRows: 0, error: e.message };
38483
- }
38484
- }
38485
- const filterKeys = Object.keys(filter);
38486
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
38487
- paramIndex += filterKeys.length;
38488
- const sql = `UPDATE ${POSTGRES_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
38489
- const values = [...Object.values(data), ...Object.values(filter)];
38953
+ const { sql, values } = buildUpdate(POSTGRES_DIALECT, table2, data, filter, params, {
38954
+ startAt: 1,
38955
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt)
38956
+ });
38490
38957
  try {
38491
38958
  const result = await this.client.query(sql, values);
38492
38959
  return { success: true, affectedRows: result.rowCount ?? 0 };
@@ -38499,21 +38966,10 @@ var init_postgres = __esm({
38499
38966
  }
38500
38967
  async deleteAsync(table2, filter, params) {
38501
38968
  this.ensureConnected();
38502
- if (typeof filter === "string") {
38503
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${this.bindable(filter, params)}` : `DELETE FROM "${table2}"`;
38504
- try {
38505
- const result = await this.client.query(sql2, params ?? []);
38506
- return { success: true, affectedRows: result.rowCount ?? 0 };
38507
- } catch (e) {
38508
- return { success: false, affectedRows: 0, error: e.message };
38509
- }
38510
- }
38511
- const filterKeys = Object.keys(filter);
38512
- let paramIndex = 1;
38513
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
38514
- paramIndex += filterKeys.length;
38515
- const sql = `DELETE FROM ${POSTGRES_DIALECT.quote(table2)} WHERE ${whereClauses}`;
38516
- const values = Object.values(filter);
38969
+ const { sql, values } = buildDelete(POSTGRES_DIALECT, table2, filter, params, {
38970
+ startAt: 1,
38971
+ convertFragment: (fragment) => this.bindable(fragment, params)
38972
+ });
38517
38973
  try {
38518
38974
  const result = await this.client.query(sql, values);
38519
38975
  return { success: true, affectedRows: result.rowCount ?? 0 };
@@ -38687,6 +39143,7 @@ var init_mysql = __esm({
38687
39143
  "src/adapters/mysql.ts"() {
38688
39144
  "use strict";
38689
39145
  init_sqlDialect();
39146
+ init_sqlCrud();
38690
39147
  init_sqlTranslator();
38691
39148
  init_connectTimeout();
38692
39149
  mysql2 = null;
@@ -38868,21 +39325,17 @@ var init_mysql = __esm({
38868
39325
  async insertAsync(table2, data) {
38869
39326
  this.ensureConnected();
38870
39327
  if (Array.isArray(data)) {
38871
- if (data.length === 0) return { success: true, affectedRows: 0 };
38872
- const keys2 = Object.keys(data[0]);
38873
- const sql2 = buildInsert(MYSQL_DIALECT, table2, keys2);
38874
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
39328
+ const batch = buildInsertRows(MYSQL_DIALECT, table2, data);
39329
+ if (!batch) return { success: true, affectedRows: 0 };
38875
39330
  try {
38876
- const result = await this.executeManyAsync(sql2, paramsList);
39331
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
38877
39332
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
38878
39333
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
38879
39334
  } catch (e) {
38880
39335
  return { success: false, affectedRows: 0, error: e.message };
38881
39336
  }
38882
39337
  }
38883
- const keys = Object.keys(data);
38884
- const sql = buildInsert(MYSQL_DIALECT, table2, keys);
38885
- const values = Object.values(data);
39338
+ const { sql, values } = buildInsertRow(MYSQL_DIALECT, table2, data);
38886
39339
  try {
38887
39340
  const result = await this.queryPromise(sql, values);
38888
39341
  this._lastInsertId = result.insertId ?? null;
@@ -38900,21 +39353,7 @@ var init_mysql = __esm({
38900
39353
  }
38901
39354
  async updateAsync(table2, data, filter, params) {
38902
39355
  this.ensureConnected();
38903
- const setClauses = buildSetClause(MYSQL_DIALECT, Object.keys(data));
38904
- if (typeof filter === "string") {
38905
- const where = filter ? ` WHERE ${filter}` : "";
38906
- const sql2 = `UPDATE ${MYSQL_DIALECT.quote(table2)} SET ${setClauses}${where}`;
38907
- const values2 = [...Object.values(data), ...params ?? []];
38908
- try {
38909
- const result = await this.queryPromise(sql2, values2);
38910
- return { success: true, affectedRows: result.affectedRows ?? 0 };
38911
- } catch (e) {
38912
- return { success: false, affectedRows: 0, error: e.message };
38913
- }
38914
- }
38915
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
38916
- const sql = `UPDATE ${MYSQL_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
38917
- const values = [...Object.values(data), ...Object.values(filter)];
39356
+ const { sql, values } = buildUpdate(MYSQL_DIALECT, table2, data, filter, params);
38918
39357
  try {
38919
39358
  const result = await this.queryPromise(sql, values);
38920
39359
  return { success: true, affectedRows: result.affectedRows ?? 0 };
@@ -38927,18 +39366,7 @@ var init_mysql = __esm({
38927
39366
  }
38928
39367
  async deleteAsync(table2, filter, params) {
38929
39368
  this.ensureConnected();
38930
- if (typeof filter === "string") {
38931
- const sql2 = filter ? `DELETE FROM \`${table2}\` WHERE ${filter}` : `DELETE FROM \`${table2}\``;
38932
- try {
38933
- const result = await this.queryPromise(sql2, params ?? []);
38934
- return { success: true, affectedRows: result.affectedRows ?? 0 };
38935
- } catch (e) {
38936
- return { success: false, affectedRows: 0, error: e.message };
38937
- }
38938
- }
38939
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
38940
- const sql = `DELETE FROM ${MYSQL_DIALECT.quote(table2)} WHERE ${whereClauses}`;
38941
- const values = Object.values(filter);
39369
+ const { sql, values } = buildDelete(MYSQL_DIALECT, table2, filter, params);
38942
39370
  try {
38943
39371
  const result = await this.queryPromise(sql, values);
38944
39372
  return { success: true, affectedRows: result.affectedRows ?? 0 };
@@ -39090,6 +39518,7 @@ var init_mssql = __esm({
39090
39518
  "src/adapters/mssql.ts"() {
39091
39519
  "use strict";
39092
39520
  init_sqlDialect();
39521
+ init_sqlCrud();
39093
39522
  init_sqlTranslator();
39094
39523
  init_connectTimeout();
39095
39524
  tedious = null;
@@ -39346,21 +39775,17 @@ var init_mssql = __esm({
39346
39775
  async insertAsync(table2, data) {
39347
39776
  this.ensureConnected();
39348
39777
  if (Array.isArray(data)) {
39349
- if (data.length === 0) return { success: true, affectedRows: 0 };
39350
- const keys2 = Object.keys(data[0]);
39351
- const sql2 = buildInsert({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table2, keys2);
39352
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
39778
+ const batch = buildInsertRows({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table2, data);
39779
+ if (!batch) return { success: true, affectedRows: 0 };
39353
39780
  try {
39354
- const result = await this.executeManyAsync(sql2, paramsList);
39781
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
39355
39782
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
39356
39783
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
39357
39784
  } catch (e) {
39358
39785
  return { success: false, affectedRows: 0, error: e.message };
39359
39786
  }
39360
39787
  }
39361
- const keys = Object.keys(data);
39362
- const sql = buildInsert(MSSQL_DIALECT, table2, keys, "; SELECT SCOPE_IDENTITY() AS id", 0);
39363
- const values = Object.values(data);
39788
+ const { sql, values } = buildInsertRow(MSSQL_DIALECT, table2, data, "; SELECT SCOPE_IDENTITY() AS id", { startAt: 0 });
39364
39789
  try {
39365
39790
  const result = await this.execSqlPromise(sql, values);
39366
39791
  const id = result.rows[0]?.id ?? null;
@@ -39383,26 +39808,10 @@ var init_mssql = __esm({
39383
39808
  }
39384
39809
  async updateAsync(table2, data, filter, params) {
39385
39810
  this.ensureConnected();
39386
- const dataKeys = Object.keys(data);
39387
- let paramIndex = 0;
39388
- const setClauses = buildSetClause(MSSQL_DIALECT, dataKeys, paramIndex);
39389
- paramIndex += dataKeys.length;
39390
- if (typeof filter === "string") {
39391
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
39392
- const sql2 = `UPDATE ${MSSQL_DIALECT.quote(table2)} SET ${setClauses}${where}`;
39393
- const values2 = [...Object.values(data), ...params ?? []];
39394
- try {
39395
- const result = await this.execSqlPromise(sql2, values2);
39396
- return { success: true, affectedRows: result.rowCount };
39397
- } catch (e) {
39398
- return { success: false, affectedRows: 0, error: e.message };
39399
- }
39400
- }
39401
- const filterKeys = Object.keys(filter);
39402
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
39403
- paramIndex += filterKeys.length;
39404
- const sql = `UPDATE ${MSSQL_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
39405
- const values = [...Object.values(data), ...Object.values(filter)];
39811
+ const { sql, values } = buildUpdate(MSSQL_DIALECT, table2, data, filter, params, {
39812
+ startAt: 0,
39813
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt)
39814
+ });
39406
39815
  try {
39407
39816
  const result = await this.execSqlPromise(sql, values);
39408
39817
  return { success: true, affectedRows: result.rowCount };
@@ -39415,21 +39824,10 @@ var init_mssql = __esm({
39415
39824
  }
39416
39825
  async deleteAsync(table2, filter, params) {
39417
39826
  this.ensureConnected();
39418
- if (typeof filter === "string") {
39419
- const sql2 = filter ? `DELETE FROM [${table2}] WHERE ${this.bindable(filter, params)}` : `DELETE FROM [${table2}]`;
39420
- try {
39421
- const result = await this.execSqlPromise(sql2, params ?? []);
39422
- return { success: true, affectedRows: result.rowCount };
39423
- } catch (e) {
39424
- return { success: false, affectedRows: 0, error: e.message };
39425
- }
39426
- }
39427
- const filterKeys = Object.keys(filter);
39428
- let paramIndex = 0;
39429
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
39430
- paramIndex += filterKeys.length;
39431
- const sql = `DELETE FROM ${MSSQL_DIALECT.quote(table2)} WHERE ${whereClauses}`;
39432
- const values = Object.values(filter);
39827
+ const { sql, values } = buildDelete(MSSQL_DIALECT, table2, filter, params, {
39828
+ startAt: 0,
39829
+ convertFragment: (fragment) => this.bindable(fragment, params)
39830
+ });
39433
39831
  try {
39434
39832
  const result = await this.execSqlPromise(sql, values);
39435
39833
  return { success: true, affectedRows: result.rowCount };
@@ -39682,6 +40080,7 @@ var init_firebird = __esm({
39682
40080
  "src/adapters/firebird.ts"() {
39683
40081
  "use strict";
39684
40082
  init_sqlDialect();
40083
+ init_sqlCrud();
39685
40084
  init_sqlTranslator();
39686
40085
  init_connectTimeout();
39687
40086
  firebird = null;
@@ -40052,17 +40451,13 @@ var init_firebird = __esm({
40052
40451
  async insertAsync(table2, data) {
40053
40452
  this.ensureConnected();
40054
40453
  if (Array.isArray(data)) {
40055
- if (data.length === 0) return { success: true, affectedRows: 0 };
40056
- const keys2 = Object.keys(data[0]);
40057
- const sql2 = buildInsert(FB_DIALECT, table2, keys2);
40058
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
40059
- const result = await this.executeManyAsync(sql2, paramsList);
40454
+ const batch = buildInsertRows(FB_DIALECT, table2, data);
40455
+ if (!batch) return { success: true, affectedRows: 0 };
40456
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
40060
40457
  const lastId2 = await this.readGeneratorId(table2) ?? result.lastId;
40061
40458
  return { success: true, affectedRows: result.totalAffected, lastId: lastId2 ?? void 0 };
40062
40459
  }
40063
- const keys = Object.keys(data);
40064
- const sql = buildInsert(FB_DIALECT, table2, keys);
40065
- const values = Object.values(data);
40460
+ const { sql, values } = buildInsertRow(FB_DIALECT, table2, data);
40066
40461
  await this.executePromise(sql, values);
40067
40462
  const lastId = await this.readGeneratorId(table2);
40068
40463
  return { success: true, affectedRows: 1, lastId: lastId ?? void 0 };
@@ -40072,18 +40467,7 @@ var init_firebird = __esm({
40072
40467
  }
40073
40468
  async updateAsync(table2, data, filter, params) {
40074
40469
  this.ensureConnected();
40075
- const setClauses = buildSetClause(FB_DIALECT, Object.keys(data));
40076
- if (typeof filter === "string") {
40077
- const where = filter ? ` WHERE ${filter}` : "";
40078
- const affected2 = await this.executeReturningCount(
40079
- `UPDATE ${fbQuote(table2)} SET ${setClauses}${where}`,
40080
- [...Object.values(data), ...params ?? []]
40081
- );
40082
- return { success: true, affectedRows: affected2 };
40083
- }
40084
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
40085
- const sql = `UPDATE ${FB_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
40086
- const values = [...Object.values(data), ...Object.values(filter)];
40470
+ const { sql, values } = buildUpdate(FB_DIALECT, table2, data, filter, params);
40087
40471
  const affected = await this.executeReturningCount(sql, values);
40088
40472
  return { success: true, affectedRows: affected };
40089
40473
  }
@@ -40092,14 +40476,7 @@ var init_firebird = __esm({
40092
40476
  }
40093
40477
  async deleteAsync(table2, filter, params) {
40094
40478
  this.ensureConnected();
40095
- if (typeof filter === "string") {
40096
- const where = filter ? ` WHERE ${filter}` : "";
40097
- const affected2 = await this.executeReturningCount(`DELETE FROM ${fbQuote(table2)}${where}`, params ?? []);
40098
- return { success: true, affectedRows: affected2 };
40099
- }
40100
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
40101
- const sql = `DELETE FROM ${FB_DIALECT.quote(table2)} WHERE ${whereClauses}`;
40102
- const values = Object.values(filter);
40479
+ const { sql, values } = buildDelete(FB_DIALECT, table2, filter, params);
40103
40480
  const affected = await this.executeReturningCount(sql, values);
40104
40481
  return { success: true, affectedRows: affected };
40105
40482
  }
@@ -40931,6 +41308,7 @@ var init_odbc = __esm({
40931
41308
  "src/adapters/odbc.ts"() {
40932
41309
  "use strict";
40933
41310
  init_sqlDialect();
41311
+ init_sqlCrud();
40934
41312
  init_sqlTranslator();
40935
41313
  init_connectTimeout();
40936
41314
  odbcModule = null;
@@ -41130,16 +41508,12 @@ var init_odbc = __esm({
41130
41508
  async insertAsync(table2, data) {
41131
41509
  this.ensureConnected();
41132
41510
  if (Array.isArray(data)) {
41133
- if (data.length === 0) return { success: true, affectedRows: 0 };
41134
- const keys2 = Object.keys(data[0]);
41135
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
41136
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
41137
- const { totalAffected } = await this.executeManyAsync(sql2, paramsList);
41511
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
41512
+ if (!batch) return { success: true, affectedRows: 0 };
41513
+ const { totalAffected } = await this.executeManyAsync(batch.sql, batch.paramsList);
41138
41514
  return { success: true, affectedRows: totalAffected };
41139
41515
  }
41140
- const keys = Object.keys(data);
41141
- const sql = buildInsert(ANSI_DIALECT, table2, keys);
41142
- const values = Object.values(data);
41516
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table2, data);
41143
41517
  try {
41144
41518
  const result = await this.connection.query(sql, values);
41145
41519
  return { success: true, affectedRows: this.affectedCount(result), lastId: this._lastInsertId ?? void 0 };
@@ -41150,17 +41524,7 @@ var init_odbc = __esm({
41150
41524
  /** Update rows in a table matching filter. */
41151
41525
  async updateAsync(table2, data, filter, params) {
41152
41526
  this.ensureConnected();
41153
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
41154
- let whereSql;
41155
- let values;
41156
- if (typeof filter === "string") {
41157
- whereSql = filter;
41158
- values = [...Object.values(data), ...params ?? []];
41159
- } else {
41160
- whereSql = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
41161
- values = [...Object.values(data), ...Object.values(filter)];
41162
- }
41163
- const sql = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereSql}`;
41527
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table2, data, filter, params);
41164
41528
  try {
41165
41529
  const result = await this.connection.query(sql, values);
41166
41530
  return { success: true, affectedRows: this.affectedCount(result) };
@@ -41179,18 +41543,7 @@ var init_odbc = __esm({
41179
41543
  }
41180
41544
  return { success: true, affectedRows: totalAffected };
41181
41545
  }
41182
- if (typeof filter === "string") {
41183
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${filter}` : `DELETE FROM "${table2}"`;
41184
- try {
41185
- const result = await this.connection.query(sql2, params ?? []);
41186
- return { success: true, affectedRows: this.affectedCount(result) };
41187
- } catch (e) {
41188
- return { success: false, affectedRows: 0, error: e.message };
41189
- }
41190
- }
41191
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
41192
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table2)} WHERE ${whereClauses}`;
41193
- const values = Object.values(filter);
41546
+ const { sql, values } = buildDelete(ANSI_DIALECT, table2, filter, params);
41194
41547
  try {
41195
41548
  const result = await this.connection.query(sql, values);
41196
41549
  return { success: true, affectedRows: this.affectedCount(result) };
@@ -44910,11 +45263,11 @@ async function eagerLoadBelongsToRelation(instances, model, relatedClass, relati
44910
45263
  const relatedPkColumn = relatedClass.getDbColumn(relatedPk);
44911
45264
  const related = await eagerQueryRelated(relatedClass, relatedPkColumn, values);
44912
45265
  if (nested.length > 0 && related.length > 0) await relatedClass._eagerLoad(related, nested);
44913
- const lookup = {};
44914
- for (const record of related) lookup[_joinKey(record[relatedPk])] = record;
45266
+ const lookup2 = {};
45267
+ for (const record of related) lookup2[_joinKey(record[relatedPk])] = record;
44915
45268
  for (const instance of instances) {
44916
45269
  const value = instance[fkProp];
44917
- relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup[_joinKey(value)] ?? null : null;
45270
+ relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup2[_joinKey(value)] ?? null : null;
44918
45271
  }
44919
45272
  }
44920
45273
  async function eagerLoadRelation(instances, model, relationName, nested) {
@@ -48070,7 +48423,7 @@ var init_attachment = __esm({
48070
48423
  // src/realtime/storage.ts
48071
48424
  import { randomBytes as randomBytes11 } from "node:crypto";
48072
48425
  import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync19 } from "node:fs";
48073
- import { resolve as resolve20, sep as sep7 } from "node:path";
48426
+ import { resolve as resolve20, sep as sep8 } from "node:path";
48074
48427
  import { createRequire as createRequire8 } from "node:module";
48075
48428
  function loadAwsSdk(packageName) {
48076
48429
  try {
@@ -48123,7 +48476,7 @@ var init_storage = __esm({
48123
48476
  // Resolve inside the root and reject any traversal attempt.
48124
48477
  pathFor(key) {
48125
48478
  const target = resolve20(this.directory, key);
48126
- if (target !== this.directory && !target.startsWith(this.directory + sep7)) {
48479
+ if (target !== this.directory && !target.startsWith(this.directory + sep8)) {
48127
48480
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
48128
48481
  }
48129
48482
  return target;
@@ -48640,6 +48993,7 @@ __export(index_exports, {
48640
48993
  resolveDbPool: () => resolveDbPool,
48641
48994
  resolveFirebirdCharset: () => resolveFirebirdCharset,
48642
48995
  resolveGraphConnectTimeout: () => resolveGraphConnectTimeout,
48996
+ resolveSqlitePath: () => resolveSqlitePath,
48643
48997
  rollback: () => rollback,
48644
48998
  seedModels: () => seedModels,
48645
48999
  seedOrm: () => seedOrm,
@@ -48806,6 +49160,7 @@ export {
48806
49160
  resolveDbPool,
48807
49161
  resolveFirebirdCharset,
48808
49162
  resolveGraphConnectTimeout,
49163
+ resolveSqlitePath,
48809
49164
  rollback,
48810
49165
  seedModels,
48811
49166
  seedOrm,