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
@@ -951,6 +951,8 @@ var auth_exports = {};
951
951
  __export(auth_exports, {
952
952
  Auth: () => Auth,
953
953
  JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
954
+ MIN_SECRET_BYTES: () => MIN_SECRET_BYTES,
955
+ NON_IDENTITY_TOKEN_TYPES: () => NON_IDENTITY_TOKEN_TYPES,
954
956
  algorithmAvailable: () => algorithmAvailable,
955
957
  authMiddleware: () => authMiddleware,
956
958
  authenticateRequest: () => authenticateRequest,
@@ -960,7 +962,10 @@ __export(auth_exports, {
960
962
  getPayload: () => getPayload,
961
963
  getToken: () => getToken,
962
964
  hashPassword: () => hashPassword,
965
+ insecureSecretMessage: () => insecureSecretMessage,
966
+ isIdentityPayload: () => isIdentityPayload,
963
967
  refreshToken: () => refreshToken,
968
+ requireBootSecret: () => requireBootSecret,
964
969
  resolveAlgorithm: () => resolveAlgorithm,
965
970
  validToken: () => validToken,
966
971
  validateApiKey: () => validateApiKey
@@ -996,6 +1001,23 @@ async function _logWarning(message) {
996
1001
  function _warnBlankSecret() {
997
1002
  void _logWarning(BLANK_SECRET_WARNING);
998
1003
  }
1004
+ function isIdentityPayload(payload) {
1005
+ return payload != null && !NON_IDENTITY_TOKEN_TYPES.includes(payload.type);
1006
+ }
1007
+ function insecureSecretMessage(secret) {
1008
+ const length = Buffer.byteLength(secret ?? "", "utf8");
1009
+ if (length >= MIN_SECRET_BYTES) return null;
1010
+ const what = length === 0 ? "is not set" : `is ${length} bytes`;
1011
+ 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.`;
1012
+ }
1013
+ function requireBootSecret() {
1014
+ const algorithm = (process.env.TINA4_JWT_ALGORITHM ?? "").trim() || "HS256";
1015
+ if (RSA_SIGN_ALGORITHMS.has(algorithm)) return;
1016
+ const secret = process.env.TINA4_SECRET ?? "";
1017
+ if (secret === "" && _isDev()) return;
1018
+ const message = insecureSecretMessage(secret);
1019
+ if (message) throw new Error(message);
1020
+ }
999
1021
  function ensureDevSecret(cwd) {
1000
1022
  if (process.env.TINA4_SECRET) return null;
1001
1023
  if (!_isDev() || _isCi() || _isProduction()) {
@@ -1126,6 +1148,13 @@ function validToken(token, secret, algorithm) {
1126
1148
  const [h, p, sig] = parts;
1127
1149
  const header = JSON.parse(base64urlDecode(h).toString());
1128
1150
  if (header.alg !== resolvedAlgorithm) return null;
1151
+ if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
1152
+ const weak = insecureSecretMessage(resolvedSecret);
1153
+ if (weak) {
1154
+ void _logWarning(weak);
1155
+ return null;
1156
+ }
1157
+ }
1129
1158
  const signingInput = `${h}.${p}`;
1130
1159
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
1131
1160
  return null;
@@ -1157,6 +1186,8 @@ function getPayload(token) {
1157
1186
  function sign(input, secret, algorithm) {
1158
1187
  const digest = HMAC_DIGESTS.get(algorithm);
1159
1188
  if (digest) {
1189
+ const weak = insecureSecretMessage(secret);
1190
+ if (weak) throw new Error(weak);
1160
1191
  return base64urlEncode(createHmac(digest, secret).update(input).digest());
1161
1192
  }
1162
1193
  const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
@@ -1214,17 +1245,17 @@ function authMiddleware(secret, algorithm) {
1214
1245
  return;
1215
1246
  }
1216
1247
  const token = authHeader.slice(7);
1217
- if (!validToken(token, secret, algorithm)) {
1248
+ const payload = validToken(token, secret, algorithm);
1249
+ if (!isIdentityPayload(payload)) {
1218
1250
  res({ error: "Unauthorized" }, 401);
1219
1251
  return;
1220
1252
  }
1221
- req2.auth = getPayload(token);
1253
+ req2.auth = payload;
1222
1254
  next();
1223
1255
  };
1224
1256
  }
1225
1257
  function refreshToken(token, expiresIn = 60) {
1226
- if (!validToken(token)) return null;
1227
- const payload = getPayload(token);
1258
+ const payload = validToken(token);
1228
1259
  if (!payload) return null;
1229
1260
  const { iat: _iat, exp: _exp, ...claims } = payload;
1230
1261
  return getToken(claims, expiresIn);
@@ -1233,7 +1264,8 @@ function authenticateRequest(headers, secret, algorithm) {
1233
1264
  const authHeader = headers.authorization ?? headers.Authorization ?? "";
1234
1265
  if (!authHeader.startsWith("Bearer ")) return null;
1235
1266
  const token = authHeader.slice(7);
1236
- if (validToken(token, secret, algorithm)) return getPayload(token);
1267
+ const payload = validToken(token, secret, algorithm);
1268
+ if (payload) return isIdentityPayload(payload) ? payload : null;
1237
1269
  if (validateApiKey(token)) {
1238
1270
  return { _auth: "api_key" };
1239
1271
  }
@@ -1247,12 +1279,14 @@ function validateApiKey(provided, expected) {
1247
1279
  if (a.length !== b.length) return false;
1248
1280
  return timingSafeEqual(a, b);
1249
1281
  }
1250
- var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, KNOWN_ALGORITHMS, ALGORITHM_REMEDY, capabilityCache, JWT_LEEWAY_SECONDS, Auth;
1282
+ 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;
1251
1283
  var init_auth = __esm({
1252
1284
  "../core/src/auth.ts"() {
1253
1285
  "use strict";
1254
1286
  init_dotenv();
1255
1287
  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.";
1288
+ NON_IDENTITY_TOKEN_TYPES = ["form"];
1289
+ MIN_SECRET_BYTES = 32;
1256
1290
  HMAC_DIGESTS = /* @__PURE__ */ new Map([
1257
1291
  ["HS256", "sha256"],
1258
1292
  ["HS384", "sha384"],
@@ -3541,14 +3575,72 @@ var init_cachedDatabase = __esm({
3541
3575
  }
3542
3576
  });
3543
3577
 
3578
+ // ../orm/src/adapters/sqlCrud.ts
3579
+ function buildInsertRow(dialect, table2, data, suffix = "", options = {}) {
3580
+ const keys = Object.keys(data);
3581
+ const sql = buildInsert(dialect, table2, keys, suffix, options.startAt ?? 1);
3582
+ return { sql, values: Object.values(data) };
3583
+ }
3584
+ function buildInsertRows(dialect, table2, rows) {
3585
+ if (rows.length === 0) return null;
3586
+ const keys = Object.keys(rows[0]);
3587
+ const sql = buildInsert(dialect, table2, keys);
3588
+ const paramsList = rows.map((row) => keys.map((k) => row[k]));
3589
+ return { sql, paramsList };
3590
+ }
3591
+ function buildUpdate(dialect, table2, data, filter, params, options = {}) {
3592
+ const startAt = options.startAt ?? 1;
3593
+ const convertFragment = options.convertFragment ?? identityFragment;
3594
+ const dataKeys = Object.keys(data);
3595
+ const setClauses = buildSetClause(dialect, dataKeys, startAt);
3596
+ const whereStart = startAt + dataKeys.length;
3597
+ if (typeof filter === "string") {
3598
+ const where = filter ? ` WHERE ${convertFragment(filter, whereStart)}` : "";
3599
+ return {
3600
+ sql: `UPDATE ${dialect.quote(table2)} SET ${setClauses}${where}`,
3601
+ values: [...Object.values(data), ...params ?? []]
3602
+ };
3603
+ }
3604
+ const whereClauses = buildWhereClause(dialect, Object.keys(filter), whereStart);
3605
+ return {
3606
+ sql: `UPDATE ${dialect.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`,
3607
+ values: [...Object.values(data), ...Object.values(filter)]
3608
+ };
3609
+ }
3610
+ function buildDelete(dialect, table2, filter, params, options = {}) {
3611
+ const startAt = options.startAt ?? 1;
3612
+ const convertFragment = options.convertFragment ?? identityFragment;
3613
+ if (typeof filter === "string") {
3614
+ const where = filter ? ` WHERE ${convertFragment(filter, startAt)}` : "";
3615
+ return {
3616
+ sql: `DELETE FROM ${dialect.quote(table2)}${where}`,
3617
+ values: params ?? []
3618
+ };
3619
+ }
3620
+ const whereClauses = buildWhereClause(dialect, Object.keys(filter), startAt);
3621
+ return {
3622
+ sql: `DELETE FROM ${dialect.quote(table2)} WHERE ${whereClauses}`,
3623
+ values: Object.values(filter)
3624
+ };
3625
+ }
3626
+ var identityFragment;
3627
+ var init_sqlCrud = __esm({
3628
+ "../orm/src/adapters/sqlCrud.ts"() {
3629
+ "use strict";
3630
+ init_sqlDialect();
3631
+ identityFragment = (fragment) => fragment;
3632
+ }
3633
+ });
3634
+
3544
3635
  // ../orm/src/adapters/sqlite.ts
3545
3636
  var sqlite_exports = {};
3546
3637
  __export(sqlite_exports, {
3547
- SQLiteAdapter: () => SQLiteAdapter
3638
+ SQLiteAdapter: () => SQLiteAdapter,
3639
+ resolveSqlitePath: () => resolveSqlitePath
3548
3640
  });
3549
3641
  import { DatabaseSync } from "node:sqlite";
3550
3642
  import { mkdirSync as mkdirSync3 } from "node:fs";
3551
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, resolve as resolve3 } from "node:path";
3643
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, resolve as resolve3, sep } from "node:path";
3552
3644
  function isIdentifier(str) {
3553
3645
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
3554
3646
  }
@@ -3579,17 +3671,20 @@ function sqliteVersionAtLeast(major, minor, patch2) {
3579
3671
  }
3580
3672
  function resolveSqlitePath(dbPath) {
3581
3673
  if (dbPath === ":memory:") return dbPath;
3582
- let path10 = dbPath;
3583
- if (!isAbsolute2(path10)) {
3584
- path10 = join5(process.cwd(), path10);
3585
- mkdirSync3(dirname4(path10), { recursive: true });
3586
- } else {
3587
- const cwd = resolve3(process.cwd());
3588
- const abs = resolve3(path10);
3589
- if (abs.startsWith(cwd + "/") || abs === cwd) {
3590
- mkdirSync3(dirname4(abs), { recursive: true });
3591
- }
3674
+ const isWindowsAbs = /^[A-Za-z]:[\\/]/.test(dbPath);
3675
+ if (isAbsolute2(dbPath) || isWindowsAbs) {
3676
+ return dbPath;
3677
+ }
3678
+ const cwd = resolve3(process.cwd());
3679
+ const path10 = join5(cwd, dbPath);
3680
+ const parent = dirname4(path10);
3681
+ const absParent = resolve3(parent);
3682
+ if (absParent !== cwd && !absParent.startsWith(cwd + sep)) {
3683
+ throw new Error(
3684
+ `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.`
3685
+ );
3592
3686
  }
3687
+ mkdirSync3(parent, { recursive: true, mode: 509 });
3593
3688
  return path10;
3594
3689
  }
3595
3690
  function fieldTypeToSQLite(def) {
@@ -3625,6 +3720,7 @@ var init_sqlite = __esm({
3625
3720
  "../orm/src/adapters/sqlite.ts"() {
3626
3721
  "use strict";
3627
3722
  init_sqlDialect();
3723
+ init_sqlCrud();
3628
3724
  init_sqlTranslator();
3629
3725
  _sqliteVersionInfo = null;
3630
3726
  SQLiteAdapter = class {
@@ -3745,16 +3841,12 @@ var init_sqlite = __esm({
3745
3841
  }
3746
3842
  insert(table2, data) {
3747
3843
  if (Array.isArray(data)) {
3748
- if (data.length === 0) return { success: true, affectedRows: 0 };
3749
- const keys2 = Object.keys(data[0]);
3750
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
3751
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
3752
- const result = this.executeMany(sql2, paramsList);
3844
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
3845
+ if (!batch) return { success: true, affectedRows: 0 };
3846
+ const result = this.executeMany(batch.sql, batch.paramsList);
3753
3847
  return { success: true, affectedRows: result.affectedRows, lastId: result.lastId };
3754
3848
  }
3755
- const keys = Object.keys(data);
3756
- const sql = buildInsert(ANSI_DIALECT, table2, keys);
3757
- const values = Object.values(data);
3849
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table2, data);
3758
3850
  try {
3759
3851
  const result = this.db.prepare(sql).run(...toSqlParams(values));
3760
3852
  this._lastInsertId = result.lastInsertRowid;
@@ -3764,21 +3856,7 @@ var init_sqlite = __esm({
3764
3856
  }
3765
3857
  }
3766
3858
  update(table2, data, filter, params) {
3767
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
3768
- if (typeof filter === "string") {
3769
- const where = filter ? ` WHERE ${filter}` : "";
3770
- const sql2 = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses}${where}`;
3771
- const values2 = [...Object.values(data), ...params ?? []];
3772
- try {
3773
- const result = this.db.prepare(sql2).run(...toSqlParams(values2));
3774
- return { success: true, affectedRows: Number(result.changes) };
3775
- } catch (e) {
3776
- return { success: false, affectedRows: 0, error: e.message };
3777
- }
3778
- }
3779
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
3780
- const sql = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
3781
- const values = [...Object.values(data), ...Object.values(filter)];
3859
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table2, data, filter, params);
3782
3860
  try {
3783
3861
  const result = this.db.prepare(sql).run(...toSqlParams(values));
3784
3862
  return { success: true, affectedRows: Number(result.changes) };
@@ -3795,18 +3873,7 @@ var init_sqlite = __esm({
3795
3873
  }
3796
3874
  return { success: true, affectedRows: totalAffected };
3797
3875
  }
3798
- if (typeof filter === "string") {
3799
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${filter}` : `DELETE FROM "${table2}"`;
3800
- try {
3801
- const result = this.db.prepare(sql2).run(...toSqlParams(params ?? []));
3802
- return { success: true, affectedRows: Number(result.changes) };
3803
- } catch (e) {
3804
- return { success: false, affectedRows: 0, error: e.message };
3805
- }
3806
- }
3807
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
3808
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table2)} WHERE ${whereClauses}`;
3809
- const values = Object.values(filter);
3876
+ const { sql, values } = buildDelete(ANSI_DIALECT, table2, filter, params);
3810
3877
  try {
3811
3878
  const result = this.db.prepare(sql).run(...toSqlParams(values));
3812
3879
  return { success: true, affectedRows: Number(result.changes) };
@@ -4098,6 +4165,7 @@ var init_postgres = __esm({
4098
4165
  "../orm/src/adapters/postgres.ts"() {
4099
4166
  "use strict";
4100
4167
  init_sqlDialect();
4168
+ init_sqlCrud();
4101
4169
  init_sqlTranslator();
4102
4170
  init_connectTimeout();
4103
4171
  pg = null;
@@ -4299,21 +4367,17 @@ var init_postgres = __esm({
4299
4367
  async insertAsync(table2, data) {
4300
4368
  this.ensureConnected();
4301
4369
  if (Array.isArray(data)) {
4302
- if (data.length === 0) return { success: true, affectedRows: 0 };
4303
- const keys2 = Object.keys(data[0]);
4304
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
4305
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
4370
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
4371
+ if (!batch) return { success: true, affectedRows: 0 };
4306
4372
  try {
4307
- const result = await this.executeManyAsync(sql2, paramsList);
4373
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
4308
4374
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
4309
4375
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
4310
4376
  } catch (e) {
4311
4377
  return { success: false, affectedRows: 0, error: e.message };
4312
4378
  }
4313
4379
  }
4314
- const keys = Object.keys(data);
4315
- const sql = buildInsert(POSTGRES_DIALECT, table2, keys, " RETURNING *");
4316
- const values = Object.values(data);
4380
+ const { sql, values } = buildInsertRow(POSTGRES_DIALECT, table2, data, " RETURNING *");
4317
4381
  try {
4318
4382
  const result = await this.client.query(sql, values);
4319
4383
  const insertedRow = result.rows[0];
@@ -4333,26 +4397,10 @@ var init_postgres = __esm({
4333
4397
  }
4334
4398
  async updateAsync(table2, data, filter, params) {
4335
4399
  this.ensureConnected();
4336
- const dataKeys = Object.keys(data);
4337
- let paramIndex = 1;
4338
- const setClauses = buildSetClause(POSTGRES_DIALECT, dataKeys, paramIndex);
4339
- paramIndex += dataKeys.length;
4340
- if (typeof filter === "string") {
4341
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
4342
- const sql2 = `UPDATE ${POSTGRES_DIALECT.quote(table2)} SET ${setClauses}${where}`;
4343
- const values2 = [...Object.values(data), ...params ?? []];
4344
- try {
4345
- const result = await this.client.query(sql2, values2);
4346
- return { success: true, affectedRows: result.rowCount ?? 0 };
4347
- } catch (e) {
4348
- return { success: false, affectedRows: 0, error: e.message };
4349
- }
4350
- }
4351
- const filterKeys = Object.keys(filter);
4352
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
4353
- paramIndex += filterKeys.length;
4354
- const sql = `UPDATE ${POSTGRES_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
4355
- const values = [...Object.values(data), ...Object.values(filter)];
4400
+ const { sql, values } = buildUpdate(POSTGRES_DIALECT, table2, data, filter, params, {
4401
+ startAt: 1,
4402
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt)
4403
+ });
4356
4404
  try {
4357
4405
  const result = await this.client.query(sql, values);
4358
4406
  return { success: true, affectedRows: result.rowCount ?? 0 };
@@ -4365,21 +4413,10 @@ var init_postgres = __esm({
4365
4413
  }
4366
4414
  async deleteAsync(table2, filter, params) {
4367
4415
  this.ensureConnected();
4368
- if (typeof filter === "string") {
4369
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${this.bindable(filter, params)}` : `DELETE FROM "${table2}"`;
4370
- try {
4371
- const result = await this.client.query(sql2, params ?? []);
4372
- return { success: true, affectedRows: result.rowCount ?? 0 };
4373
- } catch (e) {
4374
- return { success: false, affectedRows: 0, error: e.message };
4375
- }
4376
- }
4377
- const filterKeys = Object.keys(filter);
4378
- let paramIndex = 1;
4379
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
4380
- paramIndex += filterKeys.length;
4381
- const sql = `DELETE FROM ${POSTGRES_DIALECT.quote(table2)} WHERE ${whereClauses}`;
4382
- const values = Object.values(filter);
4416
+ const { sql, values } = buildDelete(POSTGRES_DIALECT, table2, filter, params, {
4417
+ startAt: 1,
4418
+ convertFragment: (fragment) => this.bindable(fragment, params)
4419
+ });
4383
4420
  try {
4384
4421
  const result = await this.client.query(sql, values);
4385
4422
  return { success: true, affectedRows: result.rowCount ?? 0 };
@@ -4553,6 +4590,7 @@ var init_mysql = __esm({
4553
4590
  "../orm/src/adapters/mysql.ts"() {
4554
4591
  "use strict";
4555
4592
  init_sqlDialect();
4593
+ init_sqlCrud();
4556
4594
  init_sqlTranslator();
4557
4595
  init_connectTimeout();
4558
4596
  mysql2 = null;
@@ -4734,21 +4772,17 @@ var init_mysql = __esm({
4734
4772
  async insertAsync(table2, data) {
4735
4773
  this.ensureConnected();
4736
4774
  if (Array.isArray(data)) {
4737
- if (data.length === 0) return { success: true, affectedRows: 0 };
4738
- const keys2 = Object.keys(data[0]);
4739
- const sql2 = buildInsert(MYSQL_DIALECT, table2, keys2);
4740
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
4775
+ const batch = buildInsertRows(MYSQL_DIALECT, table2, data);
4776
+ if (!batch) return { success: true, affectedRows: 0 };
4741
4777
  try {
4742
- const result = await this.executeManyAsync(sql2, paramsList);
4778
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
4743
4779
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
4744
4780
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
4745
4781
  } catch (e) {
4746
4782
  return { success: false, affectedRows: 0, error: e.message };
4747
4783
  }
4748
4784
  }
4749
- const keys = Object.keys(data);
4750
- const sql = buildInsert(MYSQL_DIALECT, table2, keys);
4751
- const values = Object.values(data);
4785
+ const { sql, values } = buildInsertRow(MYSQL_DIALECT, table2, data);
4752
4786
  try {
4753
4787
  const result = await this.queryPromise(sql, values);
4754
4788
  this._lastInsertId = result.insertId ?? null;
@@ -4766,21 +4800,7 @@ var init_mysql = __esm({
4766
4800
  }
4767
4801
  async updateAsync(table2, data, filter, params) {
4768
4802
  this.ensureConnected();
4769
- const setClauses = buildSetClause(MYSQL_DIALECT, Object.keys(data));
4770
- if (typeof filter === "string") {
4771
- const where = filter ? ` WHERE ${filter}` : "";
4772
- const sql2 = `UPDATE ${MYSQL_DIALECT.quote(table2)} SET ${setClauses}${where}`;
4773
- const values2 = [...Object.values(data), ...params ?? []];
4774
- try {
4775
- const result = await this.queryPromise(sql2, values2);
4776
- return { success: true, affectedRows: result.affectedRows ?? 0 };
4777
- } catch (e) {
4778
- return { success: false, affectedRows: 0, error: e.message };
4779
- }
4780
- }
4781
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
4782
- const sql = `UPDATE ${MYSQL_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
4783
- const values = [...Object.values(data), ...Object.values(filter)];
4803
+ const { sql, values } = buildUpdate(MYSQL_DIALECT, table2, data, filter, params);
4784
4804
  try {
4785
4805
  const result = await this.queryPromise(sql, values);
4786
4806
  return { success: true, affectedRows: result.affectedRows ?? 0 };
@@ -4793,18 +4813,7 @@ var init_mysql = __esm({
4793
4813
  }
4794
4814
  async deleteAsync(table2, filter, params) {
4795
4815
  this.ensureConnected();
4796
- if (typeof filter === "string") {
4797
- const sql2 = filter ? `DELETE FROM \`${table2}\` WHERE ${filter}` : `DELETE FROM \`${table2}\``;
4798
- try {
4799
- const result = await this.queryPromise(sql2, params ?? []);
4800
- return { success: true, affectedRows: result.affectedRows ?? 0 };
4801
- } catch (e) {
4802
- return { success: false, affectedRows: 0, error: e.message };
4803
- }
4804
- }
4805
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
4806
- const sql = `DELETE FROM ${MYSQL_DIALECT.quote(table2)} WHERE ${whereClauses}`;
4807
- const values = Object.values(filter);
4816
+ const { sql, values } = buildDelete(MYSQL_DIALECT, table2, filter, params);
4808
4817
  try {
4809
4818
  const result = await this.queryPromise(sql, values);
4810
4819
  return { success: true, affectedRows: result.affectedRows ?? 0 };
@@ -4956,6 +4965,7 @@ var init_mssql = __esm({
4956
4965
  "../orm/src/adapters/mssql.ts"() {
4957
4966
  "use strict";
4958
4967
  init_sqlDialect();
4968
+ init_sqlCrud();
4959
4969
  init_sqlTranslator();
4960
4970
  init_connectTimeout();
4961
4971
  tedious = null;
@@ -5212,21 +5222,17 @@ var init_mssql = __esm({
5212
5222
  async insertAsync(table2, data) {
5213
5223
  this.ensureConnected();
5214
5224
  if (Array.isArray(data)) {
5215
- if (data.length === 0) return { success: true, affectedRows: 0 };
5216
- const keys2 = Object.keys(data[0]);
5217
- const sql2 = buildInsert({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table2, keys2);
5218
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
5225
+ const batch = buildInsertRows({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table2, data);
5226
+ if (!batch) return { success: true, affectedRows: 0 };
5219
5227
  try {
5220
- const result = await this.executeManyAsync(sql2, paramsList);
5228
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
5221
5229
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
5222
5230
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
5223
5231
  } catch (e) {
5224
5232
  return { success: false, affectedRows: 0, error: e.message };
5225
5233
  }
5226
5234
  }
5227
- const keys = Object.keys(data);
5228
- const sql = buildInsert(MSSQL_DIALECT, table2, keys, "; SELECT SCOPE_IDENTITY() AS id", 0);
5229
- const values = Object.values(data);
5235
+ const { sql, values } = buildInsertRow(MSSQL_DIALECT, table2, data, "; SELECT SCOPE_IDENTITY() AS id", { startAt: 0 });
5230
5236
  try {
5231
5237
  const result = await this.execSqlPromise(sql, values);
5232
5238
  const id = result.rows[0]?.id ?? null;
@@ -5249,26 +5255,10 @@ var init_mssql = __esm({
5249
5255
  }
5250
5256
  async updateAsync(table2, data, filter, params) {
5251
5257
  this.ensureConnected();
5252
- const dataKeys = Object.keys(data);
5253
- let paramIndex = 0;
5254
- const setClauses = buildSetClause(MSSQL_DIALECT, dataKeys, paramIndex);
5255
- paramIndex += dataKeys.length;
5256
- if (typeof filter === "string") {
5257
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
5258
- const sql2 = `UPDATE ${MSSQL_DIALECT.quote(table2)} SET ${setClauses}${where}`;
5259
- const values2 = [...Object.values(data), ...params ?? []];
5260
- try {
5261
- const result = await this.execSqlPromise(sql2, values2);
5262
- return { success: true, affectedRows: result.rowCount };
5263
- } catch (e) {
5264
- return { success: false, affectedRows: 0, error: e.message };
5265
- }
5266
- }
5267
- const filterKeys = Object.keys(filter);
5268
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
5269
- paramIndex += filterKeys.length;
5270
- const sql = `UPDATE ${MSSQL_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
5271
- const values = [...Object.values(data), ...Object.values(filter)];
5258
+ const { sql, values } = buildUpdate(MSSQL_DIALECT, table2, data, filter, params, {
5259
+ startAt: 0,
5260
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt)
5261
+ });
5272
5262
  try {
5273
5263
  const result = await this.execSqlPromise(sql, values);
5274
5264
  return { success: true, affectedRows: result.rowCount };
@@ -5281,21 +5271,10 @@ var init_mssql = __esm({
5281
5271
  }
5282
5272
  async deleteAsync(table2, filter, params) {
5283
5273
  this.ensureConnected();
5284
- if (typeof filter === "string") {
5285
- const sql2 = filter ? `DELETE FROM [${table2}] WHERE ${this.bindable(filter, params)}` : `DELETE FROM [${table2}]`;
5286
- try {
5287
- const result = await this.execSqlPromise(sql2, params ?? []);
5288
- return { success: true, affectedRows: result.rowCount };
5289
- } catch (e) {
5290
- return { success: false, affectedRows: 0, error: e.message };
5291
- }
5292
- }
5293
- const filterKeys = Object.keys(filter);
5294
- let paramIndex = 0;
5295
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
5296
- paramIndex += filterKeys.length;
5297
- const sql = `DELETE FROM ${MSSQL_DIALECT.quote(table2)} WHERE ${whereClauses}`;
5298
- const values = Object.values(filter);
5274
+ const { sql, values } = buildDelete(MSSQL_DIALECT, table2, filter, params, {
5275
+ startAt: 0,
5276
+ convertFragment: (fragment) => this.bindable(fragment, params)
5277
+ });
5299
5278
  try {
5300
5279
  const result = await this.execSqlPromise(sql, values);
5301
5280
  return { success: true, affectedRows: result.rowCount };
@@ -5548,6 +5527,7 @@ var init_firebird = __esm({
5548
5527
  "../orm/src/adapters/firebird.ts"() {
5549
5528
  "use strict";
5550
5529
  init_sqlDialect();
5530
+ init_sqlCrud();
5551
5531
  init_sqlTranslator();
5552
5532
  init_connectTimeout();
5553
5533
  firebird = null;
@@ -5918,17 +5898,13 @@ var init_firebird = __esm({
5918
5898
  async insertAsync(table2, data) {
5919
5899
  this.ensureConnected();
5920
5900
  if (Array.isArray(data)) {
5921
- if (data.length === 0) return { success: true, affectedRows: 0 };
5922
- const keys2 = Object.keys(data[0]);
5923
- const sql2 = buildInsert(FB_DIALECT, table2, keys2);
5924
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
5925
- const result = await this.executeManyAsync(sql2, paramsList);
5901
+ const batch = buildInsertRows(FB_DIALECT, table2, data);
5902
+ if (!batch) return { success: true, affectedRows: 0 };
5903
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
5926
5904
  const lastId2 = await this.readGeneratorId(table2) ?? result.lastId;
5927
5905
  return { success: true, affectedRows: result.totalAffected, lastId: lastId2 ?? void 0 };
5928
5906
  }
5929
- const keys = Object.keys(data);
5930
- const sql = buildInsert(FB_DIALECT, table2, keys);
5931
- const values = Object.values(data);
5907
+ const { sql, values } = buildInsertRow(FB_DIALECT, table2, data);
5932
5908
  await this.executePromise(sql, values);
5933
5909
  const lastId = await this.readGeneratorId(table2);
5934
5910
  return { success: true, affectedRows: 1, lastId: lastId ?? void 0 };
@@ -5938,18 +5914,7 @@ var init_firebird = __esm({
5938
5914
  }
5939
5915
  async updateAsync(table2, data, filter, params) {
5940
5916
  this.ensureConnected();
5941
- const setClauses = buildSetClause(FB_DIALECT, Object.keys(data));
5942
- if (typeof filter === "string") {
5943
- const where = filter ? ` WHERE ${filter}` : "";
5944
- const affected2 = await this.executeReturningCount(
5945
- `UPDATE ${fbQuote(table2)} SET ${setClauses}${where}`,
5946
- [...Object.values(data), ...params ?? []]
5947
- );
5948
- return { success: true, affectedRows: affected2 };
5949
- }
5950
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
5951
- const sql = `UPDATE ${FB_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
5952
- const values = [...Object.values(data), ...Object.values(filter)];
5917
+ const { sql, values } = buildUpdate(FB_DIALECT, table2, data, filter, params);
5953
5918
  const affected = await this.executeReturningCount(sql, values);
5954
5919
  return { success: true, affectedRows: affected };
5955
5920
  }
@@ -5958,14 +5923,7 @@ var init_firebird = __esm({
5958
5923
  }
5959
5924
  async deleteAsync(table2, filter, params) {
5960
5925
  this.ensureConnected();
5961
- if (typeof filter === "string") {
5962
- const where = filter ? ` WHERE ${filter}` : "";
5963
- const affected2 = await this.executeReturningCount(`DELETE FROM ${fbQuote(table2)}${where}`, params ?? []);
5964
- return { success: true, affectedRows: affected2 };
5965
- }
5966
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
5967
- const sql = `DELETE FROM ${FB_DIALECT.quote(table2)} WHERE ${whereClauses}`;
5968
- const values = Object.values(filter);
5926
+ const { sql, values } = buildDelete(FB_DIALECT, table2, filter, params);
5969
5927
  const affected = await this.executeReturningCount(sql, values);
5970
5928
  return { success: true, affectedRows: affected };
5971
5929
  }
@@ -6797,6 +6755,7 @@ var init_odbc = __esm({
6797
6755
  "../orm/src/adapters/odbc.ts"() {
6798
6756
  "use strict";
6799
6757
  init_sqlDialect();
6758
+ init_sqlCrud();
6800
6759
  init_sqlTranslator();
6801
6760
  init_connectTimeout();
6802
6761
  odbcModule = null;
@@ -6996,16 +6955,12 @@ var init_odbc = __esm({
6996
6955
  async insertAsync(table2, data) {
6997
6956
  this.ensureConnected();
6998
6957
  if (Array.isArray(data)) {
6999
- if (data.length === 0) return { success: true, affectedRows: 0 };
7000
- const keys2 = Object.keys(data[0]);
7001
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
7002
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
7003
- const { totalAffected } = await this.executeManyAsync(sql2, paramsList);
6958
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
6959
+ if (!batch) return { success: true, affectedRows: 0 };
6960
+ const { totalAffected } = await this.executeManyAsync(batch.sql, batch.paramsList);
7004
6961
  return { success: true, affectedRows: totalAffected };
7005
6962
  }
7006
- const keys = Object.keys(data);
7007
- const sql = buildInsert(ANSI_DIALECT, table2, keys);
7008
- const values = Object.values(data);
6963
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table2, data);
7009
6964
  try {
7010
6965
  const result = await this.connection.query(sql, values);
7011
6966
  return { success: true, affectedRows: this.affectedCount(result), lastId: this._lastInsertId ?? void 0 };
@@ -7016,17 +6971,7 @@ var init_odbc = __esm({
7016
6971
  /** Update rows in a table matching filter. */
7017
6972
  async updateAsync(table2, data, filter, params) {
7018
6973
  this.ensureConnected();
7019
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
7020
- let whereSql;
7021
- let values;
7022
- if (typeof filter === "string") {
7023
- whereSql = filter;
7024
- values = [...Object.values(data), ...params ?? []];
7025
- } else {
7026
- whereSql = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
7027
- values = [...Object.values(data), ...Object.values(filter)];
7028
- }
7029
- const sql = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereSql}`;
6974
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table2, data, filter, params);
7030
6975
  try {
7031
6976
  const result = await this.connection.query(sql, values);
7032
6977
  return { success: true, affectedRows: this.affectedCount(result) };
@@ -7045,18 +6990,7 @@ var init_odbc = __esm({
7045
6990
  }
7046
6991
  return { success: true, affectedRows: totalAffected };
7047
6992
  }
7048
- if (typeof filter === "string") {
7049
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${filter}` : `DELETE FROM "${table2}"`;
7050
- try {
7051
- const result = await this.connection.query(sql2, params ?? []);
7052
- return { success: true, affectedRows: this.affectedCount(result) };
7053
- } catch (e) {
7054
- return { success: false, affectedRows: 0, error: e.message };
7055
- }
7056
- }
7057
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
7058
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table2)} WHERE ${whereClauses}`;
7059
- const values = Object.values(filter);
6993
+ const { sql, values } = buildDelete(ANSI_DIALECT, table2, filter, params);
7060
6994
  try {
7061
6995
  const result = await this.connection.query(sql, values);
7062
6996
  return { success: true, affectedRows: this.affectedCount(result) };
@@ -10776,11 +10710,11 @@ async function eagerLoadBelongsToRelation(instances, model, relatedClass, relati
10776
10710
  const relatedPkColumn = relatedClass.getDbColumn(relatedPk);
10777
10711
  const related = await eagerQueryRelated(relatedClass, relatedPkColumn, values);
10778
10712
  if (nested.length > 0 && related.length > 0) await relatedClass._eagerLoad(related, nested);
10779
- const lookup = {};
10780
- for (const record of related) lookup[_joinKey(record[relatedPk])] = record;
10713
+ const lookup2 = {};
10714
+ for (const record of related) lookup2[_joinKey(record[relatedPk])] = record;
10781
10715
  for (const instance of instances) {
10782
10716
  const value = instance[fkProp];
10783
- relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup[_joinKey(value)] ?? null : null;
10717
+ relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup2[_joinKey(value)] ?? null : null;
10784
10718
  }
10785
10719
  }
10786
10720
  async function eagerLoadRelation(instances, model, relationName, nested) {
@@ -14467,7 +14401,7 @@ var init_attachment = __esm({
14467
14401
  // ../orm/src/realtime/storage.ts
14468
14402
  import { randomBytes as randomBytes3 } from "node:crypto";
14469
14403
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, statSync as statSync4 } from "node:fs";
14470
- import { resolve as resolve6, sep } from "node:path";
14404
+ import { resolve as resolve6, sep as sep2 } from "node:path";
14471
14405
  import { createRequire as createRequire6 } from "node:module";
14472
14406
  function loadAwsSdk(packageName) {
14473
14407
  try {
@@ -14520,7 +14454,7 @@ var init_storage = __esm({
14520
14454
  // Resolve inside the root and reject any traversal attempt.
14521
14455
  pathFor(key) {
14522
14456
  const target = resolve6(this.directory, key);
14523
- if (target !== this.directory && !target.startsWith(this.directory + sep)) {
14457
+ if (target !== this.directory && !target.startsWith(this.directory + sep2)) {
14524
14458
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
14525
14459
  }
14526
14460
  return target;
@@ -15037,6 +14971,7 @@ __export(src_exports, {
15037
14971
  resolveDbPool: () => resolveDbPool,
15038
14972
  resolveFirebirdCharset: () => resolveFirebirdCharset,
15039
14973
  resolveGraphConnectTimeout: () => resolveGraphConnectTimeout,
14974
+ resolveSqlitePath: () => resolveSqlitePath,
15040
14975
  rollback: () => rollback,
15041
14976
  seedModels: () => seedModels,
15042
14977
  seedOrm: () => seedOrm,
@@ -15422,9 +15357,9 @@ var init_cache = __esm({
15422
15357
  return entry.value;
15423
15358
  }
15424
15359
  async set(key, value, ttl) {
15425
- const expiresAt = ttl > 0 ? Date.now() + ttl * 1e3 : 0;
15360
+ const expiresAt2 = ttl > 0 ? Date.now() + ttl * 1e3 : 0;
15426
15361
  this.store.delete(key);
15427
- this.store.set(key, { value, expiresAt });
15362
+ this.store.set(key, { value, expiresAt: expiresAt2 });
15428
15363
  while (this.store.size > this.maxEntries) {
15429
15364
  const firstKey = this.store.keys().next().value;
15430
15365
  if (firstKey !== void 0) this.store.delete(firstKey);
@@ -15857,8 +15792,8 @@ ${s}\r
15857
15792
  const oldest = files.shift();
15858
15793
  if (oldest) fs.unlinkSync(path.join(this.dir, oldest.name));
15859
15794
  }
15860
- const expiresAt = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
15861
- const entry = { key, value, expiresAt };
15795
+ const expiresAt2 = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
15796
+ const entry = { key, value, expiresAt: expiresAt2 };
15862
15797
  fs.writeFileSync(this.keyPath(key), JSON.stringify(entry));
15863
15798
  } catch {
15864
15799
  }
@@ -17511,7 +17446,7 @@ __export(engine_exports, {
17511
17446
  });
17512
17447
  import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
17513
17448
  import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync6, realpathSync } from "node:fs";
17514
- import { join as join11, resolve as resolve7, isAbsolute as isAbsolute4, sep as sep2 } from "node:path";
17449
+ import { join as join11, resolve as resolve7, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17515
17450
  function jsonText(value) {
17516
17451
  try {
17517
17452
  const text = JSON.stringify(value);
@@ -17605,8 +17540,8 @@ function capCache(cache, maxEntries) {
17605
17540
  }
17606
17541
  function sweepExpiredCache(cache) {
17607
17542
  const now = Date.now();
17608
- for (const [key, [, expiresAt]] of cache) {
17609
- if (expiresAt <= now) cache.delete(key);
17543
+ for (const [key, [, expiresAt2]] of cache) {
17544
+ if (expiresAt2 <= now) cache.delete(key);
17610
17545
  }
17611
17546
  }
17612
17547
  function tokenize(source) {
@@ -17852,15 +17787,15 @@ function findOutsideQuotes(expr, needle) {
17852
17787
  }
17853
17788
  return -1;
17854
17789
  }
17855
- function splitOutsideQuotes(expr, sep8) {
17856
- if (!expr.includes(sep8)) return [expr];
17790
+ function splitOutsideQuotes(expr, sep9) {
17791
+ if (!expr.includes(sep9)) return [expr];
17857
17792
  const parts = [];
17858
17793
  let currentStart = 0;
17859
17794
  let inQuote = null;
17860
17795
  let depth = 0;
17861
17796
  let bracketDepth = 0;
17862
17797
  let i = 0;
17863
- const sepLen = sep8.length;
17798
+ const sepLen = sep9.length;
17864
17799
  const lastStart = expr.length - sepLen;
17865
17800
  while (i <= lastStart) {
17866
17801
  const ch = expr[i];
@@ -17881,7 +17816,7 @@ function splitOutsideQuotes(expr, sep8) {
17881
17816
  else if (ch === ")") depth--;
17882
17817
  else if (ch === "[") bracketDepth++;
17883
17818
  else if (ch === "]") bracketDepth--;
17884
- if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep8, i)) {
17819
+ if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep9, i)) {
17885
17820
  parts.push(expr.slice(currentStart, i));
17886
17821
  i += sepLen;
17887
17822
  currentStart = i;
@@ -18578,6 +18513,12 @@ function setFormTokenSessionId(sessionId) {
18578
18513
  }
18579
18514
  function _buildFormTokenJwt(descriptor = "") {
18580
18515
  const secret = process.env.TINA4_SECRET ?? "";
18516
+ const secretBytes = Buffer.byteLength(secret, "utf8");
18517
+ if (secretBytes < 32) {
18518
+ throw new Error(
18519
+ `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.`
18520
+ );
18521
+ }
18581
18522
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
18582
18523
  const header = { alg: "HS256", typ: "JWT" };
18583
18524
  const now = Math.floor(Date.now() / 1e3);
@@ -18828,8 +18769,8 @@ var init_engine = __esm({
18828
18769
  },
18829
18770
  first: (v) => Array.isArray(v) ? v[0] ?? null : null,
18830
18771
  last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
18831
- join: (v, sep8) => Array.isArray(v) ? v.map(String).join(sep8 !== void 0 ? String(sep8) : ", ") : String(v),
18832
- split: (v, sep8) => String(v).split(sep8 !== void 0 ? String(sep8) : " "),
18772
+ join: (v, sep9) => Array.isArray(v) ? v.map(String).join(sep9 !== void 0 ? String(sep9) : ", ") : String(v),
18773
+ split: (v, sep9) => String(v).split(sep9 !== void 0 ? String(sep9) : " "),
18833
18774
  replace: (v, from, to) => {
18834
18775
  const s = String(v);
18835
18776
  if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
@@ -19233,7 +19174,7 @@ var init_engine = __esm({
19233
19174
  }
19234
19175
  const root = realpathSync(this.templateDir);
19235
19176
  const real = realpathSync(filePath);
19236
- if (real !== root && !real.startsWith(root + sep2)) {
19177
+ if (real !== root && !real.startsWith(root + sep3)) {
19237
19178
  throw new Error(`Template path escapes the templates directory: ${name}`);
19238
19179
  }
19239
19180
  return readFileSync6(real, "utf-8");
@@ -20119,8 +20060,8 @@ var init_engine = __esm({
20119
20060
  sweepExpiredCache(this.fragmentCache);
20120
20061
  const cached = this.fragmentCache.get(cacheKey);
20121
20062
  if (cached) {
20122
- const [htmlContent, expiresAt] = cached;
20123
- if (Date.now() < expiresAt) {
20063
+ const [htmlContent, expiresAt2] = cached;
20064
+ if (Date.now() < expiresAt2) {
20124
20065
  let i2 = start2 + 1;
20125
20066
  let depth = 0;
20126
20067
  while (i2 < tokens.length) {
@@ -20913,7 +20854,7 @@ function csrfBearerIsValid(req2, secret) {
20913
20854
  const authHeader = req2.headers.authorization ?? "";
20914
20855
  if (!authHeader.startsWith("Bearer ")) return false;
20915
20856
  const bearerToken = authHeader.slice(7).trim();
20916
- return Boolean(bearerToken && validToken(bearerToken, secret));
20857
+ return Boolean(bearerToken && isIdentityPayload(validToken(bearerToken, secret)));
20917
20858
  }
20918
20859
  function csrfRequestToken(req2) {
20919
20860
  const query = req2.query ?? {};
@@ -22050,6 +21991,22 @@ var init_transport = __esm({
22050
21991
  }
22051
21992
  });
22052
21993
 
21994
+ // ../core/src/ssoIdentity.ts
21995
+ function liveSessionIdentity(stored) {
21996
+ if (!stored || typeof stored !== "object") return null;
21997
+ const identity = stored.identity;
21998
+ if (!identity || typeof identity !== "object" || !identity.issuer || !identity.subject) return null;
21999
+ const expiresAt2 = stored.expires_at ?? 0;
22000
+ if (typeof expiresAt2 !== "number") return null;
22001
+ if (expiresAt2 > 0 && Math.floor(Date.now() / 1e3) >= expiresAt2) return null;
22002
+ return identity;
22003
+ }
22004
+ var init_ssoIdentity = __esm({
22005
+ "../core/src/ssoIdentity.ts"() {
22006
+ "use strict";
22007
+ }
22008
+ });
22009
+
22053
22010
  // ../core/src/authGate.ts
22054
22011
  function enforceRouteAuth(req2, res, match, isDevAdmin) {
22055
22012
  if (!(match.secure === true && match.noAuth !== true && !isDevAdmin)) {
@@ -22057,38 +22014,50 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
22057
22014
  }
22058
22015
  const authHeader = req2.headers.authorization ?? "";
22059
22016
  const headerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
22060
- let resolvedToken = "";
22017
+ let payload = null;
22061
22018
  let tokenSource = "";
22062
- if (headerToken && validToken(headerToken)) {
22063
- resolvedToken = headerToken;
22064
- tokenSource = "header";
22019
+ let resolvedToken = "";
22020
+ if (headerToken) {
22021
+ const candidate = validToken(headerToken);
22022
+ if (isIdentityPayload(candidate)) {
22023
+ payload = candidate;
22024
+ tokenSource = "header";
22025
+ resolvedToken = headerToken;
22026
+ }
22065
22027
  }
22066
- if (!resolvedToken) {
22028
+ if (!payload) {
22067
22029
  const bodyToken = req2.body?.formToken;
22068
- if (bodyToken && validToken(bodyToken)) {
22069
- resolvedToken = bodyToken;
22070
- tokenSource = "body";
22030
+ if (bodyToken) {
22031
+ const candidate = validToken(bodyToken);
22032
+ if (isIdentityPayload(candidate)) {
22033
+ payload = candidate;
22034
+ tokenSource = "body";
22035
+ resolvedToken = bodyToken;
22036
+ }
22071
22037
  }
22072
22038
  }
22073
- if (!resolvedToken) {
22074
- const sso = req2.session?.get?.("_tina4_sso");
22075
- const identity = sso?.identity;
22076
- if (identity?.issuer && identity?.subject) {
22039
+ if (!payload) {
22040
+ const identity = liveSessionIdentity(req2.session?.get?.("_tina4_sso"));
22041
+ if (identity) {
22077
22042
  req2.user = identity;
22078
22043
  return rbacForbidden(match, identity, res);
22079
22044
  }
22080
22045
  const sessionToken = req2.session?.get?.("token");
22081
- if (sessionToken && validToken(sessionToken)) {
22082
- resolvedToken = sessionToken;
22083
- tokenSource = "session";
22046
+ if (sessionToken) {
22047
+ const candidate = validToken(sessionToken);
22048
+ if (isIdentityPayload(candidate)) {
22049
+ payload = candidate;
22050
+ tokenSource = "session";
22051
+ resolvedToken = sessionToken;
22052
+ }
22084
22053
  }
22085
22054
  }
22086
- if (!resolvedToken) {
22055
+ if (!payload) {
22087
22056
  res.raw.writeHead(401, { "Content-Type": "application/json" });
22088
22057
  res.raw.end(JSON.stringify({ error: "Unauthorized" }));
22089
22058
  return true;
22090
22059
  }
22091
- req2.user = getPayload(resolvedToken) ?? {};
22060
+ req2.user = payload;
22092
22061
  if (tokenSource === "body") {
22093
22062
  const fresh = refreshToken(resolvedToken);
22094
22063
  if (fresh) {
@@ -22138,6 +22107,7 @@ var init_authGate = __esm({
22138
22107
  "../core/src/authGate.ts"() {
22139
22108
  "use strict";
22140
22109
  init_auth();
22110
+ init_ssoIdentity();
22141
22111
  }
22142
22112
  });
22143
22113
 
@@ -23231,8 +23201,8 @@ var init_mongoHandler = __esm({
23231
23201
  if (!result || result === "__EMPTY__") return null;
23232
23202
  try {
23233
23203
  const doc = JSON.parse(result);
23234
- const expiresAt = Number(doc?.expires_at ?? 0);
23235
- if (expiresAt > 0 && Date.now() / 1e3 > expiresAt) {
23204
+ const expiresAt2 = Number(doc?.expires_at ?? 0);
23205
+ if (expiresAt2 > 0 && Date.now() / 1e3 > expiresAt2) {
23236
23206
  this.destroy(sessionId);
23237
23207
  return null;
23238
23208
  }
@@ -23458,6 +23428,9 @@ parentPort.on("message", (message) => {
23458
23428
 
23459
23429
  // ../core/src/sessionHandlers/databaseHandler.ts
23460
23430
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
23431
+ function sleepSync2(ms) {
23432
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23433
+ }
23461
23434
  function column(row, name) {
23462
23435
  if (name in row) return row[name];
23463
23436
  return row[name.toUpperCase()];
@@ -23466,7 +23439,7 @@ function schemeOf2(url) {
23466
23439
  const match = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
23467
23440
  return match ? match[1].toLowerCase() : "unknown";
23468
23441
  }
23469
- var CREATE_TABLE, DatabaseSessionHandler;
23442
+ var CREATE_TABLE, CREATE_ATTEMPTS, CREATE_RETRY_DELAY_MS, DatabaseSessionHandler;
23470
23443
  var init_databaseHandler = __esm({
23471
23444
  "../core/src/sessionHandlers/databaseHandler.ts"() {
23472
23445
  "use strict";
@@ -23546,6 +23519,8 @@ var init_databaseHandler = __esm({
23546
23519
  END
23547
23520
  `
23548
23521
  };
23522
+ CREATE_ATTEMPTS = 5;
23523
+ CREATE_RETRY_DELAY_MS = 50;
23549
23524
  DatabaseSessionHandler = class {
23550
23525
  sqliteHandle = null;
23551
23526
  /** Set for SQLite. Null when this handler talks to a networked engine. */
@@ -23586,6 +23561,7 @@ var init_databaseHandler = __esm({
23586
23561
  get sqlite() {
23587
23562
  if (this.sqliteHandle === null) {
23588
23563
  this.sqliteHandle = new DatabaseSync3(this.dbPath);
23564
+ this.sqliteHandle.exec("PRAGMA busy_timeout = 5000");
23589
23565
  this.sqliteHandle.exec("PRAGMA journal_mode = WAL");
23590
23566
  }
23591
23567
  return this.sqliteHandle;
@@ -23673,15 +23649,55 @@ var init_databaseHandler = __esm({
23673
23649
  sqlCommandSync(this.target, sql, params);
23674
23650
  }
23675
23651
  /**
23676
- * Ensure the session table exists (called once on first use).
23652
+ * Ensure the session table exists (called once on first use) - safely when
23653
+ * several processes race to create it.
23654
+ *
23655
+ * Every per-engine statement above is check-then-act, so two processes can
23656
+ * both find the table absent and both CREATE; the loser errors, and every
23657
+ * engine spells that differently (a pg_type unique violation on PostgreSQL,
23658
+ * Msg 2714 on SQL Server, an RDB$RELATIONS unique violation on Firebird). So
23659
+ * a failure is judged by RE-CHECKING whether the table now exists, never by
23660
+ * parsing the message - the rescue the Firebird note above has always
23661
+ * assumed, and which was not here. MEASURED on the lab before it: six real
23662
+ * processes racing lost one to five of them on PostgreSQL, SQLite and
23663
+ * Firebird.
23664
+ *
23665
+ * The re-check is RETRIED because the loser can fail BEFORE the winner has
23666
+ * committed. On MySQL, CREATE TABLE IF NOT EXISTS takes a shared metadata
23667
+ * lock, checks, then upgrades it to exclusive; two upgrades deadlock, and when
23668
+ * MySQL cannot back the victim off silently it answers 1213 "Deadlock found"
23669
+ * while the winner's table is still invisible (tina4-php CI run 35972320442).
23670
+ * Bounded, so a genuine failure (no CREATE grant) still throws. Same attempts
23671
+ * and delays in all four frameworks.
23677
23672
  */
23678
23673
  ensureTable() {
23679
23674
  if (this.initialized) return;
23680
23675
  const ddl = CREATE_TABLE[this.engine];
23681
- if (this.target === null) this.sqlite.exec(ddl);
23682
- else sqlCommandSync(this.target, ddl, []);
23676
+ for (let attempt = 1; ; attempt++) {
23677
+ try {
23678
+ if (this.target === null) this.sqlite.exec(ddl);
23679
+ else sqlCommandSync(this.target, ddl, []);
23680
+ break;
23681
+ } catch (error2) {
23682
+ if (this.tableExists()) break;
23683
+ if (attempt >= CREATE_ATTEMPTS) throw error2;
23684
+ sleepSync2(CREATE_RETRY_DELAY_MS * attempt);
23685
+ }
23686
+ }
23683
23687
  this.initialized = true;
23684
23688
  }
23689
+ /**
23690
+ * Whether tina4_session exists, asked the one way every engine answers alike:
23691
+ * a query against it that returns no rows succeeds only if it is there.
23692
+ */
23693
+ tableExists() {
23694
+ try {
23695
+ this.query("SELECT 1 FROM tina4_session WHERE 1 = 0", []);
23696
+ return true;
23697
+ } catch {
23698
+ return false;
23699
+ }
23700
+ }
23685
23701
  read(sessionId) {
23686
23702
  this.ensureTable();
23687
23703
  const rows = this.query(
@@ -23691,8 +23707,8 @@ var init_databaseHandler = __esm({
23691
23707
  const row = rows[0];
23692
23708
  if (!row) return null;
23693
23709
  const now = Date.now() / 1e3;
23694
- const expiresAt = Number(column(row, "expires_at") ?? 0);
23695
- if (expiresAt > 0 && expiresAt < now) {
23710
+ const expiresAt2 = Number(column(row, "expires_at") ?? 0);
23711
+ if (expiresAt2 > 0 && expiresAt2 < now) {
23696
23712
  this.destroy(sessionId);
23697
23713
  return null;
23698
23714
  }
@@ -23705,7 +23721,7 @@ var init_databaseHandler = __esm({
23705
23721
  write(sessionId, data, ttl) {
23706
23722
  this.ensureTable();
23707
23723
  const json = JSON.stringify(data);
23708
- const expiresAt = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
23724
+ const expiresAt2 = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
23709
23725
  const existing = this.query(
23710
23726
  "SELECT session_id FROM tina4_session WHERE session_id = ?",
23711
23727
  [sessionId]
@@ -23713,12 +23729,12 @@ var init_databaseHandler = __esm({
23713
23729
  if (existing.length > 0) {
23714
23730
  this.exec(
23715
23731
  "UPDATE tina4_session SET data = ?, expires_at = ? WHERE session_id = ?",
23716
- [json, expiresAt, sessionId]
23732
+ [json, expiresAt2, sessionId]
23717
23733
  );
23718
23734
  } else {
23719
23735
  this.exec(
23720
23736
  "INSERT INTO tina4_session (session_id, data, expires_at) VALUES (?, ?, ?)",
23721
- [sessionId, json, expiresAt]
23737
+ [sessionId, json, expiresAt2]
23722
23738
  );
23723
23739
  }
23724
23740
  }
@@ -24548,7 +24564,7 @@ var init_dispatchPipeline = __esm({
24548
24564
 
24549
24565
  // ../core/src/static.ts
24550
24566
  import { readFileSync as readFileSync9, realpathSync as realpathSync3, statSync as statSync8 } from "node:fs";
24551
- import { join as join16, extname as extname3, sep as sep3 } from "node:path";
24567
+ import { join as join16, extname as extname3, sep as sep4 } from "node:path";
24552
24568
  import { gzipSync as gzipSync2 } from "node:zlib";
24553
24569
  function isCompressible(contentType) {
24554
24570
  return COMPRESSIBLE_PREFIXES2.some((prefix) => contentType.includes(prefix));
@@ -24579,8 +24595,8 @@ function tryServeStatic(staticDir, req2, res) {
24579
24595
  }
24580
24596
  const stat = statSync8(realPath);
24581
24597
  if (!stat.isFile()) continue;
24582
- if (realPath !== realDir && !realPath.startsWith(realDir + sep3)) continue;
24583
- if (hasHiddenSegment(realPath.slice(realDir.length + 1), sep3)) continue;
24598
+ if (realPath !== realDir && !realPath.startsWith(realDir + sep4)) continue;
24599
+ if (hasHiddenSegment(realPath.slice(realDir.length + 1), sep4)) continue;
24584
24600
  const ext = extname3(realPath);
24585
24601
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
24586
24602
  const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs / 1e3)}"`;
@@ -25563,7 +25579,7 @@ __export(generate_exports, {
25563
25579
  toTableName: () => toTableName
25564
25580
  });
25565
25581
  import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync10 } from "node:fs";
25566
- import { join as join21, relative as relative2, resolve as resolve11, sep as sep4 } from "node:path";
25582
+ import { join as join21, relative as relative2, resolve as resolve11, sep as sep5 } from "node:path";
25567
25583
  function ensureDir(dir) {
25568
25584
  if (__resolution.dryRun) return;
25569
25585
  if (!existsSync16(dir)) {
@@ -25689,7 +25705,7 @@ function toRelPath(absPath) {
25689
25705
  const cwd = process.cwd();
25690
25706
  const rel = relative2(cwd, absPath);
25691
25707
  if (!rel) return absPath;
25692
- return sep4 === "/" ? rel : rel.split(sep4).join("/");
25708
+ return sep5 === "/" ? rel : rel.split(sep5).join("/");
25693
25709
  }
25694
25710
  function captureEditHints(absPath, content) {
25695
25711
  if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
@@ -25761,12 +25777,6 @@ function printResolution() {
25761
25777
  lines.push("");
25762
25778
  process.stderr.write(lines.join("\n"));
25763
25779
  }
25764
- function toPlural(name) {
25765
- const lower = name.toLowerCase();
25766
- if (lower.endsWith("s")) return lower;
25767
- if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
25768
- return lower + "s";
25769
- }
25770
25780
  function toCamel(name) {
25771
25781
  return name.charAt(0).toLowerCase() + name.slice(1);
25772
25782
  }
@@ -26199,7 +26209,7 @@ ${aiFill(`delete_${singular}`, {
26199
26209
  }
26200
26210
  function generateCrud(name, flags) {
26201
26211
  const table2 = resolveTable(name, flags, { announce: false });
26202
- const routeName = toPlural(table2);
26212
+ const routeName = pluralizeReserved(toSnake(name));
26203
26213
  const isPublic = Boolean(flags.public);
26204
26214
  if (!__resolution.jsonMode) console.log(`
26205
26215
  Generating CRUD for ${name}...
@@ -26453,7 +26463,7 @@ void test${titleName};
26453
26463
  function generateForm(name, flags) {
26454
26464
  const fields = fieldsOrDefault(flags.fields || "");
26455
26465
  const table2 = resolveTable(name, flags, { announce: false });
26456
- const routeName = toPlural(table2);
26466
+ const routeName = pluralizeReserved(toSnake(name));
26457
26467
  const inputTypes = {
26458
26468
  string: "text",
26459
26469
  str: "text",
@@ -26519,7 +26529,7 @@ function generateForm(name, flags) {
26519
26529
  function generateView(name, flags) {
26520
26530
  const fields = fieldsOrDefault(flags.fields || "");
26521
26531
  const table2 = resolveTable(name, flags, { announce: false });
26522
- const routeName = toPlural(table2);
26532
+ const routeName = pluralizeReserved(toSnake(name));
26523
26533
  const cols = fields.map(([f]) => f);
26524
26534
  const dir = resolve11("src/templates/pages");
26525
26535
  ensureDir(dir);
@@ -27531,7 +27541,7 @@ var init_generate = __esm({
27531
27541
  crud: ({ name, table: table2 }) => [
27532
27542
  `Apply the migration: npx tina4nodejs migrate`,
27533
27543
  `Serve and try: npx tina4nodejs serve -> visit /swagger`,
27534
- `Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
27544
+ `Run the gate test: npx tsx tests/${pluralizeReserved(toSnake(name))}.test.ts`,
27535
27545
  `Change fields: edit src/models/${name}.ts then re-run generate crud`
27536
27546
  ],
27537
27547
  migration: () => [
@@ -27549,10 +27559,10 @@ var init_generate = __esm({
27549
27559
  ],
27550
27560
  form: ({ name, table: table2 }) => [
27551
27561
  `Render from a route: res.render("forms/${table2}.twig", { item })`,
27552
- `Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
27562
+ `Add the POST route: npx tina4nodejs generate route ${pluralizeReserved(toSnake(name))} --model ${name}`
27553
27563
  ],
27554
- view: ({ table: table2 }) => [
27555
- `Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
27564
+ view: ({ name, table: table2 }) => [
27565
+ `Wire routes to render list -> ${pluralizeReserved(toSnake(name))}.twig, detail -> ${table2}.twig`,
27556
27566
  `Customize the templates in src/templates/pages/`
27557
27567
  ],
27558
27568
  auth: () => [
@@ -27713,7 +27723,7 @@ function envTruthy(val) {
27713
27723
  return ["true", "1", "yes", "on"].includes(val.trim().toLowerCase());
27714
27724
  }
27715
27725
  function isLoopback(ip) {
27716
- if (ip == null || ip === "") return true;
27726
+ if (ip == null || ip === "") return false;
27717
27727
  let addr = ip.trim().toLowerCase();
27718
27728
  if (addr.startsWith("::ffff:")) addr = addr.slice(7);
27719
27729
  return addr === "::1" || addr === "localhost" || addr.startsWith("127.");
@@ -29304,6 +29314,7 @@ __export(graphql_exports, {
29304
29314
  graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
29305
29315
  graphqlEndpoint: () => graphqlEndpoint,
29306
29316
  graphqlMaxDepth: () => graphqlMaxDepth,
29317
+ graphqlMaxNodes: () => graphqlMaxNodes,
29307
29318
  tokenize: () => tokenize2
29308
29319
  });
29309
29320
  function tokenize2(source) {
@@ -29343,7 +29354,12 @@ function graphqlMaxDepth() {
29343
29354
  const n = parseInt(raw, 10);
29344
29355
  return Number.isNaN(n) ? 50 : n;
29345
29356
  }
29346
- var TOKEN_PATTERNS, ParseError, Parser, GraphQL, GraphQLType;
29357
+ function graphqlMaxNodes() {
29358
+ const raw = (process.env.TINA4_GRAPHQL_MAX_NODES ?? "1000").trim();
29359
+ const n = parseInt(raw, 10);
29360
+ return Number.isNaN(n) ? 1e3 : n;
29361
+ }
29362
+ var TOKEN_PATTERNS, ParseError, Parser, ComplexityError, GraphQL, GraphQLType;
29347
29363
  var init_graphql = __esm({
29348
29364
  "../core/src/graphql.ts"() {
29349
29365
  "use strict";
@@ -29382,9 +29398,16 @@ var init_graphql = __esm({
29382
29398
  Parser = class {
29383
29399
  tokens;
29384
29400
  pos;
29401
+ // F2: bound the parser's own recursion. A deeply nested query
29402
+ // ("{a{a{a...}}}") otherwise recurses in parseSelectionSet until the call
29403
+ // stack overflows — BEFORE the execution-time depth guard can run. Reusing
29404
+ // TINA4_GRAPHQL_MAX_DEPTH keeps parse and execution on one bound.
29405
+ depth = 0;
29406
+ maxDepth;
29385
29407
  constructor(tokens) {
29386
29408
  this.tokens = tokens;
29387
29409
  this.pos = 0;
29410
+ this.maxDepth = graphqlMaxDepth();
29388
29411
  }
29389
29412
  peek() {
29390
29413
  return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
@@ -29466,6 +29489,17 @@ var init_graphql = __esm({
29466
29489
  };
29467
29490
  }
29468
29491
  parseSelectionSet() {
29492
+ this.depth++;
29493
+ if (this.maxDepth > 0 && this.depth > this.maxDepth) {
29494
+ throw new Error(`Query exceeds maximum depth of ${this.maxDepth}`);
29495
+ }
29496
+ try {
29497
+ return this.parseSelectionSetInner();
29498
+ } finally {
29499
+ this.depth--;
29500
+ }
29501
+ }
29502
+ parseSelectionSetInner() {
29469
29503
  this.expect("LBRACE");
29470
29504
  const selections = [];
29471
29505
  while (!this.match("RBRACE")) {
@@ -29620,6 +29654,8 @@ var init_graphql = __esm({
29620
29654
  return t;
29621
29655
  }
29622
29656
  };
29657
+ ComplexityError = class extends Error {
29658
+ };
29623
29659
  GraphQL = class _GraphQL {
29624
29660
  types = /* @__PURE__ */ new Map();
29625
29661
  queries = /* @__PURE__ */ new Map();
@@ -29642,6 +29678,12 @@ var init_graphql = __esm({
29642
29678
  * spread / inline fragment, so circular fragments are caught too.
29643
29679
  */
29644
29680
  maxDepth = graphqlMaxDepth();
29681
+ /**
29682
+ * Maximum expanded selection nodes per query (from `TINA4_GRAPHQL_MAX_NODES`,
29683
+ * default 1000; `<= 0` disables). Bounds query WIDTH — fragment bombs and
29684
+ * alias explosions — which the depth guard does not. See {@link graphqlMaxNodes}.
29685
+ */
29686
+ maxNodes = graphqlMaxNodes();
29645
29687
  /**
29646
29688
  * Decorator-style resolver registration.
29647
29689
  *
@@ -29789,6 +29831,16 @@ var init_graphql = __esm({
29789
29831
  return { data: null, errors: [{ message: "No operation found" }] };
29790
29832
  }
29791
29833
  const op = operations[0];
29834
+ if (this.maxNodes > 0) {
29835
+ try {
29836
+ this.assertComplexity(op.selections, fragments);
29837
+ } catch (e) {
29838
+ if (e instanceof ComplexityError) {
29839
+ return { data: null, errors: [{ message: e.message }] };
29840
+ }
29841
+ throw e;
29842
+ }
29843
+ }
29792
29844
  const resolvers = op.operation === "query" ? this.queries : this.mutations;
29793
29845
  for (const vdef of op.variables) {
29794
29846
  if (!(vdef.name in vars) && vdef.default !== void 0) {
@@ -29814,6 +29866,36 @@ var init_graphql = __esm({
29814
29866
  * instead of recursing until the interpreter stack overflows. Top-level
29815
29867
  * starts at depth 1; `maxDepth <= 0` disables the guard.
29816
29868
  */
29869
+ /**
29870
+ * Count the expanded selection nodes, throwing {@link ComplexityError} past
29871
+ * `maxNodes` (F2). Fragment spreads are expanded (so a fragment bomb is
29872
+ * counted, not the small unexpanded document) and every field — including
29873
+ * each alias — counts once. The count is capped by `maxNodes` and the walk by
29874
+ * `maxDepth`, so a circular fragment trips the budget instead of looping and
29875
+ * the check itself can never cost more than the budget.
29876
+ */
29877
+ assertComplexity(selections, fragments) {
29878
+ const depthCap = this.maxDepth > 0 ? this.maxDepth : 1e3;
29879
+ let count = 0;
29880
+ const walk2 = (sels, depth) => {
29881
+ if (depth > depthCap) return;
29882
+ for (const sel of sels) {
29883
+ count++;
29884
+ if (count > this.maxNodes) {
29885
+ throw new ComplexityError(`Query exceeds maximum complexity of ${this.maxNodes} nodes`);
29886
+ }
29887
+ if (sel.kind === "fragment_spread") {
29888
+ const frag = fragments.get(sel.name);
29889
+ if (frag) walk2(frag.selections, depth + 1);
29890
+ } else if (sel.kind === "inline_fragment") {
29891
+ walk2(sel.selections, depth + 1);
29892
+ } else if (sel.kind === "field" && sel.selections) {
29893
+ walk2(sel.selections, depth + 1);
29894
+ }
29895
+ }
29896
+ };
29897
+ walk2(selections, 1);
29898
+ }
29817
29899
  async resolveSelectionsInto(selections, resolvers, parent, variables, context, fragments, target, depth) {
29818
29900
  const errors = [];
29819
29901
  if (this.maxDepth > 0 && depth > this.maxDepth) {
@@ -31016,7 +31098,8 @@ function wsAuthorized(route, headers, queryString = "", subprotocol = "") {
31016
31098
  const token = wsToken(headers, queryString, subprotocol);
31017
31099
  if (!token) return [null, false];
31018
31100
  const payload = validToken(token);
31019
- return [payload, payload !== null];
31101
+ if (!isIdentityPayload(payload)) return [null, false];
31102
+ return [payload, true];
31020
31103
  }
31021
31104
  function offeredBearerSubprotocol(subprotocol) {
31022
31105
  return subprotocol.split(",").some((p) => p.trim().toLowerCase() === "bearer");
@@ -32211,12 +32294,36 @@ var init_mongoBackend = __esm({
32211
32294
  }
32212
32295
  }
32213
32296
  else if (operation === "size") {
32214
- const count = await col.countDocuments({
32215
- queue: queueName,
32216
- status: "pending",
32217
- });
32297
+ // ADR-0022 dec 7: size(status) must not answer a different question.
32298
+ // The dead aliases count the .dead_letter store (== deadLetters().length);
32299
+ // reserved/completed count by status; else pending. data = status.
32300
+ const status = data || "pending";
32301
+ let count: number;
32302
+ if (status === "dead" || status === "failed" || status === "dead_letter") {
32303
+ count = await col.countDocuments({ queue: queueName + ".dead_letter" });
32304
+ } else if (status === "reserved" || status === "completed") {
32305
+ count = await col.countDocuments({ queue: queueName, status });
32306
+ } else {
32307
+ count = await col.countDocuments({ queue: queueName, status: "pending" });
32308
+ }
32218
32309
  process.stdout.write(String(count));
32219
32310
  }
32311
+ else if (operation === "reject") {
32312
+ // Dead-letter NOW, no retry (ADR-0023). Floor attempts at maxRetries
32313
+ // so it reads as exhausted (parity with fail()'s dead-letter branch
32314
+ // and size("dead")). data = JSON { id, reason, maxRetries }.
32315
+ const info = JSON.parse(data);
32316
+ const doc = await col.findOne({ queue: queueName, id: info.id });
32317
+ if (doc) {
32318
+ const attempts = Math.max((doc.attempts || 0) + 1, info.maxRetries);
32319
+ await col.insertOne({
32320
+ ...doc, _id: undefined, attempts,
32321
+ status: "dead", queue: queueName + ".dead_letter", error: info.reason,
32322
+ });
32323
+ await col.deleteOne({ _id: doc._id, queue: queueName });
32324
+ }
32325
+ process.stdout.write("__OK__");
32326
+ }
32220
32327
  else if (operation === "clear") {
32221
32328
  await col.deleteMany({ queue: queueName });
32222
32329
  process.stdout.write("__CLEARED__");
@@ -32452,11 +32559,20 @@ var init_mongoBackend = __esm({
32452
32559
  return null;
32453
32560
  }
32454
32561
  }
32455
- size(queue) {
32456
- const result = this.execSync("size", queue);
32562
+ size(queue, status2 = "pending") {
32563
+ const result = this.execSync("size", queue, status2);
32457
32564
  const num = parseInt(result, 10);
32458
32565
  return isNaN(num) ? 0 : num;
32459
32566
  }
32567
+ /**
32568
+ * Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
32569
+ * fail(): fail() requeues until maxRetries is spent. attempts is floored at
32570
+ * maxRetries so deadLetters()/size("dead") (which filter/scope to the
32571
+ * dead-letter store) see it.
32572
+ */
32573
+ reject(queue, id, reason, maxRetries) {
32574
+ this.execSync("reject", queue, JSON.stringify({ id, reason, maxRetries }));
32575
+ }
32460
32576
  clear(queue) {
32461
32577
  this.execSync("clear", queue);
32462
32578
  }
@@ -32551,7 +32667,9 @@ function createJob(data, queue) {
32551
32667
  queue._failJob(job.topic, job, reason, queue.getMaxRetries());
32552
32668
  },
32553
32669
  reject(reason = "") {
32554
- job.fail(reason);
32670
+ job.status = "dead";
32671
+ job.error = reason;
32672
+ queue._rejectJob(job.topic, job, reason, queue.getMaxRetries());
32555
32673
  },
32556
32674
  retry(delaySeconds) {
32557
32675
  queue._retryJob(job.topic, job, delaySeconds);
@@ -33160,6 +33278,20 @@ var init_liteBackend = __esm({
33160
33278
  * dead-letter file -- two spellings of the same intent that previously
33161
33279
  * diverged.
33162
33280
  */
33281
+ /**
33282
+ * Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
33283
+ *
33284
+ * Unlike failJob(), which requeues until maxRetries is spent, rejectJob()
33285
+ * moves a known-poison job straight to the dead-letter store. attempts is
33286
+ * floored at maxRetries so deadLetters() (attempts >= maxRetries) returns it
33287
+ * and size("dead") agrees.
33288
+ */
33289
+ rejectJob(queue, job, reason, maxRetries) {
33290
+ this.clearReservation(queue, job.id);
33291
+ job.attempts = Math.max((job.attempts || 0) + 1, maxRetries);
33292
+ job.error = reason;
33293
+ this.deadLetter(queue, job, reason);
33294
+ }
33163
33295
  retryJob(queue, job, delaySeconds) {
33164
33296
  this.clearReservation(queue, job.id);
33165
33297
  try {
@@ -33381,7 +33513,7 @@ var init_queue = __esm({
33381
33513
  size(status2 = "pending") {
33382
33514
  const q = this.topic;
33383
33515
  if (this.externalBackend) {
33384
- return this.externalBackend.size(q);
33516
+ return this.externalBackend.size(q, status2);
33385
33517
  }
33386
33518
  return this.liteBackend.size(q, status2);
33387
33519
  }
@@ -33649,6 +33781,17 @@ var init_queue = __esm({
33649
33781
  }
33650
33782
  this.liteBackend.failJob(queue, job, error2, maxRetries, this._retryBackoff);
33651
33783
  }
33784
+ /**
33785
+ * Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
33786
+ * _failJob, which retries until maxRetries is spent.
33787
+ */
33788
+ _rejectJob(queue, job, reason, maxRetries) {
33789
+ if (this.externalBackend?.reject) {
33790
+ this.externalBackend.reject(queue, job.id, reason, maxRetries);
33791
+ return;
33792
+ }
33793
+ this.liteBackend.rejectJob(queue, job, reason, maxRetries);
33794
+ }
33652
33795
  /**
33653
33796
  * Re-queue a job back to the main queue directory with incremented attempts.
33654
33797
  */
@@ -35773,7 +35916,7 @@ ${end}
35773
35916
  // ../core/src/devAdmin.ts
35774
35917
  import { cpus as osCpus } from "node:os";
35775
35918
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync23, readdirSync as readdirSync15, mkdirSync as mkdirSync19, copyFileSync as copyFileSync2, statSync as statSync17, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
35776
- import { join as join28, dirname as dirname13, resolve as resolve17, relative as relative9, sep as sep7, basename as basename6 } from "node:path";
35919
+ import { join as join28, dirname as dirname13, resolve as resolve17, relative as relative9, sep as sep8, basename as basename6 } from "node:path";
35777
35920
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35778
35921
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
35779
35922
  function escapeHtml(value) {
@@ -36164,7 +36307,7 @@ function safeJoin(projectRoot3, rel, extraRoots = []) {
36164
36307
  const resolved = realPathOf(resolve17(root, rel));
36165
36308
  const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf(resolve17(r)))];
36166
36309
  for (const r of roots) {
36167
- if (resolved === r || resolved.startsWith(r.endsWith(sep7) ? r : r + sep7)) return resolved;
36310
+ if (resolved === r || resolved.startsWith(r.endsWith(sep8) ? r : r + sep8)) return resolved;
36168
36311
  }
36169
36312
  } catch {
36170
36313
  }
@@ -39360,13 +39503,20 @@ var sso_exports = {};
39360
39503
  __export(sso_exports, {
39361
39504
  SSO: () => Sso,
39362
39505
  Sso: () => Sso,
39363
- SsoError: () => SsoError
39506
+ SsoError: () => SsoError,
39507
+ liveSessionIdentity: () => liveSessionIdentity
39364
39508
  });
39365
39509
  import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
39510
+ function expiresAt(tokens) {
39511
+ const lifetime = Number(tokens.expires_in ?? 0);
39512
+ return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
39513
+ }
39366
39514
  var SsoError, Sso;
39367
39515
  var init_sso = __esm({
39368
39516
  "../core/src/sso.ts"() {
39369
39517
  "use strict";
39518
+ init_ssoIdentity();
39519
+ init_ssoIdentity();
39370
39520
  SsoError = class extends Error {
39371
39521
  };
39372
39522
  Sso = class _Sso {
@@ -39572,13 +39722,12 @@ var init_sso = __esm({
39572
39722
  access_token: tokens.access_token,
39573
39723
  refresh_token: tokens.refresh_token,
39574
39724
  id_token: tokens.id_token,
39575
- expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
39725
+ expires_at: expiresAt(tokens)
39576
39726
  });
39577
39727
  return { identity, return_to: _Sso.safeReturn(pending.return_to) };
39578
39728
  }
39579
39729
  identity(requestOrSession) {
39580
- const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
39581
- const identity = stored?.identity ?? null;
39730
+ const identity = liveSessionIdentity(this.session(requestOrSession)?.get(_Sso.SESSION_KEY));
39582
39731
  if (identity && requestOrSession?.session) requestOrSession.user = identity;
39583
39732
  return identity;
39584
39733
  }
@@ -39605,7 +39754,7 @@ var init_sso = __esm({
39605
39754
  access_token: tokens.access_token,
39606
39755
  refresh_token: tokens.refresh_token ?? stored.refresh_token,
39607
39756
  id_token: tokens.id_token ?? stored.id_token,
39608
- expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
39757
+ expires_at: expiresAt(tokens)
39609
39758
  });
39610
39759
  return identity;
39611
39760
  } catch (error2) {
@@ -39697,8 +39846,8 @@ function writeMcpDiscovery(projectRoot3, port) {
39697
39846
  const lines = contents.split(/\r?\n/);
39698
39847
  const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
39699
39848
  if (!already) {
39700
- const sep8 = contents.endsWith("\n") || contents === "" ? "" : "\n";
39701
- fs9.writeFileSync(gitignorePath, `${contents}${sep8}${GITIGNORE_LINE}
39849
+ const sep9 = contents.endsWith("\n") || contents === "" ? "" : "\n";
39850
+ fs9.writeFileSync(gitignorePath, `${contents}${sep9}${GITIGNORE_LINE}
39702
39851
  `, "utf-8");
39703
39852
  }
39704
39853
  }
@@ -39747,12 +39896,12 @@ function bannerSurfaceLines(port, opts) {
39747
39896
  ];
39748
39897
  }
39749
39898
  function swaggerAdvertised() {
39750
- const TRUTHY2 = ["true", "1", "yes", "on"];
39899
+ const TRUTHY3 = ["true", "1", "yes", "on"];
39751
39900
  const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
39752
39901
  if (raw === "") {
39753
- return TRUTHY2.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
39902
+ return TRUTHY3.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
39754
39903
  }
39755
- return TRUTHY2.includes(raw);
39904
+ return TRUTHY3.includes(raw);
39756
39905
  }
39757
39906
  async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
39758
39907
  const dir = resolve20(base, migrationDir);
@@ -40671,8 +40820,9 @@ function wireLocaleGlobal(frondEngine, base) {
40671
40820
  async function startServer(config) {
40672
40821
  loadEnv(".env.local");
40673
40822
  loadEnv();
40674
- const { ensureDevSecret: ensureDevSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
40823
+ const { ensureDevSecret: ensureDevSecret2, requireBootSecret: requireBootSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
40675
40824
  ensureDevSecret2();
40825
+ requireBootSecret2();
40676
40826
  _checkLegacyEnvVars();
40677
40827
  const resolved = resolvePortAndHost(config);
40678
40828
  const host = resolved.host;
@@ -42337,6 +42487,178 @@ var init_service = __esm({
42337
42487
  }
42338
42488
  });
42339
42489
 
42490
+ // ../core/src/ssrf.ts
42491
+ import { isIP } from "node:net";
42492
+ import { lookup } from "node:dns/promises";
42493
+ function v4ToBytes(ip) {
42494
+ const parts = ip.split(".");
42495
+ if (parts.length !== 4) return null;
42496
+ const bytes = new Uint8Array(4);
42497
+ for (let i = 0; i < 4; i++) {
42498
+ const n = Number(parts[i]);
42499
+ if (!Number.isInteger(n) || n < 0 || n > 255 || !/^\d+$/.test(parts[i])) return null;
42500
+ bytes[i] = n;
42501
+ }
42502
+ return bytes;
42503
+ }
42504
+ function v6ToBytes(input) {
42505
+ let ip = input.split("%")[0];
42506
+ const lastColon = ip.lastIndexOf(":");
42507
+ const tail = ip.slice(lastColon + 1);
42508
+ if (tail.includes(".")) {
42509
+ const v4 = v4ToBytes(tail);
42510
+ if (!v4) return null;
42511
+ const hextetA = (v4[0] << 8 | v4[1]).toString(16);
42512
+ const hextetB = (v4[2] << 8 | v4[3]).toString(16);
42513
+ ip = ip.slice(0, lastColon + 1) + hextetA + ":" + hextetB;
42514
+ }
42515
+ const halves = ip.split("::");
42516
+ if (halves.length > 2) return null;
42517
+ const head = halves[0] ? halves[0].split(":") : [];
42518
+ const back = halves.length === 2 ? halves[1] ? halves[1].split(":") : [] : [];
42519
+ let groups;
42520
+ if (halves.length === 1) {
42521
+ if (head.length !== 8) return null;
42522
+ groups = head;
42523
+ } else {
42524
+ const missing = 8 - (head.length + back.length);
42525
+ if (missing < 0) return null;
42526
+ groups = [...head, ...Array(missing).fill("0"), ...back];
42527
+ }
42528
+ if (groups.length !== 8) return null;
42529
+ const bytes = new Uint8Array(16);
42530
+ for (let i = 0; i < 8; i++) {
42531
+ if (!/^[0-9a-fA-F]{1,4}$/.test(groups[i])) return null;
42532
+ const g = parseInt(groups[i], 16);
42533
+ bytes[i * 2] = g >> 8 & 255;
42534
+ bytes[i * 2 + 1] = g & 255;
42535
+ }
42536
+ return bytes;
42537
+ }
42538
+ function ipToBytes(ip) {
42539
+ const cleaned = ip.split("%")[0];
42540
+ const family = isIP(cleaned);
42541
+ if (family === 4) return v4ToBytes(cleaned);
42542
+ if (family === 6) {
42543
+ const bytes = v6ToBytes(cleaned);
42544
+ if (!bytes) return null;
42545
+ const isMapped = bytes.subarray(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
42546
+ return isMapped ? bytes.subarray(12) : bytes;
42547
+ }
42548
+ return null;
42549
+ }
42550
+ function parseCidr(cidr) {
42551
+ const [netStr, bitsStr] = cidr.split("/");
42552
+ const net4 = ipToBytes(netStr);
42553
+ if (!net4) return null;
42554
+ return { net: net4, bits: Number(bitsStr) };
42555
+ }
42556
+ function inCidr(packed, cidr) {
42557
+ if (packed.length !== cidr.net.length) return false;
42558
+ const whole = Math.floor(cidr.bits / 8);
42559
+ const remainder = cidr.bits % 8;
42560
+ for (let i = 0; i < whole; i++) {
42561
+ if (packed[i] !== cidr.net[i]) return false;
42562
+ }
42563
+ if (remainder === 0) return true;
42564
+ const mask = 255 << 8 - remainder & 255;
42565
+ return (packed[whole] & mask) === (cidr.net[whole] & mask);
42566
+ }
42567
+ function allowPrivateRequests() {
42568
+ return TRUTHY2.has((process.env[ALLOW_PRIVATE_ENV] ?? "").trim().toLowerCase());
42569
+ }
42570
+ function isBlockedAddress(ip) {
42571
+ const packed = ipToBytes(ip);
42572
+ if (!packed) return true;
42573
+ return BLOCKED.some((cidr) => inCidr(packed, cidr));
42574
+ }
42575
+ function matchesAllowList(host, port, resolved, allowHosts) {
42576
+ const hostLower = host.toLowerCase();
42577
+ for (const raw of allowHosts ?? []) {
42578
+ const entry = String(raw).trim().toLowerCase();
42579
+ if (!entry) continue;
42580
+ if (entry === hostLower || entry === `${hostLower}:${port}`) return true;
42581
+ if (entry.includes("/")) {
42582
+ const cidr = parseCidr(entry);
42583
+ if (cidr && resolved.some((ip) => {
42584
+ const packed = ipToBytes(ip);
42585
+ return packed !== null && inCidr(packed, cidr);
42586
+ })) {
42587
+ return true;
42588
+ }
42589
+ }
42590
+ }
42591
+ return false;
42592
+ }
42593
+ function logBlock(host, ip) {
42594
+ void Promise.resolve().then(() => (init_logger(), logger_exports)).then((mod) => mod.Log?.warning?.(`SSRF guard blocked outbound request to ${host} (${ip})`)).catch(() => void 0);
42595
+ }
42596
+ async function guardUrl(url, allowHosts) {
42597
+ let parsed;
42598
+ try {
42599
+ parsed = new URL(url);
42600
+ } catch {
42601
+ throw new SsrfError(`Blocked request: '${url}' is not a valid URL`);
42602
+ }
42603
+ const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
42604
+ if (scheme !== "http" && scheme !== "https") {
42605
+ throw new SsrfError(`Blocked request: URL scheme '${scheme || "(none)"}' is not http or https`);
42606
+ }
42607
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
42608
+ if (!host) {
42609
+ throw new SsrfError("Blocked request: URL has no host");
42610
+ }
42611
+ const port = parsed.port ? Number(parsed.port) : scheme === "https" ? 443 : 80;
42612
+ if (allowPrivateRequests()) return;
42613
+ let resolved;
42614
+ try {
42615
+ const results = await lookup(host, { all: true });
42616
+ resolved = results.map((r) => r.address);
42617
+ } catch {
42618
+ throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
42619
+ }
42620
+ if (resolved.length === 0) {
42621
+ throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
42622
+ }
42623
+ if (matchesAllowList(host, port, resolved, allowHosts)) return;
42624
+ for (const ip of resolved) {
42625
+ if (isBlockedAddress(ip)) {
42626
+ logBlock(host, ip);
42627
+ throw new SsrfError(
42628
+ `Blocked request to private/internal address ${ip} (host ${host}): set ${ALLOW_PRIVATE_ENV}=true to allow, or pass an allow-list.`
42629
+ );
42630
+ }
42631
+ }
42632
+ }
42633
+ var ALLOW_PRIVATE_ENV, TRUTHY2, SsrfError, BLOCKED_CIDRS, BLOCKED;
42634
+ var init_ssrf = __esm({
42635
+ "../core/src/ssrf.ts"() {
42636
+ "use strict";
42637
+ ALLOW_PRIVATE_ENV = "TINA4_ALLOW_PRIVATE_REQUESTS";
42638
+ TRUTHY2 = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
42639
+ SsrfError = class extends Error {
42640
+ constructor(message) {
42641
+ super(message);
42642
+ this.name = "SsrfError";
42643
+ }
42644
+ };
42645
+ BLOCKED_CIDRS = [
42646
+ "0.0.0.0/8",
42647
+ "10.0.0.0/8",
42648
+ "127.0.0.0/8",
42649
+ "169.254.0.0/16",
42650
+ "172.16.0.0/12",
42651
+ "192.168.0.0/16",
42652
+ "100.64.0.0/10",
42653
+ "::1/128",
42654
+ "::/128",
42655
+ "fc00::/7",
42656
+ "fe80::/10"
42657
+ ];
42658
+ BLOCKED = BLOCKED_CIDRS.map(parseCidr).filter((c) => c !== null);
42659
+ }
42660
+ });
42661
+
42340
42662
  // ../core/src/api.ts
42341
42663
  import http from "node:http";
42342
42664
  import https from "node:https";
@@ -42493,6 +42815,7 @@ var init_api = __esm({
42493
42815
  "../core/src/api.ts"() {
42494
42816
  "use strict";
42495
42817
  init_version();
42818
+ init_ssrf();
42496
42819
  ApiStreamError = class extends Error {
42497
42820
  constructor(message, status2 = null) {
42498
42821
  super(message);
@@ -42547,6 +42870,7 @@ var init_api = __esm({
42547
42870
  transportFn;
42548
42871
  cookiesEnabled;
42549
42872
  cookies;
42873
+ allowHosts;
42550
42874
  /**
42551
42875
  * Construct an Api client.
42552
42876
  *
@@ -42586,6 +42910,7 @@ var init_api = __esm({
42586
42910
  this.transportFn = void 0;
42587
42911
  this.cookiesEnabled = false;
42588
42912
  this.cookies = {};
42913
+ this.allowHosts = [];
42589
42914
  if (typeof authHeaderOrOptions === "object" && authHeaderOrOptions !== null) {
42590
42915
  const opts = authHeaderOrOptions;
42591
42916
  this.authHeader = opts.authHeader ?? "";
@@ -42595,6 +42920,7 @@ var init_api = __esm({
42595
42920
  this.retryBackoff = opts.retryBackoff ?? 0.5;
42596
42921
  this.transportFn = opts.transport;
42597
42922
  this.cookiesEnabled = opts.cookies ?? false;
42923
+ this.allowHosts = opts.allowHosts ?? [];
42598
42924
  if (opts.bearerToken != null) {
42599
42925
  this.setBearerToken(opts.bearerToken);
42600
42926
  } else if (opts.username != null && opts.password != null) {
@@ -42908,7 +43234,12 @@ var init_api = __esm({
42908
43234
  * body-phase timeout is applied by the caller (streamBytes) via
42909
43235
  * `res.destroy()`.
42910
43236
  */
42911
- openStreamRequest(method, url, headers, data, connectSec) {
43237
+ async openStreamRequest(method, url, headers, data, connectSec) {
43238
+ try {
43239
+ await guardUrl(url, this.allowHosts);
43240
+ } catch (err) {
43241
+ throw new ApiStreamError(err instanceof Error ? err.message : String(err), null);
43242
+ }
42912
43243
  return new Promise((resolve29, reject) => {
42913
43244
  let parsed;
42914
43245
  try {
@@ -42967,10 +43298,11 @@ var init_api = __esm({
42967
43298
  */
42968
43299
  buildRequest(method, contentType, body, extraHeaders, targetUrl) {
42969
43300
  const headers = { "User-Agent": `Tina4/${TINA4_VERSION}`, ...this.headers };
42970
- if (this.authHeader) {
43301
+ const sameOriginAsBase = targetUrl === void 0 || sameOrigin(targetUrl, this.baseUrl);
43302
+ if (this.authHeader && sameOriginAsBase) {
42971
43303
  headers["Authorization"] = this.authHeader;
42972
43304
  }
42973
- if (this.cookiesEnabled) {
43305
+ if (this.cookiesEnabled && sameOriginAsBase) {
42974
43306
  const cookieHeader = this.cookieHeader();
42975
43307
  if (cookieHeader) {
42976
43308
  headers["Cookie"] = cookieHeader;
@@ -43071,7 +43403,12 @@ var init_api = __esm({
43071
43403
  * bearer token / session cookie never leaks to a host you didn't
43072
43404
  * authenticate to.
43073
43405
  */
43074
- performRequest(method, url, headers, data, redirectsLeft) {
43406
+ async performRequest(method, url, headers, data, redirectsLeft) {
43407
+ try {
43408
+ await guardUrl(url, this.allowHosts);
43409
+ } catch (err) {
43410
+ return { kind: "error", error: err instanceof Error ? err.message : String(err) };
43411
+ }
43075
43412
  return new Promise((resolve29) => {
43076
43413
  let parsed;
43077
43414
  try {
@@ -45829,6 +46166,7 @@ var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
45829
46166
  var init_push = __esm({
45830
46167
  "../core/src/push.ts"() {
45831
46168
  "use strict";
46169
+ init_ssrf();
45832
46170
  CURVE = "prime256v1";
45833
46171
  RECORD_SIZE = 4096;
45834
46172
  MAX_PAYLOAD = RECORD_SIZE - 17;
@@ -45903,10 +46241,19 @@ var init_push = __esm({
45903
46241
  return { rawPublic, rawPrivate };
45904
46242
  }
45905
46243
  async deliver(endpoint, config, keys, body) {
46244
+ try {
46245
+ await guardUrl(endpoint.toString(), this.options.allowHosts);
46246
+ } catch (error2) {
46247
+ if (error2 instanceof SsrfError) throw new PushError(error2.message);
46248
+ throw error2;
46249
+ }
45906
46250
  let response;
45907
46251
  try {
45908
46252
  response = await fetch(endpoint, {
45909
46253
  method: "POST",
46254
+ // A real push service answers the POST directly; a redirect from a push
46255
+ // endpoint is not followed to a private address (ADR-0084).
46256
+ redirect: "manual",
45910
46257
  headers: {
45911
46258
  Authorization: `vapid t=${vapidToken(endpoint.toString(), config.subject, keys.rawPrivate, keys.rawPublic)}, k=${config.publicKey}`,
45912
46259
  "Content-Encoding": "aes128gcm",
@@ -47993,6 +48340,7 @@ var init_testClient = __esm({
47993
48340
  reqHeaders["content-length"] = String(Buffer.byteLength(rawBody));
47994
48341
  }
47995
48342
  const socket = new Socket();
48343
+ Object.defineProperty(socket, "remoteAddress", { value: "127.0.0.1" });
47996
48344
  const rawReq = new IncomingMessage2(socket);
47997
48345
  rawReq.method = method.toUpperCase();
47998
48346
  rawReq.url = path10;
@@ -48365,6 +48713,7 @@ var init_validator = __esm({
48365
48713
  var src_exports3 = {};
48366
48714
  __export(src_exports3, {
48367
48715
  AI_TOOLS: () => AI_TOOLS,
48716
+ ALLOW_PRIVATE_ENV: () => ALLOW_PRIVATE_ENV,
48368
48717
  APPLICATION_FORM: () => APPLICATION_FORM,
48369
48718
  APPLICATION_JSON: () => APPLICATION_JSON,
48370
48719
  APPLICATION_OCTET: () => APPLICATION_OCTET,
@@ -48472,6 +48821,7 @@ __export(src_exports3, {
48472
48821
  Session: () => Session,
48473
48822
  Sso: () => Sso,
48474
48823
  SsoError: () => SsoError,
48824
+ SsrfError: () => SsrfError,
48475
48825
  TAKEOVER_KILLED: () => TAKEOVER_KILLED,
48476
48826
  TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
48477
48827
  TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
@@ -48505,6 +48855,7 @@ __export(src_exports3, {
48505
48855
  addHtmlHelpers: () => addHtmlHelpers,
48506
48856
  algorithmAvailable: () => algorithmAvailable,
48507
48857
  allEnv: () => allEnv,
48858
+ allowPrivateRequests: () => allowPrivateRequests,
48508
48859
  any: () => any,
48509
48860
  attachCsrfFromEnv: () => attachCsrfFromEnv,
48510
48861
  authMiddleware: () => authMiddleware,
@@ -48570,6 +48921,7 @@ __export(src_exports3, {
48570
48921
  graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
48571
48922
  graphqlEndpoint: () => graphqlEndpoint,
48572
48923
  graphqlMaxDepth: () => graphqlMaxDepth,
48924
+ guardUrl: () => guardUrl,
48573
48925
  handle: () => handle,
48574
48926
  handleFeedbackTurn: () => handleFeedbackTurn,
48575
48927
  handleFeedbackWidgetJs: () => handleFeedbackWidgetJs,
@@ -48583,6 +48935,7 @@ __export(src_exports3, {
48583
48935
  installAll: () => installAll,
48584
48936
  installSelected: () => installSelected,
48585
48937
  isBannerSuppressed: () => isBannerSuppressed,
48938
+ isBlockedAddress: () => isBlockedAddress,
48586
48939
  isDebugMode: () => isDebugMode,
48587
48940
  isDev: () => isDev,
48588
48941
  isInstalled: () => isInstalled,
@@ -48702,6 +49055,7 @@ var init_src3 = __esm({
48702
49055
  init_service();
48703
49056
  init_cache();
48704
49057
  init_api();
49058
+ init_ssrf();
48705
49059
  init_context();
48706
49060
  init_events();
48707
49061
  init_devAdmin();