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
@@ -950,6 +950,8 @@ var auth_exports = {};
950
950
  __export(auth_exports, {
951
951
  Auth: () => Auth,
952
952
  JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
953
+ MIN_SECRET_BYTES: () => MIN_SECRET_BYTES,
954
+ NON_IDENTITY_TOKEN_TYPES: () => NON_IDENTITY_TOKEN_TYPES,
953
955
  algorithmAvailable: () => algorithmAvailable,
954
956
  authMiddleware: () => authMiddleware,
955
957
  authenticateRequest: () => authenticateRequest,
@@ -959,7 +961,10 @@ __export(auth_exports, {
959
961
  getPayload: () => getPayload,
960
962
  getToken: () => getToken,
961
963
  hashPassword: () => hashPassword,
964
+ insecureSecretMessage: () => insecureSecretMessage,
965
+ isIdentityPayload: () => isIdentityPayload,
962
966
  refreshToken: () => refreshToken,
967
+ requireBootSecret: () => requireBootSecret,
963
968
  resolveAlgorithm: () => resolveAlgorithm,
964
969
  validToken: () => validToken,
965
970
  validateApiKey: () => validateApiKey
@@ -995,6 +1000,23 @@ async function _logWarning(message) {
995
1000
  function _warnBlankSecret() {
996
1001
  void _logWarning(BLANK_SECRET_WARNING);
997
1002
  }
1003
+ function isIdentityPayload(payload) {
1004
+ return payload != null && !NON_IDENTITY_TOKEN_TYPES.includes(payload.type);
1005
+ }
1006
+ function insecureSecretMessage(secret) {
1007
+ const length = Buffer.byteLength(secret ?? "", "utf8");
1008
+ if (length >= MIN_SECRET_BYTES) return null;
1009
+ const what = length === 0 ? "is not set" : `is ${length} bytes`;
1010
+ 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.`;
1011
+ }
1012
+ function requireBootSecret() {
1013
+ const algorithm = (process.env.TINA4_JWT_ALGORITHM ?? "").trim() || "HS256";
1014
+ if (RSA_SIGN_ALGORITHMS.has(algorithm)) return;
1015
+ const secret = process.env.TINA4_SECRET ?? "";
1016
+ if (secret === "" && _isDev()) return;
1017
+ const message = insecureSecretMessage(secret);
1018
+ if (message) throw new Error(message);
1019
+ }
998
1020
  function ensureDevSecret(cwd) {
999
1021
  if (process.env.TINA4_SECRET) return null;
1000
1022
  if (!_isDev() || _isCi() || _isProduction()) {
@@ -1125,6 +1147,13 @@ function validToken(token, secret, algorithm) {
1125
1147
  const [h, p, sig] = parts;
1126
1148
  const header = JSON.parse(base64urlDecode(h).toString());
1127
1149
  if (header.alg !== resolvedAlgorithm) return null;
1150
+ if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
1151
+ const weak = insecureSecretMessage(resolvedSecret);
1152
+ if (weak) {
1153
+ void _logWarning(weak);
1154
+ return null;
1155
+ }
1156
+ }
1128
1157
  const signingInput = `${h}.${p}`;
1129
1158
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
1130
1159
  return null;
@@ -1156,6 +1185,8 @@ function getPayload(token) {
1156
1185
  function sign(input, secret, algorithm) {
1157
1186
  const digest = HMAC_DIGESTS.get(algorithm);
1158
1187
  if (digest) {
1188
+ const weak = insecureSecretMessage(secret);
1189
+ if (weak) throw new Error(weak);
1159
1190
  return base64urlEncode(createHmac(digest, secret).update(input).digest());
1160
1191
  }
1161
1192
  const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
@@ -1213,17 +1244,17 @@ function authMiddleware(secret, algorithm) {
1213
1244
  return;
1214
1245
  }
1215
1246
  const token = authHeader.slice(7);
1216
- if (!validToken(token, secret, algorithm)) {
1247
+ const payload = validToken(token, secret, algorithm);
1248
+ if (!isIdentityPayload(payload)) {
1217
1249
  res({ error: "Unauthorized" }, 401);
1218
1250
  return;
1219
1251
  }
1220
- req2.auth = getPayload(token);
1252
+ req2.auth = payload;
1221
1253
  next();
1222
1254
  };
1223
1255
  }
1224
1256
  function refreshToken(token, expiresIn = 60) {
1225
- if (!validToken(token)) return null;
1226
- const payload = getPayload(token);
1257
+ const payload = validToken(token);
1227
1258
  if (!payload) return null;
1228
1259
  const { iat: _iat, exp: _exp, ...claims } = payload;
1229
1260
  return getToken(claims, expiresIn);
@@ -1232,7 +1263,8 @@ function authenticateRequest(headers, secret, algorithm) {
1232
1263
  const authHeader = headers.authorization ?? headers.Authorization ?? "";
1233
1264
  if (!authHeader.startsWith("Bearer ")) return null;
1234
1265
  const token = authHeader.slice(7);
1235
- if (validToken(token, secret, algorithm)) return getPayload(token);
1266
+ const payload = validToken(token, secret, algorithm);
1267
+ if (payload) return isIdentityPayload(payload) ? payload : null;
1236
1268
  if (validateApiKey(token)) {
1237
1269
  return { _auth: "api_key" };
1238
1270
  }
@@ -1246,12 +1278,14 @@ function validateApiKey(provided, expected) {
1246
1278
  if (a.length !== b.length) return false;
1247
1279
  return timingSafeEqual(a, b);
1248
1280
  }
1249
- var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, KNOWN_ALGORITHMS, ALGORITHM_REMEDY, capabilityCache, JWT_LEEWAY_SECONDS, Auth;
1281
+ 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;
1250
1282
  var init_auth = __esm({
1251
1283
  "src/auth.ts"() {
1252
1284
  "use strict";
1253
1285
  init_dotenv();
1254
1286
  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.";
1287
+ NON_IDENTITY_TOKEN_TYPES = ["form"];
1288
+ MIN_SECRET_BYTES = 32;
1255
1289
  HMAC_DIGESTS = /* @__PURE__ */ new Map([
1256
1290
  ["HS256", "sha256"],
1257
1291
  ["HS384", "sha384"],
@@ -3540,14 +3574,72 @@ var init_cachedDatabase = __esm({
3540
3574
  }
3541
3575
  });
3542
3576
 
3577
+ // ../orm/src/adapters/sqlCrud.ts
3578
+ function buildInsertRow(dialect, table2, data, suffix = "", options = {}) {
3579
+ const keys = Object.keys(data);
3580
+ const sql = buildInsert(dialect, table2, keys, suffix, options.startAt ?? 1);
3581
+ return { sql, values: Object.values(data) };
3582
+ }
3583
+ function buildInsertRows(dialect, table2, rows) {
3584
+ if (rows.length === 0) return null;
3585
+ const keys = Object.keys(rows[0]);
3586
+ const sql = buildInsert(dialect, table2, keys);
3587
+ const paramsList = rows.map((row) => keys.map((k) => row[k]));
3588
+ return { sql, paramsList };
3589
+ }
3590
+ function buildUpdate(dialect, table2, data, filter, params, options = {}) {
3591
+ const startAt = options.startAt ?? 1;
3592
+ const convertFragment = options.convertFragment ?? identityFragment;
3593
+ const dataKeys = Object.keys(data);
3594
+ const setClauses = buildSetClause(dialect, dataKeys, startAt);
3595
+ const whereStart = startAt + dataKeys.length;
3596
+ if (typeof filter === "string") {
3597
+ const where = filter ? ` WHERE ${convertFragment(filter, whereStart)}` : "";
3598
+ return {
3599
+ sql: `UPDATE ${dialect.quote(table2)} SET ${setClauses}${where}`,
3600
+ values: [...Object.values(data), ...params ?? []]
3601
+ };
3602
+ }
3603
+ const whereClauses = buildWhereClause(dialect, Object.keys(filter), whereStart);
3604
+ return {
3605
+ sql: `UPDATE ${dialect.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`,
3606
+ values: [...Object.values(data), ...Object.values(filter)]
3607
+ };
3608
+ }
3609
+ function buildDelete(dialect, table2, filter, params, options = {}) {
3610
+ const startAt = options.startAt ?? 1;
3611
+ const convertFragment = options.convertFragment ?? identityFragment;
3612
+ if (typeof filter === "string") {
3613
+ const where = filter ? ` WHERE ${convertFragment(filter, startAt)}` : "";
3614
+ return {
3615
+ sql: `DELETE FROM ${dialect.quote(table2)}${where}`,
3616
+ values: params ?? []
3617
+ };
3618
+ }
3619
+ const whereClauses = buildWhereClause(dialect, Object.keys(filter), startAt);
3620
+ return {
3621
+ sql: `DELETE FROM ${dialect.quote(table2)} WHERE ${whereClauses}`,
3622
+ values: Object.values(filter)
3623
+ };
3624
+ }
3625
+ var identityFragment;
3626
+ var init_sqlCrud = __esm({
3627
+ "../orm/src/adapters/sqlCrud.ts"() {
3628
+ "use strict";
3629
+ init_sqlDialect();
3630
+ identityFragment = (fragment) => fragment;
3631
+ }
3632
+ });
3633
+
3543
3634
  // ../orm/src/adapters/sqlite.ts
3544
3635
  var sqlite_exports = {};
3545
3636
  __export(sqlite_exports, {
3546
- SQLiteAdapter: () => SQLiteAdapter
3637
+ SQLiteAdapter: () => SQLiteAdapter,
3638
+ resolveSqlitePath: () => resolveSqlitePath
3547
3639
  });
3548
3640
  import { DatabaseSync } from "node:sqlite";
3549
3641
  import { mkdirSync as mkdirSync2 } from "node:fs";
3550
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
3642
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve2, sep } from "node:path";
3551
3643
  function isIdentifier(str) {
3552
3644
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(str);
3553
3645
  }
@@ -3578,17 +3670,20 @@ function sqliteVersionAtLeast(major, minor, patch2) {
3578
3670
  }
3579
3671
  function resolveSqlitePath(dbPath) {
3580
3672
  if (dbPath === ":memory:") return dbPath;
3581
- let path10 = dbPath;
3582
- if (!isAbsolute2(path10)) {
3583
- path10 = join4(process.cwd(), path10);
3584
- mkdirSync2(dirname3(path10), { recursive: true });
3585
- } else {
3586
- const cwd = resolve2(process.cwd());
3587
- const abs = resolve2(path10);
3588
- if (abs.startsWith(cwd + "/") || abs === cwd) {
3589
- mkdirSync2(dirname3(abs), { recursive: true });
3590
- }
3673
+ const isWindowsAbs = /^[A-Za-z]:[\\/]/.test(dbPath);
3674
+ if (isAbsolute2(dbPath) || isWindowsAbs) {
3675
+ return dbPath;
3676
+ }
3677
+ const cwd = resolve2(process.cwd());
3678
+ const path10 = join4(cwd, dbPath);
3679
+ const parent = dirname3(path10);
3680
+ const absParent = resolve2(parent);
3681
+ if (absParent !== cwd && !absParent.startsWith(cwd + sep)) {
3682
+ throw new Error(
3683
+ `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.`
3684
+ );
3591
3685
  }
3686
+ mkdirSync2(parent, { recursive: true, mode: 509 });
3592
3687
  return path10;
3593
3688
  }
3594
3689
  function fieldTypeToSQLite(def) {
@@ -3624,6 +3719,7 @@ var init_sqlite = __esm({
3624
3719
  "../orm/src/adapters/sqlite.ts"() {
3625
3720
  "use strict";
3626
3721
  init_sqlDialect();
3722
+ init_sqlCrud();
3627
3723
  init_sqlTranslator();
3628
3724
  _sqliteVersionInfo = null;
3629
3725
  SQLiteAdapter = class {
@@ -3744,16 +3840,12 @@ var init_sqlite = __esm({
3744
3840
  }
3745
3841
  insert(table2, data) {
3746
3842
  if (Array.isArray(data)) {
3747
- if (data.length === 0) return { success: true, affectedRows: 0 };
3748
- const keys2 = Object.keys(data[0]);
3749
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
3750
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
3751
- const result = this.executeMany(sql2, paramsList);
3843
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
3844
+ if (!batch) return { success: true, affectedRows: 0 };
3845
+ const result = this.executeMany(batch.sql, batch.paramsList);
3752
3846
  return { success: true, affectedRows: result.affectedRows, lastId: result.lastId };
3753
3847
  }
3754
- const keys = Object.keys(data);
3755
- const sql = buildInsert(ANSI_DIALECT, table2, keys);
3756
- const values = Object.values(data);
3848
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table2, data);
3757
3849
  try {
3758
3850
  const result = this.db.prepare(sql).run(...toSqlParams(values));
3759
3851
  this._lastInsertId = result.lastInsertRowid;
@@ -3763,21 +3855,7 @@ var init_sqlite = __esm({
3763
3855
  }
3764
3856
  }
3765
3857
  update(table2, data, filter, params) {
3766
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
3767
- if (typeof filter === "string") {
3768
- const where = filter ? ` WHERE ${filter}` : "";
3769
- const sql2 = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses}${where}`;
3770
- const values2 = [...Object.values(data), ...params ?? []];
3771
- try {
3772
- const result = this.db.prepare(sql2).run(...toSqlParams(values2));
3773
- return { success: true, affectedRows: Number(result.changes) };
3774
- } catch (e) {
3775
- return { success: false, affectedRows: 0, error: e.message };
3776
- }
3777
- }
3778
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
3779
- const sql = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
3780
- const values = [...Object.values(data), ...Object.values(filter)];
3858
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table2, data, filter, params);
3781
3859
  try {
3782
3860
  const result = this.db.prepare(sql).run(...toSqlParams(values));
3783
3861
  return { success: true, affectedRows: Number(result.changes) };
@@ -3794,18 +3872,7 @@ var init_sqlite = __esm({
3794
3872
  }
3795
3873
  return { success: true, affectedRows: totalAffected };
3796
3874
  }
3797
- if (typeof filter === "string") {
3798
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${filter}` : `DELETE FROM "${table2}"`;
3799
- try {
3800
- const result = this.db.prepare(sql2).run(...toSqlParams(params ?? []));
3801
- return { success: true, affectedRows: Number(result.changes) };
3802
- } catch (e) {
3803
- return { success: false, affectedRows: 0, error: e.message };
3804
- }
3805
- }
3806
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
3807
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table2)} WHERE ${whereClauses}`;
3808
- const values = Object.values(filter);
3875
+ const { sql, values } = buildDelete(ANSI_DIALECT, table2, filter, params);
3809
3876
  try {
3810
3877
  const result = this.db.prepare(sql).run(...toSqlParams(values));
3811
3878
  return { success: true, affectedRows: Number(result.changes) };
@@ -4097,6 +4164,7 @@ var init_postgres = __esm({
4097
4164
  "../orm/src/adapters/postgres.ts"() {
4098
4165
  "use strict";
4099
4166
  init_sqlDialect();
4167
+ init_sqlCrud();
4100
4168
  init_sqlTranslator();
4101
4169
  init_connectTimeout();
4102
4170
  pg = null;
@@ -4298,21 +4366,17 @@ var init_postgres = __esm({
4298
4366
  async insertAsync(table2, data) {
4299
4367
  this.ensureConnected();
4300
4368
  if (Array.isArray(data)) {
4301
- if (data.length === 0) return { success: true, affectedRows: 0 };
4302
- const keys2 = Object.keys(data[0]);
4303
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
4304
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
4369
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
4370
+ if (!batch) return { success: true, affectedRows: 0 };
4305
4371
  try {
4306
- const result = await this.executeManyAsync(sql2, paramsList);
4372
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
4307
4373
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
4308
4374
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
4309
4375
  } catch (e) {
4310
4376
  return { success: false, affectedRows: 0, error: e.message };
4311
4377
  }
4312
4378
  }
4313
- const keys = Object.keys(data);
4314
- const sql = buildInsert(POSTGRES_DIALECT, table2, keys, " RETURNING *");
4315
- const values = Object.values(data);
4379
+ const { sql, values } = buildInsertRow(POSTGRES_DIALECT, table2, data, " RETURNING *");
4316
4380
  try {
4317
4381
  const result = await this.client.query(sql, values);
4318
4382
  const insertedRow = result.rows[0];
@@ -4332,26 +4396,10 @@ var init_postgres = __esm({
4332
4396
  }
4333
4397
  async updateAsync(table2, data, filter, params) {
4334
4398
  this.ensureConnected();
4335
- const dataKeys = Object.keys(data);
4336
- let paramIndex = 1;
4337
- const setClauses = buildSetClause(POSTGRES_DIALECT, dataKeys, paramIndex);
4338
- paramIndex += dataKeys.length;
4339
- if (typeof filter === "string") {
4340
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
4341
- const sql2 = `UPDATE ${POSTGRES_DIALECT.quote(table2)} SET ${setClauses}${where}`;
4342
- const values2 = [...Object.values(data), ...params ?? []];
4343
- try {
4344
- const result = await this.client.query(sql2, values2);
4345
- return { success: true, affectedRows: result.rowCount ?? 0 };
4346
- } catch (e) {
4347
- return { success: false, affectedRows: 0, error: e.message };
4348
- }
4349
- }
4350
- const filterKeys = Object.keys(filter);
4351
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
4352
- paramIndex += filterKeys.length;
4353
- const sql = `UPDATE ${POSTGRES_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
4354
- const values = [...Object.values(data), ...Object.values(filter)];
4399
+ const { sql, values } = buildUpdate(POSTGRES_DIALECT, table2, data, filter, params, {
4400
+ startAt: 1,
4401
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt)
4402
+ });
4355
4403
  try {
4356
4404
  const result = await this.client.query(sql, values);
4357
4405
  return { success: true, affectedRows: result.rowCount ?? 0 };
@@ -4364,21 +4412,10 @@ var init_postgres = __esm({
4364
4412
  }
4365
4413
  async deleteAsync(table2, filter, params) {
4366
4414
  this.ensureConnected();
4367
- if (typeof filter === "string") {
4368
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${this.bindable(filter, params)}` : `DELETE FROM "${table2}"`;
4369
- try {
4370
- const result = await this.client.query(sql2, params ?? []);
4371
- return { success: true, affectedRows: result.rowCount ?? 0 };
4372
- } catch (e) {
4373
- return { success: false, affectedRows: 0, error: e.message };
4374
- }
4375
- }
4376
- const filterKeys = Object.keys(filter);
4377
- let paramIndex = 1;
4378
- const whereClauses = buildWhereClause(POSTGRES_DIALECT, filterKeys, paramIndex);
4379
- paramIndex += filterKeys.length;
4380
- const sql = `DELETE FROM ${POSTGRES_DIALECT.quote(table2)} WHERE ${whereClauses}`;
4381
- const values = Object.values(filter);
4415
+ const { sql, values } = buildDelete(POSTGRES_DIALECT, table2, filter, params, {
4416
+ startAt: 1,
4417
+ convertFragment: (fragment) => this.bindable(fragment, params)
4418
+ });
4382
4419
  try {
4383
4420
  const result = await this.client.query(sql, values);
4384
4421
  return { success: true, affectedRows: result.rowCount ?? 0 };
@@ -4552,6 +4589,7 @@ var init_mysql = __esm({
4552
4589
  "../orm/src/adapters/mysql.ts"() {
4553
4590
  "use strict";
4554
4591
  init_sqlDialect();
4592
+ init_sqlCrud();
4555
4593
  init_sqlTranslator();
4556
4594
  init_connectTimeout();
4557
4595
  mysql2 = null;
@@ -4733,21 +4771,17 @@ var init_mysql = __esm({
4733
4771
  async insertAsync(table2, data) {
4734
4772
  this.ensureConnected();
4735
4773
  if (Array.isArray(data)) {
4736
- if (data.length === 0) return { success: true, affectedRows: 0 };
4737
- const keys2 = Object.keys(data[0]);
4738
- const sql2 = buildInsert(MYSQL_DIALECT, table2, keys2);
4739
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
4774
+ const batch = buildInsertRows(MYSQL_DIALECT, table2, data);
4775
+ if (!batch) return { success: true, affectedRows: 0 };
4740
4776
  try {
4741
- const result = await this.executeManyAsync(sql2, paramsList);
4777
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
4742
4778
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
4743
4779
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
4744
4780
  } catch (e) {
4745
4781
  return { success: false, affectedRows: 0, error: e.message };
4746
4782
  }
4747
4783
  }
4748
- const keys = Object.keys(data);
4749
- const sql = buildInsert(MYSQL_DIALECT, table2, keys);
4750
- const values = Object.values(data);
4784
+ const { sql, values } = buildInsertRow(MYSQL_DIALECT, table2, data);
4751
4785
  try {
4752
4786
  const result = await this.queryPromise(sql, values);
4753
4787
  this._lastInsertId = result.insertId ?? null;
@@ -4765,21 +4799,7 @@ var init_mysql = __esm({
4765
4799
  }
4766
4800
  async updateAsync(table2, data, filter, params) {
4767
4801
  this.ensureConnected();
4768
- const setClauses = buildSetClause(MYSQL_DIALECT, Object.keys(data));
4769
- if (typeof filter === "string") {
4770
- const where = filter ? ` WHERE ${filter}` : "";
4771
- const sql2 = `UPDATE ${MYSQL_DIALECT.quote(table2)} SET ${setClauses}${where}`;
4772
- const values2 = [...Object.values(data), ...params ?? []];
4773
- try {
4774
- const result = await this.queryPromise(sql2, values2);
4775
- return { success: true, affectedRows: result.affectedRows ?? 0 };
4776
- } catch (e) {
4777
- return { success: false, affectedRows: 0, error: e.message };
4778
- }
4779
- }
4780
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
4781
- const sql = `UPDATE ${MYSQL_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
4782
- const values = [...Object.values(data), ...Object.values(filter)];
4802
+ const { sql, values } = buildUpdate(MYSQL_DIALECT, table2, data, filter, params);
4783
4803
  try {
4784
4804
  const result = await this.queryPromise(sql, values);
4785
4805
  return { success: true, affectedRows: result.affectedRows ?? 0 };
@@ -4792,18 +4812,7 @@ var init_mysql = __esm({
4792
4812
  }
4793
4813
  async deleteAsync(table2, filter, params) {
4794
4814
  this.ensureConnected();
4795
- if (typeof filter === "string") {
4796
- const sql2 = filter ? `DELETE FROM \`${table2}\` WHERE ${filter}` : `DELETE FROM \`${table2}\``;
4797
- try {
4798
- const result = await this.queryPromise(sql2, params ?? []);
4799
- return { success: true, affectedRows: result.affectedRows ?? 0 };
4800
- } catch (e) {
4801
- return { success: false, affectedRows: 0, error: e.message };
4802
- }
4803
- }
4804
- const whereClauses = buildWhereClause(MYSQL_DIALECT, Object.keys(filter));
4805
- const sql = `DELETE FROM ${MYSQL_DIALECT.quote(table2)} WHERE ${whereClauses}`;
4806
- const values = Object.values(filter);
4815
+ const { sql, values } = buildDelete(MYSQL_DIALECT, table2, filter, params);
4807
4816
  try {
4808
4817
  const result = await this.queryPromise(sql, values);
4809
4818
  return { success: true, affectedRows: result.affectedRows ?? 0 };
@@ -4955,6 +4964,7 @@ var init_mssql = __esm({
4955
4964
  "../orm/src/adapters/mssql.ts"() {
4956
4965
  "use strict";
4957
4966
  init_sqlDialect();
4967
+ init_sqlCrud();
4958
4968
  init_sqlTranslator();
4959
4969
  init_connectTimeout();
4960
4970
  tedious = null;
@@ -5211,21 +5221,17 @@ var init_mssql = __esm({
5211
5221
  async insertAsync(table2, data) {
5212
5222
  this.ensureConnected();
5213
5223
  if (Array.isArray(data)) {
5214
- if (data.length === 0) return { success: true, affectedRows: 0 };
5215
- const keys2 = Object.keys(data[0]);
5216
- const sql2 = buildInsert({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table2, keys2);
5217
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
5224
+ const batch = buildInsertRows({ quote: MSSQL_DIALECT.quote, marker: ANSI_DIALECT.marker }, table2, data);
5225
+ if (!batch) return { success: true, affectedRows: 0 };
5218
5226
  try {
5219
- const result = await this.executeManyAsync(sql2, paramsList);
5227
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
5220
5228
  if (result.lastId !== void 0) this._lastInsertId = result.lastId;
5221
5229
  return { success: true, affectedRows: result.totalAffected, lastId: result.lastId };
5222
5230
  } catch (e) {
5223
5231
  return { success: false, affectedRows: 0, error: e.message };
5224
5232
  }
5225
5233
  }
5226
- const keys = Object.keys(data);
5227
- const sql = buildInsert(MSSQL_DIALECT, table2, keys, "; SELECT SCOPE_IDENTITY() AS id", 0);
5228
- const values = Object.values(data);
5234
+ const { sql, values } = buildInsertRow(MSSQL_DIALECT, table2, data, "; SELECT SCOPE_IDENTITY() AS id", { startAt: 0 });
5229
5235
  try {
5230
5236
  const result = await this.execSqlPromise(sql, values);
5231
5237
  const id = result.rows[0]?.id ?? null;
@@ -5248,26 +5254,10 @@ var init_mssql = __esm({
5248
5254
  }
5249
5255
  async updateAsync(table2, data, filter, params) {
5250
5256
  this.ensureConnected();
5251
- const dataKeys = Object.keys(data);
5252
- let paramIndex = 0;
5253
- const setClauses = buildSetClause(MSSQL_DIALECT, dataKeys, paramIndex);
5254
- paramIndex += dataKeys.length;
5255
- if (typeof filter === "string") {
5256
- const where = filter ? ` WHERE ${this.convertPlaceholders(filter, paramIndex)}` : "";
5257
- const sql2 = `UPDATE ${MSSQL_DIALECT.quote(table2)} SET ${setClauses}${where}`;
5258
- const values2 = [...Object.values(data), ...params ?? []];
5259
- try {
5260
- const result = await this.execSqlPromise(sql2, values2);
5261
- return { success: true, affectedRows: result.rowCount };
5262
- } catch (e) {
5263
- return { success: false, affectedRows: 0, error: e.message };
5264
- }
5265
- }
5266
- const filterKeys = Object.keys(filter);
5267
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
5268
- paramIndex += filterKeys.length;
5269
- const sql = `UPDATE ${MSSQL_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
5270
- const values = [...Object.values(data), ...Object.values(filter)];
5257
+ const { sql, values } = buildUpdate(MSSQL_DIALECT, table2, data, filter, params, {
5258
+ startAt: 0,
5259
+ convertFragment: (fragment, startAt) => this.convertPlaceholders(fragment, startAt)
5260
+ });
5271
5261
  try {
5272
5262
  const result = await this.execSqlPromise(sql, values);
5273
5263
  return { success: true, affectedRows: result.rowCount };
@@ -5280,21 +5270,10 @@ var init_mssql = __esm({
5280
5270
  }
5281
5271
  async deleteAsync(table2, filter, params) {
5282
5272
  this.ensureConnected();
5283
- if (typeof filter === "string") {
5284
- const sql2 = filter ? `DELETE FROM [${table2}] WHERE ${this.bindable(filter, params)}` : `DELETE FROM [${table2}]`;
5285
- try {
5286
- const result = await this.execSqlPromise(sql2, params ?? []);
5287
- return { success: true, affectedRows: result.rowCount };
5288
- } catch (e) {
5289
- return { success: false, affectedRows: 0, error: e.message };
5290
- }
5291
- }
5292
- const filterKeys = Object.keys(filter);
5293
- let paramIndex = 0;
5294
- const whereClauses = buildWhereClause(MSSQL_DIALECT, filterKeys, paramIndex);
5295
- paramIndex += filterKeys.length;
5296
- const sql = `DELETE FROM ${MSSQL_DIALECT.quote(table2)} WHERE ${whereClauses}`;
5297
- const values = Object.values(filter);
5273
+ const { sql, values } = buildDelete(MSSQL_DIALECT, table2, filter, params, {
5274
+ startAt: 0,
5275
+ convertFragment: (fragment) => this.bindable(fragment, params)
5276
+ });
5298
5277
  try {
5299
5278
  const result = await this.execSqlPromise(sql, values);
5300
5279
  return { success: true, affectedRows: result.rowCount };
@@ -5547,6 +5526,7 @@ var init_firebird = __esm({
5547
5526
  "../orm/src/adapters/firebird.ts"() {
5548
5527
  "use strict";
5549
5528
  init_sqlDialect();
5529
+ init_sqlCrud();
5550
5530
  init_sqlTranslator();
5551
5531
  init_connectTimeout();
5552
5532
  firebird = null;
@@ -5917,17 +5897,13 @@ var init_firebird = __esm({
5917
5897
  async insertAsync(table2, data) {
5918
5898
  this.ensureConnected();
5919
5899
  if (Array.isArray(data)) {
5920
- if (data.length === 0) return { success: true, affectedRows: 0 };
5921
- const keys2 = Object.keys(data[0]);
5922
- const sql2 = buildInsert(FB_DIALECT, table2, keys2);
5923
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
5924
- const result = await this.executeManyAsync(sql2, paramsList);
5900
+ const batch = buildInsertRows(FB_DIALECT, table2, data);
5901
+ if (!batch) return { success: true, affectedRows: 0 };
5902
+ const result = await this.executeManyAsync(batch.sql, batch.paramsList);
5925
5903
  const lastId2 = await this.readGeneratorId(table2) ?? result.lastId;
5926
5904
  return { success: true, affectedRows: result.totalAffected, lastId: lastId2 ?? void 0 };
5927
5905
  }
5928
- const keys = Object.keys(data);
5929
- const sql = buildInsert(FB_DIALECT, table2, keys);
5930
- const values = Object.values(data);
5906
+ const { sql, values } = buildInsertRow(FB_DIALECT, table2, data);
5931
5907
  await this.executePromise(sql, values);
5932
5908
  const lastId = await this.readGeneratorId(table2);
5933
5909
  return { success: true, affectedRows: 1, lastId: lastId ?? void 0 };
@@ -5937,18 +5913,7 @@ var init_firebird = __esm({
5937
5913
  }
5938
5914
  async updateAsync(table2, data, filter, params) {
5939
5915
  this.ensureConnected();
5940
- const setClauses = buildSetClause(FB_DIALECT, Object.keys(data));
5941
- if (typeof filter === "string") {
5942
- const where = filter ? ` WHERE ${filter}` : "";
5943
- const affected2 = await this.executeReturningCount(
5944
- `UPDATE ${fbQuote(table2)} SET ${setClauses}${where}`,
5945
- [...Object.values(data), ...params ?? []]
5946
- );
5947
- return { success: true, affectedRows: affected2 };
5948
- }
5949
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
5950
- const sql = `UPDATE ${FB_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereClauses}`;
5951
- const values = [...Object.values(data), ...Object.values(filter)];
5916
+ const { sql, values } = buildUpdate(FB_DIALECT, table2, data, filter, params);
5952
5917
  const affected = await this.executeReturningCount(sql, values);
5953
5918
  return { success: true, affectedRows: affected };
5954
5919
  }
@@ -5957,14 +5922,7 @@ var init_firebird = __esm({
5957
5922
  }
5958
5923
  async deleteAsync(table2, filter, params) {
5959
5924
  this.ensureConnected();
5960
- if (typeof filter === "string") {
5961
- const where = filter ? ` WHERE ${filter}` : "";
5962
- const affected2 = await this.executeReturningCount(`DELETE FROM ${fbQuote(table2)}${where}`, params ?? []);
5963
- return { success: true, affectedRows: affected2 };
5964
- }
5965
- const whereClauses = buildWhereClause(FB_DIALECT, Object.keys(filter));
5966
- const sql = `DELETE FROM ${FB_DIALECT.quote(table2)} WHERE ${whereClauses}`;
5967
- const values = Object.values(filter);
5925
+ const { sql, values } = buildDelete(FB_DIALECT, table2, filter, params);
5968
5926
  const affected = await this.executeReturningCount(sql, values);
5969
5927
  return { success: true, affectedRows: affected };
5970
5928
  }
@@ -6796,6 +6754,7 @@ var init_odbc = __esm({
6796
6754
  "../orm/src/adapters/odbc.ts"() {
6797
6755
  "use strict";
6798
6756
  init_sqlDialect();
6757
+ init_sqlCrud();
6799
6758
  init_sqlTranslator();
6800
6759
  init_connectTimeout();
6801
6760
  odbcModule = null;
@@ -6995,16 +6954,12 @@ var init_odbc = __esm({
6995
6954
  async insertAsync(table2, data) {
6996
6955
  this.ensureConnected();
6997
6956
  if (Array.isArray(data)) {
6998
- if (data.length === 0) return { success: true, affectedRows: 0 };
6999
- const keys2 = Object.keys(data[0]);
7000
- const sql2 = buildInsert(ANSI_DIALECT, table2, keys2);
7001
- const paramsList = data.map((row) => keys2.map((k) => row[k]));
7002
- const { totalAffected } = await this.executeManyAsync(sql2, paramsList);
6957
+ const batch = buildInsertRows(ANSI_DIALECT, table2, data);
6958
+ if (!batch) return { success: true, affectedRows: 0 };
6959
+ const { totalAffected } = await this.executeManyAsync(batch.sql, batch.paramsList);
7003
6960
  return { success: true, affectedRows: totalAffected };
7004
6961
  }
7005
- const keys = Object.keys(data);
7006
- const sql = buildInsert(ANSI_DIALECT, table2, keys);
7007
- const values = Object.values(data);
6962
+ const { sql, values } = buildInsertRow(ANSI_DIALECT, table2, data);
7008
6963
  try {
7009
6964
  const result = await this.connection.query(sql, values);
7010
6965
  return { success: true, affectedRows: this.affectedCount(result), lastId: this._lastInsertId ?? void 0 };
@@ -7015,17 +6970,7 @@ var init_odbc = __esm({
7015
6970
  /** Update rows in a table matching filter. */
7016
6971
  async updateAsync(table2, data, filter, params) {
7017
6972
  this.ensureConnected();
7018
- const setClauses = buildSetClause(ANSI_DIALECT, Object.keys(data));
7019
- let whereSql;
7020
- let values;
7021
- if (typeof filter === "string") {
7022
- whereSql = filter;
7023
- values = [...Object.values(data), ...params ?? []];
7024
- } else {
7025
- whereSql = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
7026
- values = [...Object.values(data), ...Object.values(filter)];
7027
- }
7028
- const sql = `UPDATE ${ANSI_DIALECT.quote(table2)} SET ${setClauses} WHERE ${whereSql}`;
6973
+ const { sql, values } = buildUpdate(ANSI_DIALECT, table2, data, filter, params);
7029
6974
  try {
7030
6975
  const result = await this.connection.query(sql, values);
7031
6976
  return { success: true, affectedRows: this.affectedCount(result) };
@@ -7044,18 +6989,7 @@ var init_odbc = __esm({
7044
6989
  }
7045
6990
  return { success: true, affectedRows: totalAffected };
7046
6991
  }
7047
- if (typeof filter === "string") {
7048
- const sql2 = filter ? `DELETE FROM "${table2}" WHERE ${filter}` : `DELETE FROM "${table2}"`;
7049
- try {
7050
- const result = await this.connection.query(sql2, params ?? []);
7051
- return { success: true, affectedRows: this.affectedCount(result) };
7052
- } catch (e) {
7053
- return { success: false, affectedRows: 0, error: e.message };
7054
- }
7055
- }
7056
- const whereClauses = buildWhereClause(ANSI_DIALECT, Object.keys(filter));
7057
- const sql = `DELETE FROM ${ANSI_DIALECT.quote(table2)} WHERE ${whereClauses}`;
7058
- const values = Object.values(filter);
6992
+ const { sql, values } = buildDelete(ANSI_DIALECT, table2, filter, params);
7059
6993
  try {
7060
6994
  const result = await this.connection.query(sql, values);
7061
6995
  return { success: true, affectedRows: this.affectedCount(result) };
@@ -10775,11 +10709,11 @@ async function eagerLoadBelongsToRelation(instances, model, relatedClass, relati
10775
10709
  const relatedPkColumn = relatedClass.getDbColumn(relatedPk);
10776
10710
  const related = await eagerQueryRelated(relatedClass, relatedPkColumn, values);
10777
10711
  if (nested.length > 0 && related.length > 0) await relatedClass._eagerLoad(related, nested);
10778
- const lookup = {};
10779
- for (const record of related) lookup[_joinKey(record[relatedPk])] = record;
10712
+ const lookup2 = {};
10713
+ for (const record of related) lookup2[_joinKey(record[relatedPk])] = record;
10780
10714
  for (const instance of instances) {
10781
10715
  const value = instance[fkProp];
10782
- relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup[_joinKey(value)] ?? null : null;
10716
+ relationshipCache(instance)[relationName] = value !== void 0 && value !== null ? lookup2[_joinKey(value)] ?? null : null;
10783
10717
  }
10784
10718
  }
10785
10719
  async function eagerLoadRelation(instances, model, relationName, nested) {
@@ -14466,7 +14400,7 @@ var init_attachment = __esm({
14466
14400
  // ../orm/src/realtime/storage.ts
14467
14401
  import { randomBytes as randomBytes3 } from "node:crypto";
14468
14402
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, statSync as statSync4 } from "node:fs";
14469
- import { resolve as resolve5, sep } from "node:path";
14403
+ import { resolve as resolve5, sep as sep2 } from "node:path";
14470
14404
  import { createRequire as createRequire6 } from "node:module";
14471
14405
  function loadAwsSdk(packageName) {
14472
14406
  try {
@@ -14519,7 +14453,7 @@ var init_storage = __esm({
14519
14453
  // Resolve inside the root and reject any traversal attempt.
14520
14454
  pathFor(key) {
14521
14455
  const target = resolve5(this.directory, key);
14522
- if (target !== this.directory && !target.startsWith(this.directory + sep)) {
14456
+ if (target !== this.directory && !target.startsWith(this.directory + sep2)) {
14523
14457
  throw new Error(`unsafe storage key: ${JSON.stringify(key)}`);
14524
14458
  }
14525
14459
  return target;
@@ -15036,6 +14970,7 @@ __export(src_exports, {
15036
14970
  resolveDbPool: () => resolveDbPool,
15037
14971
  resolveFirebirdCharset: () => resolveFirebirdCharset,
15038
14972
  resolveGraphConnectTimeout: () => resolveGraphConnectTimeout,
14973
+ resolveSqlitePath: () => resolveSqlitePath,
15039
14974
  rollback: () => rollback,
15040
14975
  seedModels: () => seedModels,
15041
14976
  seedOrm: () => seedOrm,
@@ -15421,9 +15356,9 @@ var init_cache = __esm({
15421
15356
  return entry.value;
15422
15357
  }
15423
15358
  async set(key, value, ttl) {
15424
- const expiresAt = ttl > 0 ? Date.now() + ttl * 1e3 : 0;
15359
+ const expiresAt2 = ttl > 0 ? Date.now() + ttl * 1e3 : 0;
15425
15360
  this.store.delete(key);
15426
- this.store.set(key, { value, expiresAt });
15361
+ this.store.set(key, { value, expiresAt: expiresAt2 });
15427
15362
  while (this.store.size > this.maxEntries) {
15428
15363
  const firstKey = this.store.keys().next().value;
15429
15364
  if (firstKey !== void 0) this.store.delete(firstKey);
@@ -15856,8 +15791,8 @@ ${s}\r
15856
15791
  const oldest = files.shift();
15857
15792
  if (oldest) fs.unlinkSync(path.join(this.dir, oldest.name));
15858
15793
  }
15859
- const expiresAt = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
15860
- const entry = { key, value, expiresAt };
15794
+ const expiresAt2 = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
15795
+ const entry = { key, value, expiresAt: expiresAt2 };
15861
15796
  fs.writeFileSync(this.keyPath(key), JSON.stringify(entry));
15862
15797
  } catch {
15863
15798
  }
@@ -17510,7 +17445,7 @@ __export(engine_exports, {
17510
17445
  });
17511
17446
  import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
17512
17447
  import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync6, realpathSync } from "node:fs";
17513
- import { join as join10, resolve as resolve6, isAbsolute as isAbsolute4, sep as sep2 } from "node:path";
17448
+ import { join as join10, resolve as resolve6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17514
17449
  function jsonText(value) {
17515
17450
  try {
17516
17451
  const text = JSON.stringify(value);
@@ -17604,8 +17539,8 @@ function capCache(cache, maxEntries) {
17604
17539
  }
17605
17540
  function sweepExpiredCache(cache) {
17606
17541
  const now = Date.now();
17607
- for (const [key, [, expiresAt]] of cache) {
17608
- if (expiresAt <= now) cache.delete(key);
17542
+ for (const [key, [, expiresAt2]] of cache) {
17543
+ if (expiresAt2 <= now) cache.delete(key);
17609
17544
  }
17610
17545
  }
17611
17546
  function tokenize(source) {
@@ -17851,15 +17786,15 @@ function findOutsideQuotes(expr, needle) {
17851
17786
  }
17852
17787
  return -1;
17853
17788
  }
17854
- function splitOutsideQuotes(expr, sep8) {
17855
- if (!expr.includes(sep8)) return [expr];
17789
+ function splitOutsideQuotes(expr, sep9) {
17790
+ if (!expr.includes(sep9)) return [expr];
17856
17791
  const parts = [];
17857
17792
  let currentStart = 0;
17858
17793
  let inQuote = null;
17859
17794
  let depth = 0;
17860
17795
  let bracketDepth = 0;
17861
17796
  let i = 0;
17862
- const sepLen = sep8.length;
17797
+ const sepLen = sep9.length;
17863
17798
  const lastStart = expr.length - sepLen;
17864
17799
  while (i <= lastStart) {
17865
17800
  const ch = expr[i];
@@ -17880,7 +17815,7 @@ function splitOutsideQuotes(expr, sep8) {
17880
17815
  else if (ch === ")") depth--;
17881
17816
  else if (ch === "[") bracketDepth++;
17882
17817
  else if (ch === "]") bracketDepth--;
17883
- if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep8, i)) {
17818
+ if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep9, i)) {
17884
17819
  parts.push(expr.slice(currentStart, i));
17885
17820
  i += sepLen;
17886
17821
  currentStart = i;
@@ -18577,6 +18512,12 @@ function setFormTokenSessionId(sessionId) {
18577
18512
  }
18578
18513
  function _buildFormTokenJwt(descriptor = "") {
18579
18514
  const secret = process.env.TINA4_SECRET ?? "";
18515
+ const secretBytes = Buffer.byteLength(secret, "utf8");
18516
+ if (secretBytes < 32) {
18517
+ throw new Error(
18518
+ `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.`
18519
+ );
18520
+ }
18580
18521
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
18581
18522
  const header = { alg: "HS256", typ: "JWT" };
18582
18523
  const now = Math.floor(Date.now() / 1e3);
@@ -18827,8 +18768,8 @@ var init_engine = __esm({
18827
18768
  },
18828
18769
  first: (v) => Array.isArray(v) ? v[0] ?? null : null,
18829
18770
  last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
18830
- join: (v, sep8) => Array.isArray(v) ? v.map(String).join(sep8 !== void 0 ? String(sep8) : ", ") : String(v),
18831
- split: (v, sep8) => String(v).split(sep8 !== void 0 ? String(sep8) : " "),
18771
+ join: (v, sep9) => Array.isArray(v) ? v.map(String).join(sep9 !== void 0 ? String(sep9) : ", ") : String(v),
18772
+ split: (v, sep9) => String(v).split(sep9 !== void 0 ? String(sep9) : " "),
18832
18773
  replace: (v, from, to) => {
18833
18774
  const s = String(v);
18834
18775
  if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
@@ -19232,7 +19173,7 @@ var init_engine = __esm({
19232
19173
  }
19233
19174
  const root = realpathSync(this.templateDir);
19234
19175
  const real = realpathSync(filePath);
19235
- if (real !== root && !real.startsWith(root + sep2)) {
19176
+ if (real !== root && !real.startsWith(root + sep3)) {
19236
19177
  throw new Error(`Template path escapes the templates directory: ${name}`);
19237
19178
  }
19238
19179
  return readFileSync6(real, "utf-8");
@@ -20118,8 +20059,8 @@ var init_engine = __esm({
20118
20059
  sweepExpiredCache(this.fragmentCache);
20119
20060
  const cached = this.fragmentCache.get(cacheKey);
20120
20061
  if (cached) {
20121
- const [htmlContent, expiresAt] = cached;
20122
- if (Date.now() < expiresAt) {
20062
+ const [htmlContent, expiresAt2] = cached;
20063
+ if (Date.now() < expiresAt2) {
20123
20064
  let i2 = start2 + 1;
20124
20065
  let depth = 0;
20125
20066
  while (i2 < tokens.length) {
@@ -20912,7 +20853,7 @@ function csrfBearerIsValid(req2, secret) {
20912
20853
  const authHeader = req2.headers.authorization ?? "";
20913
20854
  if (!authHeader.startsWith("Bearer ")) return false;
20914
20855
  const bearerToken = authHeader.slice(7).trim();
20915
- return Boolean(bearerToken && validToken(bearerToken, secret));
20856
+ return Boolean(bearerToken && isIdentityPayload(validToken(bearerToken, secret)));
20916
20857
  }
20917
20858
  function csrfRequestToken(req2) {
20918
20859
  const query = req2.query ?? {};
@@ -22049,6 +21990,22 @@ var init_transport = __esm({
22049
21990
  }
22050
21991
  });
22051
21992
 
21993
+ // src/ssoIdentity.ts
21994
+ function liveSessionIdentity(stored) {
21995
+ if (!stored || typeof stored !== "object") return null;
21996
+ const identity = stored.identity;
21997
+ if (!identity || typeof identity !== "object" || !identity.issuer || !identity.subject) return null;
21998
+ const expiresAt2 = stored.expires_at ?? 0;
21999
+ if (typeof expiresAt2 !== "number") return null;
22000
+ if (expiresAt2 > 0 && Math.floor(Date.now() / 1e3) >= expiresAt2) return null;
22001
+ return identity;
22002
+ }
22003
+ var init_ssoIdentity = __esm({
22004
+ "src/ssoIdentity.ts"() {
22005
+ "use strict";
22006
+ }
22007
+ });
22008
+
22052
22009
  // src/authGate.ts
22053
22010
  function enforceRouteAuth(req2, res, match, isDevAdmin) {
22054
22011
  if (!(match.secure === true && match.noAuth !== true && !isDevAdmin)) {
@@ -22056,38 +22013,50 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
22056
22013
  }
22057
22014
  const authHeader = req2.headers.authorization ?? "";
22058
22015
  const headerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
22059
- let resolvedToken = "";
22016
+ let payload = null;
22060
22017
  let tokenSource = "";
22061
- if (headerToken && validToken(headerToken)) {
22062
- resolvedToken = headerToken;
22063
- tokenSource = "header";
22018
+ let resolvedToken = "";
22019
+ if (headerToken) {
22020
+ const candidate = validToken(headerToken);
22021
+ if (isIdentityPayload(candidate)) {
22022
+ payload = candidate;
22023
+ tokenSource = "header";
22024
+ resolvedToken = headerToken;
22025
+ }
22064
22026
  }
22065
- if (!resolvedToken) {
22027
+ if (!payload) {
22066
22028
  const bodyToken = req2.body?.formToken;
22067
- if (bodyToken && validToken(bodyToken)) {
22068
- resolvedToken = bodyToken;
22069
- tokenSource = "body";
22029
+ if (bodyToken) {
22030
+ const candidate = validToken(bodyToken);
22031
+ if (isIdentityPayload(candidate)) {
22032
+ payload = candidate;
22033
+ tokenSource = "body";
22034
+ resolvedToken = bodyToken;
22035
+ }
22070
22036
  }
22071
22037
  }
22072
- if (!resolvedToken) {
22073
- const sso = req2.session?.get?.("_tina4_sso");
22074
- const identity = sso?.identity;
22075
- if (identity?.issuer && identity?.subject) {
22038
+ if (!payload) {
22039
+ const identity = liveSessionIdentity(req2.session?.get?.("_tina4_sso"));
22040
+ if (identity) {
22076
22041
  req2.user = identity;
22077
22042
  return rbacForbidden(match, identity, res);
22078
22043
  }
22079
22044
  const sessionToken = req2.session?.get?.("token");
22080
- if (sessionToken && validToken(sessionToken)) {
22081
- resolvedToken = sessionToken;
22082
- tokenSource = "session";
22045
+ if (sessionToken) {
22046
+ const candidate = validToken(sessionToken);
22047
+ if (isIdentityPayload(candidate)) {
22048
+ payload = candidate;
22049
+ tokenSource = "session";
22050
+ resolvedToken = sessionToken;
22051
+ }
22083
22052
  }
22084
22053
  }
22085
- if (!resolvedToken) {
22054
+ if (!payload) {
22086
22055
  res.raw.writeHead(401, { "Content-Type": "application/json" });
22087
22056
  res.raw.end(JSON.stringify({ error: "Unauthorized" }));
22088
22057
  return true;
22089
22058
  }
22090
- req2.user = getPayload(resolvedToken) ?? {};
22059
+ req2.user = payload;
22091
22060
  if (tokenSource === "body") {
22092
22061
  const fresh = refreshToken(resolvedToken);
22093
22062
  if (fresh) {
@@ -22137,6 +22106,7 @@ var init_authGate = __esm({
22137
22106
  "src/authGate.ts"() {
22138
22107
  "use strict";
22139
22108
  init_auth();
22109
+ init_ssoIdentity();
22140
22110
  }
22141
22111
  });
22142
22112
 
@@ -23210,8 +23180,8 @@ var init_mongoHandler = __esm({
23210
23180
  if (!result || result === "__EMPTY__") return null;
23211
23181
  try {
23212
23182
  const doc = JSON.parse(result);
23213
- const expiresAt = Number(doc?.expires_at ?? 0);
23214
- if (expiresAt > 0 && Date.now() / 1e3 > expiresAt) {
23183
+ const expiresAt2 = Number(doc?.expires_at ?? 0);
23184
+ if (expiresAt2 > 0 && Date.now() / 1e3 > expiresAt2) {
23215
23185
  this.destroy(sessionId);
23216
23186
  return null;
23217
23187
  }
@@ -23437,6 +23407,9 @@ parentPort.on("message", (message) => {
23437
23407
 
23438
23408
  // src/sessionHandlers/databaseHandler.ts
23439
23409
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
23410
+ function sleepSync2(ms) {
23411
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
23412
+ }
23440
23413
  function column(row, name) {
23441
23414
  if (name in row) return row[name];
23442
23415
  return row[name.toUpperCase()];
@@ -23445,7 +23418,7 @@ function schemeOf2(url) {
23445
23418
  const match = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
23446
23419
  return match ? match[1].toLowerCase() : "unknown";
23447
23420
  }
23448
- var CREATE_TABLE, DatabaseSessionHandler;
23421
+ var CREATE_TABLE, CREATE_ATTEMPTS, CREATE_RETRY_DELAY_MS, DatabaseSessionHandler;
23449
23422
  var init_databaseHandler = __esm({
23450
23423
  "src/sessionHandlers/databaseHandler.ts"() {
23451
23424
  "use strict";
@@ -23525,6 +23498,8 @@ var init_databaseHandler = __esm({
23525
23498
  END
23526
23499
  `
23527
23500
  };
23501
+ CREATE_ATTEMPTS = 5;
23502
+ CREATE_RETRY_DELAY_MS = 50;
23528
23503
  DatabaseSessionHandler = class {
23529
23504
  sqliteHandle = null;
23530
23505
  /** Set for SQLite. Null when this handler talks to a networked engine. */
@@ -23565,6 +23540,7 @@ var init_databaseHandler = __esm({
23565
23540
  get sqlite() {
23566
23541
  if (this.sqliteHandle === null) {
23567
23542
  this.sqliteHandle = new DatabaseSync3(this.dbPath);
23543
+ this.sqliteHandle.exec("PRAGMA busy_timeout = 5000");
23568
23544
  this.sqliteHandle.exec("PRAGMA journal_mode = WAL");
23569
23545
  }
23570
23546
  return this.sqliteHandle;
@@ -23652,15 +23628,55 @@ var init_databaseHandler = __esm({
23652
23628
  sqlCommandSync(this.target, sql, params);
23653
23629
  }
23654
23630
  /**
23655
- * Ensure the session table exists (called once on first use).
23631
+ * Ensure the session table exists (called once on first use) - safely when
23632
+ * several processes race to create it.
23633
+ *
23634
+ * Every per-engine statement above is check-then-act, so two processes can
23635
+ * both find the table absent and both CREATE; the loser errors, and every
23636
+ * engine spells that differently (a pg_type unique violation on PostgreSQL,
23637
+ * Msg 2714 on SQL Server, an RDB$RELATIONS unique violation on Firebird). So
23638
+ * a failure is judged by RE-CHECKING whether the table now exists, never by
23639
+ * parsing the message - the rescue the Firebird note above has always
23640
+ * assumed, and which was not here. MEASURED on the lab before it: six real
23641
+ * processes racing lost one to five of them on PostgreSQL, SQLite and
23642
+ * Firebird.
23643
+ *
23644
+ * The re-check is RETRIED because the loser can fail BEFORE the winner has
23645
+ * committed. On MySQL, CREATE TABLE IF NOT EXISTS takes a shared metadata
23646
+ * lock, checks, then upgrades it to exclusive; two upgrades deadlock, and when
23647
+ * MySQL cannot back the victim off silently it answers 1213 "Deadlock found"
23648
+ * while the winner's table is still invisible (tina4-php CI run 35972320442).
23649
+ * Bounded, so a genuine failure (no CREATE grant) still throws. Same attempts
23650
+ * and delays in all four frameworks.
23656
23651
  */
23657
23652
  ensureTable() {
23658
23653
  if (this.initialized) return;
23659
23654
  const ddl = CREATE_TABLE[this.engine];
23660
- if (this.target === null) this.sqlite.exec(ddl);
23661
- else sqlCommandSync(this.target, ddl, []);
23655
+ for (let attempt = 1; ; attempt++) {
23656
+ try {
23657
+ if (this.target === null) this.sqlite.exec(ddl);
23658
+ else sqlCommandSync(this.target, ddl, []);
23659
+ break;
23660
+ } catch (error2) {
23661
+ if (this.tableExists()) break;
23662
+ if (attempt >= CREATE_ATTEMPTS) throw error2;
23663
+ sleepSync2(CREATE_RETRY_DELAY_MS * attempt);
23664
+ }
23665
+ }
23662
23666
  this.initialized = true;
23663
23667
  }
23668
+ /**
23669
+ * Whether tina4_session exists, asked the one way every engine answers alike:
23670
+ * a query against it that returns no rows succeeds only if it is there.
23671
+ */
23672
+ tableExists() {
23673
+ try {
23674
+ this.query("SELECT 1 FROM tina4_session WHERE 1 = 0", []);
23675
+ return true;
23676
+ } catch {
23677
+ return false;
23678
+ }
23679
+ }
23664
23680
  read(sessionId) {
23665
23681
  this.ensureTable();
23666
23682
  const rows = this.query(
@@ -23670,8 +23686,8 @@ var init_databaseHandler = __esm({
23670
23686
  const row = rows[0];
23671
23687
  if (!row) return null;
23672
23688
  const now = Date.now() / 1e3;
23673
- const expiresAt = Number(column(row, "expires_at") ?? 0);
23674
- if (expiresAt > 0 && expiresAt < now) {
23689
+ const expiresAt2 = Number(column(row, "expires_at") ?? 0);
23690
+ if (expiresAt2 > 0 && expiresAt2 < now) {
23675
23691
  this.destroy(sessionId);
23676
23692
  return null;
23677
23693
  }
@@ -23684,7 +23700,7 @@ var init_databaseHandler = __esm({
23684
23700
  write(sessionId, data, ttl) {
23685
23701
  this.ensureTable();
23686
23702
  const json = JSON.stringify(data);
23687
- const expiresAt = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
23703
+ const expiresAt2 = ttl > 0 ? Date.now() / 1e3 + ttl : 0;
23688
23704
  const existing = this.query(
23689
23705
  "SELECT session_id FROM tina4_session WHERE session_id = ?",
23690
23706
  [sessionId]
@@ -23692,12 +23708,12 @@ var init_databaseHandler = __esm({
23692
23708
  if (existing.length > 0) {
23693
23709
  this.exec(
23694
23710
  "UPDATE tina4_session SET data = ?, expires_at = ? WHERE session_id = ?",
23695
- [json, expiresAt, sessionId]
23711
+ [json, expiresAt2, sessionId]
23696
23712
  );
23697
23713
  } else {
23698
23714
  this.exec(
23699
23715
  "INSERT INTO tina4_session (session_id, data, expires_at) VALUES (?, ?, ?)",
23700
- [sessionId, json, expiresAt]
23716
+ [sessionId, json, expiresAt2]
23701
23717
  );
23702
23718
  }
23703
23719
  }
@@ -24527,7 +24543,7 @@ var init_dispatchPipeline = __esm({
24527
24543
 
24528
24544
  // src/static.ts
24529
24545
  import { readFileSync as readFileSync9, realpathSync as realpathSync3, statSync as statSync8 } from "node:fs";
24530
- import { join as join15, extname as extname3, sep as sep3 } from "node:path";
24546
+ import { join as join15, extname as extname3, sep as sep4 } from "node:path";
24531
24547
  import { gzipSync as gzipSync2 } from "node:zlib";
24532
24548
  function isCompressible(contentType) {
24533
24549
  return COMPRESSIBLE_PREFIXES2.some((prefix) => contentType.includes(prefix));
@@ -24558,8 +24574,8 @@ function tryServeStatic(staticDir, req2, res) {
24558
24574
  }
24559
24575
  const stat = statSync8(realPath);
24560
24576
  if (!stat.isFile()) continue;
24561
- if (realPath !== realDir && !realPath.startsWith(realDir + sep3)) continue;
24562
- if (hasHiddenSegment(realPath.slice(realDir.length + 1), sep3)) continue;
24577
+ if (realPath !== realDir && !realPath.startsWith(realDir + sep4)) continue;
24578
+ if (hasHiddenSegment(realPath.slice(realDir.length + 1), sep4)) continue;
24563
24579
  const ext = extname3(realPath);
24564
24580
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
24565
24581
  const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs / 1e3)}"`;
@@ -25542,7 +25558,7 @@ __export(generate_exports, {
25542
25558
  toTableName: () => toTableName
25543
25559
  });
25544
25560
  import { existsSync as existsSync15, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "node:fs";
25545
- import { join as join20, relative as relative2, resolve as resolve10, sep as sep4 } from "node:path";
25561
+ import { join as join20, relative as relative2, resolve as resolve10, sep as sep5 } from "node:path";
25546
25562
  function ensureDir(dir) {
25547
25563
  if (__resolution.dryRun) return;
25548
25564
  if (!existsSync15(dir)) {
@@ -25668,7 +25684,7 @@ function toRelPath(absPath) {
25668
25684
  const cwd = process.cwd();
25669
25685
  const rel = relative2(cwd, absPath);
25670
25686
  if (!rel) return absPath;
25671
- return sep4 === "/" ? rel : rel.split(sep4).join("/");
25687
+ return sep5 === "/" ? rel : rel.split(sep5).join("/");
25672
25688
  }
25673
25689
  function captureEditHints(absPath, content) {
25674
25690
  if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
@@ -25740,12 +25756,6 @@ function printResolution() {
25740
25756
  lines.push("");
25741
25757
  process.stderr.write(lines.join("\n"));
25742
25758
  }
25743
- function toPlural(name) {
25744
- const lower = name.toLowerCase();
25745
- if (lower.endsWith("s")) return lower;
25746
- if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return lower.slice(0, -1) + "ies";
25747
- return lower + "s";
25748
- }
25749
25759
  function toCamel(name) {
25750
25760
  return name.charAt(0).toLowerCase() + name.slice(1);
25751
25761
  }
@@ -26178,7 +26188,7 @@ ${aiFill(`delete_${singular}`, {
26178
26188
  }
26179
26189
  function generateCrud(name, flags) {
26180
26190
  const table2 = resolveTable(name, flags, { announce: false });
26181
- const routeName = toPlural(table2);
26191
+ const routeName = pluralizeReserved(toSnake(name));
26182
26192
  const isPublic = Boolean(flags.public);
26183
26193
  if (!__resolution.jsonMode) console.log(`
26184
26194
  Generating CRUD for ${name}...
@@ -26432,7 +26442,7 @@ void test${titleName};
26432
26442
  function generateForm(name, flags) {
26433
26443
  const fields = fieldsOrDefault(flags.fields || "");
26434
26444
  const table2 = resolveTable(name, flags, { announce: false });
26435
- const routeName = toPlural(table2);
26445
+ const routeName = pluralizeReserved(toSnake(name));
26436
26446
  const inputTypes = {
26437
26447
  string: "text",
26438
26448
  str: "text",
@@ -26498,7 +26508,7 @@ function generateForm(name, flags) {
26498
26508
  function generateView(name, flags) {
26499
26509
  const fields = fieldsOrDefault(flags.fields || "");
26500
26510
  const table2 = resolveTable(name, flags, { announce: false });
26501
- const routeName = toPlural(table2);
26511
+ const routeName = pluralizeReserved(toSnake(name));
26502
26512
  const cols = fields.map(([f]) => f);
26503
26513
  const dir = resolve10("src/templates/pages");
26504
26514
  ensureDir(dir);
@@ -27510,7 +27520,7 @@ var init_generate = __esm({
27510
27520
  crud: ({ name, table: table2 }) => [
27511
27521
  `Apply the migration: npx tina4nodejs migrate`,
27512
27522
  `Serve and try: npx tina4nodejs serve -> visit /swagger`,
27513
- `Run the gate test: npx tsx tests/${toPlural(table2)}.test.ts`,
27523
+ `Run the gate test: npx tsx tests/${pluralizeReserved(toSnake(name))}.test.ts`,
27514
27524
  `Change fields: edit src/models/${name}.ts then re-run generate crud`
27515
27525
  ],
27516
27526
  migration: () => [
@@ -27528,10 +27538,10 @@ var init_generate = __esm({
27528
27538
  ],
27529
27539
  form: ({ name, table: table2 }) => [
27530
27540
  `Render from a route: res.render("forms/${table2}.twig", { item })`,
27531
- `Add the POST route: npx tina4nodejs generate route ${toPlural(table2)} --model ${name}`
27541
+ `Add the POST route: npx tina4nodejs generate route ${pluralizeReserved(toSnake(name))} --model ${name}`
27532
27542
  ],
27533
- view: ({ table: table2 }) => [
27534
- `Wire routes to render list -> ${toPlural(table2)}.twig, detail -> ${table2}.twig`,
27543
+ view: ({ name, table: table2 }) => [
27544
+ `Wire routes to render list -> ${pluralizeReserved(toSnake(name))}.twig, detail -> ${table2}.twig`,
27535
27545
  `Customize the templates in src/templates/pages/`
27536
27546
  ],
27537
27547
  auth: () => [
@@ -27692,7 +27702,7 @@ function envTruthy(val) {
27692
27702
  return ["true", "1", "yes", "on"].includes(val.trim().toLowerCase());
27693
27703
  }
27694
27704
  function isLoopback(ip) {
27695
- if (ip == null || ip === "") return true;
27705
+ if (ip == null || ip === "") return false;
27696
27706
  let addr = ip.trim().toLowerCase();
27697
27707
  if (addr.startsWith("::ffff:")) addr = addr.slice(7);
27698
27708
  return addr === "::1" || addr === "localhost" || addr.startsWith("127.");
@@ -29283,6 +29293,7 @@ __export(graphql_exports, {
29283
29293
  graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
29284
29294
  graphqlEndpoint: () => graphqlEndpoint,
29285
29295
  graphqlMaxDepth: () => graphqlMaxDepth,
29296
+ graphqlMaxNodes: () => graphqlMaxNodes,
29286
29297
  tokenize: () => tokenize2
29287
29298
  });
29288
29299
  function tokenize2(source) {
@@ -29322,7 +29333,12 @@ function graphqlMaxDepth() {
29322
29333
  const n = parseInt(raw, 10);
29323
29334
  return Number.isNaN(n) ? 50 : n;
29324
29335
  }
29325
- var TOKEN_PATTERNS, ParseError, Parser, GraphQL, GraphQLType;
29336
+ function graphqlMaxNodes() {
29337
+ const raw = (process.env.TINA4_GRAPHQL_MAX_NODES ?? "1000").trim();
29338
+ const n = parseInt(raw, 10);
29339
+ return Number.isNaN(n) ? 1e3 : n;
29340
+ }
29341
+ var TOKEN_PATTERNS, ParseError, Parser, ComplexityError, GraphQL, GraphQLType;
29326
29342
  var init_graphql = __esm({
29327
29343
  "src/graphql.ts"() {
29328
29344
  "use strict";
@@ -29361,9 +29377,16 @@ var init_graphql = __esm({
29361
29377
  Parser = class {
29362
29378
  tokens;
29363
29379
  pos;
29380
+ // F2: bound the parser's own recursion. A deeply nested query
29381
+ // ("{a{a{a...}}}") otherwise recurses in parseSelectionSet until the call
29382
+ // stack overflows — BEFORE the execution-time depth guard can run. Reusing
29383
+ // TINA4_GRAPHQL_MAX_DEPTH keeps parse and execution on one bound.
29384
+ depth = 0;
29385
+ maxDepth;
29364
29386
  constructor(tokens) {
29365
29387
  this.tokens = tokens;
29366
29388
  this.pos = 0;
29389
+ this.maxDepth = graphqlMaxDepth();
29367
29390
  }
29368
29391
  peek() {
29369
29392
  return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
@@ -29445,6 +29468,17 @@ var init_graphql = __esm({
29445
29468
  };
29446
29469
  }
29447
29470
  parseSelectionSet() {
29471
+ this.depth++;
29472
+ if (this.maxDepth > 0 && this.depth > this.maxDepth) {
29473
+ throw new Error(`Query exceeds maximum depth of ${this.maxDepth}`);
29474
+ }
29475
+ try {
29476
+ return this.parseSelectionSetInner();
29477
+ } finally {
29478
+ this.depth--;
29479
+ }
29480
+ }
29481
+ parseSelectionSetInner() {
29448
29482
  this.expect("LBRACE");
29449
29483
  const selections = [];
29450
29484
  while (!this.match("RBRACE")) {
@@ -29599,6 +29633,8 @@ var init_graphql = __esm({
29599
29633
  return t;
29600
29634
  }
29601
29635
  };
29636
+ ComplexityError = class extends Error {
29637
+ };
29602
29638
  GraphQL = class _GraphQL {
29603
29639
  types = /* @__PURE__ */ new Map();
29604
29640
  queries = /* @__PURE__ */ new Map();
@@ -29621,6 +29657,12 @@ var init_graphql = __esm({
29621
29657
  * spread / inline fragment, so circular fragments are caught too.
29622
29658
  */
29623
29659
  maxDepth = graphqlMaxDepth();
29660
+ /**
29661
+ * Maximum expanded selection nodes per query (from `TINA4_GRAPHQL_MAX_NODES`,
29662
+ * default 1000; `<= 0` disables). Bounds query WIDTH — fragment bombs and
29663
+ * alias explosions — which the depth guard does not. See {@link graphqlMaxNodes}.
29664
+ */
29665
+ maxNodes = graphqlMaxNodes();
29624
29666
  /**
29625
29667
  * Decorator-style resolver registration.
29626
29668
  *
@@ -29768,6 +29810,16 @@ var init_graphql = __esm({
29768
29810
  return { data: null, errors: [{ message: "No operation found" }] };
29769
29811
  }
29770
29812
  const op = operations[0];
29813
+ if (this.maxNodes > 0) {
29814
+ try {
29815
+ this.assertComplexity(op.selections, fragments);
29816
+ } catch (e) {
29817
+ if (e instanceof ComplexityError) {
29818
+ return { data: null, errors: [{ message: e.message }] };
29819
+ }
29820
+ throw e;
29821
+ }
29822
+ }
29771
29823
  const resolvers = op.operation === "query" ? this.queries : this.mutations;
29772
29824
  for (const vdef of op.variables) {
29773
29825
  if (!(vdef.name in vars) && vdef.default !== void 0) {
@@ -29793,6 +29845,36 @@ var init_graphql = __esm({
29793
29845
  * instead of recursing until the interpreter stack overflows. Top-level
29794
29846
  * starts at depth 1; `maxDepth <= 0` disables the guard.
29795
29847
  */
29848
+ /**
29849
+ * Count the expanded selection nodes, throwing {@link ComplexityError} past
29850
+ * `maxNodes` (F2). Fragment spreads are expanded (so a fragment bomb is
29851
+ * counted, not the small unexpanded document) and every field — including
29852
+ * each alias — counts once. The count is capped by `maxNodes` and the walk by
29853
+ * `maxDepth`, so a circular fragment trips the budget instead of looping and
29854
+ * the check itself can never cost more than the budget.
29855
+ */
29856
+ assertComplexity(selections, fragments) {
29857
+ const depthCap = this.maxDepth > 0 ? this.maxDepth : 1e3;
29858
+ let count = 0;
29859
+ const walk2 = (sels, depth) => {
29860
+ if (depth > depthCap) return;
29861
+ for (const sel of sels) {
29862
+ count++;
29863
+ if (count > this.maxNodes) {
29864
+ throw new ComplexityError(`Query exceeds maximum complexity of ${this.maxNodes} nodes`);
29865
+ }
29866
+ if (sel.kind === "fragment_spread") {
29867
+ const frag = fragments.get(sel.name);
29868
+ if (frag) walk2(frag.selections, depth + 1);
29869
+ } else if (sel.kind === "inline_fragment") {
29870
+ walk2(sel.selections, depth + 1);
29871
+ } else if (sel.kind === "field" && sel.selections) {
29872
+ walk2(sel.selections, depth + 1);
29873
+ }
29874
+ }
29875
+ };
29876
+ walk2(selections, 1);
29877
+ }
29796
29878
  async resolveSelectionsInto(selections, resolvers, parent, variables, context, fragments, target, depth) {
29797
29879
  const errors = [];
29798
29880
  if (this.maxDepth > 0 && depth > this.maxDepth) {
@@ -30995,7 +31077,8 @@ function wsAuthorized(route, headers, queryString = "", subprotocol = "") {
30995
31077
  const token = wsToken(headers, queryString, subprotocol);
30996
31078
  if (!token) return [null, false];
30997
31079
  const payload = validToken(token);
30998
- return [payload, payload !== null];
31080
+ if (!isIdentityPayload(payload)) return [null, false];
31081
+ return [payload, true];
30999
31082
  }
31000
31083
  function offeredBearerSubprotocol(subprotocol) {
31001
31084
  return subprotocol.split(",").some((p) => p.trim().toLowerCase() === "bearer");
@@ -32190,12 +32273,36 @@ var init_mongoBackend = __esm({
32190
32273
  }
32191
32274
  }
32192
32275
  else if (operation === "size") {
32193
- const count = await col.countDocuments({
32194
- queue: queueName,
32195
- status: "pending",
32196
- });
32276
+ // ADR-0022 dec 7: size(status) must not answer a different question.
32277
+ // The dead aliases count the .dead_letter store (== deadLetters().length);
32278
+ // reserved/completed count by status; else pending. data = status.
32279
+ const status = data || "pending";
32280
+ let count: number;
32281
+ if (status === "dead" || status === "failed" || status === "dead_letter") {
32282
+ count = await col.countDocuments({ queue: queueName + ".dead_letter" });
32283
+ } else if (status === "reserved" || status === "completed") {
32284
+ count = await col.countDocuments({ queue: queueName, status });
32285
+ } else {
32286
+ count = await col.countDocuments({ queue: queueName, status: "pending" });
32287
+ }
32197
32288
  process.stdout.write(String(count));
32198
32289
  }
32290
+ else if (operation === "reject") {
32291
+ // Dead-letter NOW, no retry (ADR-0023). Floor attempts at maxRetries
32292
+ // so it reads as exhausted (parity with fail()'s dead-letter branch
32293
+ // and size("dead")). data = JSON { id, reason, maxRetries }.
32294
+ const info = JSON.parse(data);
32295
+ const doc = await col.findOne({ queue: queueName, id: info.id });
32296
+ if (doc) {
32297
+ const attempts = Math.max((doc.attempts || 0) + 1, info.maxRetries);
32298
+ await col.insertOne({
32299
+ ...doc, _id: undefined, attempts,
32300
+ status: "dead", queue: queueName + ".dead_letter", error: info.reason,
32301
+ });
32302
+ await col.deleteOne({ _id: doc._id, queue: queueName });
32303
+ }
32304
+ process.stdout.write("__OK__");
32305
+ }
32199
32306
  else if (operation === "clear") {
32200
32307
  await col.deleteMany({ queue: queueName });
32201
32308
  process.stdout.write("__CLEARED__");
@@ -32431,11 +32538,20 @@ var init_mongoBackend = __esm({
32431
32538
  return null;
32432
32539
  }
32433
32540
  }
32434
- size(queue) {
32435
- const result = this.execSync("size", queue);
32541
+ size(queue, status2 = "pending") {
32542
+ const result = this.execSync("size", queue, status2);
32436
32543
  const num = parseInt(result, 10);
32437
32544
  return isNaN(num) ? 0 : num;
32438
32545
  }
32546
+ /**
32547
+ * Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
32548
+ * fail(): fail() requeues until maxRetries is spent. attempts is floored at
32549
+ * maxRetries so deadLetters()/size("dead") (which filter/scope to the
32550
+ * dead-letter store) see it.
32551
+ */
32552
+ reject(queue, id, reason, maxRetries) {
32553
+ this.execSync("reject", queue, JSON.stringify({ id, reason, maxRetries }));
32554
+ }
32439
32555
  clear(queue) {
32440
32556
  this.execSync("clear", queue);
32441
32557
  }
@@ -32530,7 +32646,9 @@ function createJob(data, queue) {
32530
32646
  queue._failJob(job.topic, job, reason, queue.getMaxRetries());
32531
32647
  },
32532
32648
  reject(reason = "") {
32533
- job.fail(reason);
32649
+ job.status = "dead";
32650
+ job.error = reason;
32651
+ queue._rejectJob(job.topic, job, reason, queue.getMaxRetries());
32534
32652
  },
32535
32653
  retry(delaySeconds) {
32536
32654
  queue._retryJob(job.topic, job, delaySeconds);
@@ -33139,6 +33257,20 @@ var init_liteBackend = __esm({
33139
33257
  * dead-letter file -- two spellings of the same intent that previously
33140
33258
  * diverged.
33141
33259
  */
33260
+ /**
33261
+ * Reject a job permanently — dead-letter it NOW, no retry (ADR-0023).
33262
+ *
33263
+ * Unlike failJob(), which requeues until maxRetries is spent, rejectJob()
33264
+ * moves a known-poison job straight to the dead-letter store. attempts is
33265
+ * floored at maxRetries so deadLetters() (attempts >= maxRetries) returns it
33266
+ * and size("dead") agrees.
33267
+ */
33268
+ rejectJob(queue, job, reason, maxRetries) {
33269
+ this.clearReservation(queue, job.id);
33270
+ job.attempts = Math.max((job.attempts || 0) + 1, maxRetries);
33271
+ job.error = reason;
33272
+ this.deadLetter(queue, job, reason);
33273
+ }
33142
33274
  retryJob(queue, job, delaySeconds) {
33143
33275
  this.clearReservation(queue, job.id);
33144
33276
  try {
@@ -33360,7 +33492,7 @@ var init_queue = __esm({
33360
33492
  size(status2 = "pending") {
33361
33493
  const q = this.topic;
33362
33494
  if (this.externalBackend) {
33363
- return this.externalBackend.size(q);
33495
+ return this.externalBackend.size(q, status2);
33364
33496
  }
33365
33497
  return this.liteBackend.size(q, status2);
33366
33498
  }
@@ -33628,6 +33760,17 @@ var init_queue = __esm({
33628
33760
  }
33629
33761
  this.liteBackend.failJob(queue, job, error2, maxRetries, this._retryBackoff);
33630
33762
  }
33763
+ /**
33764
+ * Dead-letter a job immediately — no retry (ADR-0023 reject()). Distinct from
33765
+ * _failJob, which retries until maxRetries is spent.
33766
+ */
33767
+ _rejectJob(queue, job, reason, maxRetries) {
33768
+ if (this.externalBackend?.reject) {
33769
+ this.externalBackend.reject(queue, job.id, reason, maxRetries);
33770
+ return;
33771
+ }
33772
+ this.liteBackend.rejectJob(queue, job, reason, maxRetries);
33773
+ }
33631
33774
  /**
33632
33775
  * Re-queue a job back to the main queue directory with incremented attempts.
33633
33776
  */
@@ -35752,7 +35895,7 @@ ${end}
35752
35895
  // src/devAdmin.ts
35753
35896
  import { cpus as osCpus } from "node:os";
35754
35897
  import { readFileSync as readFileSync20, writeFileSync as writeFileSync15, existsSync as existsSync22, readdirSync as readdirSync15, mkdirSync as mkdirSync18, copyFileSync, 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";
35755
- import { join as join27, dirname as dirname12, resolve as resolve16, relative as relative9, sep as sep7, basename as basename5 } from "node:path";
35898
+ import { join as join27, dirname as dirname12, resolve as resolve16, relative as relative9, sep as sep8, basename as basename5 } from "node:path";
35756
35899
  import { fileURLToPath as fileURLToPath5 } from "node:url";
35757
35900
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
35758
35901
  function escapeHtml(value) {
@@ -36143,7 +36286,7 @@ function safeJoin(projectRoot3, rel, extraRoots = []) {
36143
36286
  const resolved = realPathOf(resolve16(root, rel));
36144
36287
  const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf(resolve16(r)))];
36145
36288
  for (const r of roots) {
36146
- if (resolved === r || resolved.startsWith(r.endsWith(sep7) ? r : r + sep7)) return resolved;
36289
+ if (resolved === r || resolved.startsWith(r.endsWith(sep8) ? r : r + sep8)) return resolved;
36147
36290
  }
36148
36291
  } catch {
36149
36292
  }
@@ -39339,13 +39482,20 @@ var sso_exports = {};
39339
39482
  __export(sso_exports, {
39340
39483
  SSO: () => Sso,
39341
39484
  Sso: () => Sso,
39342
- SsoError: () => SsoError
39485
+ SsoError: () => SsoError,
39486
+ liveSessionIdentity: () => liveSessionIdentity
39343
39487
  });
39344
39488
  import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
39489
+ function expiresAt(tokens) {
39490
+ const lifetime = Number(tokens.expires_in ?? 0);
39491
+ return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
39492
+ }
39345
39493
  var SsoError, Sso;
39346
39494
  var init_sso = __esm({
39347
39495
  "src/sso.ts"() {
39348
39496
  "use strict";
39497
+ init_ssoIdentity();
39498
+ init_ssoIdentity();
39349
39499
  SsoError = class extends Error {
39350
39500
  };
39351
39501
  Sso = class _Sso {
@@ -39551,13 +39701,12 @@ var init_sso = __esm({
39551
39701
  access_token: tokens.access_token,
39552
39702
  refresh_token: tokens.refresh_token,
39553
39703
  id_token: tokens.id_token,
39554
- expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
39704
+ expires_at: expiresAt(tokens)
39555
39705
  });
39556
39706
  return { identity, return_to: _Sso.safeReturn(pending.return_to) };
39557
39707
  }
39558
39708
  identity(requestOrSession) {
39559
- const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
39560
- const identity = stored?.identity ?? null;
39709
+ const identity = liveSessionIdentity(this.session(requestOrSession)?.get(_Sso.SESSION_KEY));
39561
39710
  if (identity && requestOrSession?.session) requestOrSession.user = identity;
39562
39711
  return identity;
39563
39712
  }
@@ -39584,7 +39733,7 @@ var init_sso = __esm({
39584
39733
  access_token: tokens.access_token,
39585
39734
  refresh_token: tokens.refresh_token ?? stored.refresh_token,
39586
39735
  id_token: tokens.id_token ?? stored.id_token,
39587
- expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
39736
+ expires_at: expiresAt(tokens)
39588
39737
  });
39589
39738
  return identity;
39590
39739
  } catch (error2) {
@@ -39676,8 +39825,8 @@ function writeMcpDiscovery(projectRoot3, port) {
39676
39825
  const lines = contents.split(/\r?\n/);
39677
39826
  const already = lines.some((l) => l.trim() === GITIGNORE_LINE || l.trim() === ".tina4");
39678
39827
  if (!already) {
39679
- const sep8 = contents.endsWith("\n") || contents === "" ? "" : "\n";
39680
- fs9.writeFileSync(gitignorePath, `${contents}${sep8}${GITIGNORE_LINE}
39828
+ const sep9 = contents.endsWith("\n") || contents === "" ? "" : "\n";
39829
+ fs9.writeFileSync(gitignorePath, `${contents}${sep9}${GITIGNORE_LINE}
39681
39830
  `, "utf-8");
39682
39831
  }
39683
39832
  }
@@ -39726,12 +39875,12 @@ function bannerSurfaceLines(port, opts) {
39726
39875
  ];
39727
39876
  }
39728
39877
  function swaggerAdvertised() {
39729
- const TRUTHY2 = ["true", "1", "yes", "on"];
39878
+ const TRUTHY3 = ["true", "1", "yes", "on"];
39730
39879
  const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
39731
39880
  if (raw === "") {
39732
- return TRUTHY2.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
39881
+ return TRUTHY3.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
39733
39882
  }
39734
- return TRUTHY2.includes(raw);
39883
+ return TRUTHY3.includes(raw);
39735
39884
  }
39736
39885
  async function autoMigrateOnStartup(migrationDir = "migrations", base = process.cwd()) {
39737
39886
  const dir = resolve19(base, migrationDir);
@@ -40650,8 +40799,9 @@ function wireLocaleGlobal(frondEngine, base) {
40650
40799
  async function startServer(config) {
40651
40800
  loadEnv(".env.local");
40652
40801
  loadEnv();
40653
- const { ensureDevSecret: ensureDevSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
40802
+ const { ensureDevSecret: ensureDevSecret2, requireBootSecret: requireBootSecret2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
40654
40803
  ensureDevSecret2();
40804
+ requireBootSecret2();
40655
40805
  _checkLegacyEnvVars();
40656
40806
  const resolved = resolvePortAndHost(config);
40657
40807
  const host = resolved.host;
@@ -42316,6 +42466,178 @@ var init_service = __esm({
42316
42466
  }
42317
42467
  });
42318
42468
 
42469
+ // src/ssrf.ts
42470
+ import { isIP } from "node:net";
42471
+ import { lookup } from "node:dns/promises";
42472
+ function v4ToBytes(ip) {
42473
+ const parts = ip.split(".");
42474
+ if (parts.length !== 4) return null;
42475
+ const bytes = new Uint8Array(4);
42476
+ for (let i = 0; i < 4; i++) {
42477
+ const n = Number(parts[i]);
42478
+ if (!Number.isInteger(n) || n < 0 || n > 255 || !/^\d+$/.test(parts[i])) return null;
42479
+ bytes[i] = n;
42480
+ }
42481
+ return bytes;
42482
+ }
42483
+ function v6ToBytes(input) {
42484
+ let ip = input.split("%")[0];
42485
+ const lastColon = ip.lastIndexOf(":");
42486
+ const tail = ip.slice(lastColon + 1);
42487
+ if (tail.includes(".")) {
42488
+ const v4 = v4ToBytes(tail);
42489
+ if (!v4) return null;
42490
+ const hextetA = (v4[0] << 8 | v4[1]).toString(16);
42491
+ const hextetB = (v4[2] << 8 | v4[3]).toString(16);
42492
+ ip = ip.slice(0, lastColon + 1) + hextetA + ":" + hextetB;
42493
+ }
42494
+ const halves = ip.split("::");
42495
+ if (halves.length > 2) return null;
42496
+ const head = halves[0] ? halves[0].split(":") : [];
42497
+ const back = halves.length === 2 ? halves[1] ? halves[1].split(":") : [] : [];
42498
+ let groups;
42499
+ if (halves.length === 1) {
42500
+ if (head.length !== 8) return null;
42501
+ groups = head;
42502
+ } else {
42503
+ const missing = 8 - (head.length + back.length);
42504
+ if (missing < 0) return null;
42505
+ groups = [...head, ...Array(missing).fill("0"), ...back];
42506
+ }
42507
+ if (groups.length !== 8) return null;
42508
+ const bytes = new Uint8Array(16);
42509
+ for (let i = 0; i < 8; i++) {
42510
+ if (!/^[0-9a-fA-F]{1,4}$/.test(groups[i])) return null;
42511
+ const g = parseInt(groups[i], 16);
42512
+ bytes[i * 2] = g >> 8 & 255;
42513
+ bytes[i * 2 + 1] = g & 255;
42514
+ }
42515
+ return bytes;
42516
+ }
42517
+ function ipToBytes(ip) {
42518
+ const cleaned = ip.split("%")[0];
42519
+ const family = isIP(cleaned);
42520
+ if (family === 4) return v4ToBytes(cleaned);
42521
+ if (family === 6) {
42522
+ const bytes = v6ToBytes(cleaned);
42523
+ if (!bytes) return null;
42524
+ const isMapped = bytes.subarray(0, 10).every((b) => b === 0) && bytes[10] === 255 && bytes[11] === 255;
42525
+ return isMapped ? bytes.subarray(12) : bytes;
42526
+ }
42527
+ return null;
42528
+ }
42529
+ function parseCidr(cidr) {
42530
+ const [netStr, bitsStr] = cidr.split("/");
42531
+ const net4 = ipToBytes(netStr);
42532
+ if (!net4) return null;
42533
+ return { net: net4, bits: Number(bitsStr) };
42534
+ }
42535
+ function inCidr(packed, cidr) {
42536
+ if (packed.length !== cidr.net.length) return false;
42537
+ const whole = Math.floor(cidr.bits / 8);
42538
+ const remainder = cidr.bits % 8;
42539
+ for (let i = 0; i < whole; i++) {
42540
+ if (packed[i] !== cidr.net[i]) return false;
42541
+ }
42542
+ if (remainder === 0) return true;
42543
+ const mask = 255 << 8 - remainder & 255;
42544
+ return (packed[whole] & mask) === (cidr.net[whole] & mask);
42545
+ }
42546
+ function allowPrivateRequests() {
42547
+ return TRUTHY2.has((process.env[ALLOW_PRIVATE_ENV] ?? "").trim().toLowerCase());
42548
+ }
42549
+ function isBlockedAddress(ip) {
42550
+ const packed = ipToBytes(ip);
42551
+ if (!packed) return true;
42552
+ return BLOCKED.some((cidr) => inCidr(packed, cidr));
42553
+ }
42554
+ function matchesAllowList(host, port, resolved, allowHosts) {
42555
+ const hostLower = host.toLowerCase();
42556
+ for (const raw of allowHosts ?? []) {
42557
+ const entry = String(raw).trim().toLowerCase();
42558
+ if (!entry) continue;
42559
+ if (entry === hostLower || entry === `${hostLower}:${port}`) return true;
42560
+ if (entry.includes("/")) {
42561
+ const cidr = parseCidr(entry);
42562
+ if (cidr && resolved.some((ip) => {
42563
+ const packed = ipToBytes(ip);
42564
+ return packed !== null && inCidr(packed, cidr);
42565
+ })) {
42566
+ return true;
42567
+ }
42568
+ }
42569
+ }
42570
+ return false;
42571
+ }
42572
+ function logBlock(host, ip) {
42573
+ void Promise.resolve().then(() => (init_logger(), logger_exports)).then((mod) => mod.Log?.warning?.(`SSRF guard blocked outbound request to ${host} (${ip})`)).catch(() => void 0);
42574
+ }
42575
+ async function guardUrl(url, allowHosts) {
42576
+ let parsed;
42577
+ try {
42578
+ parsed = new URL(url);
42579
+ } catch {
42580
+ throw new SsrfError(`Blocked request: '${url}' is not a valid URL`);
42581
+ }
42582
+ const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
42583
+ if (scheme !== "http" && scheme !== "https") {
42584
+ throw new SsrfError(`Blocked request: URL scheme '${scheme || "(none)"}' is not http or https`);
42585
+ }
42586
+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
42587
+ if (!host) {
42588
+ throw new SsrfError("Blocked request: URL has no host");
42589
+ }
42590
+ const port = parsed.port ? Number(parsed.port) : scheme === "https" ? 443 : 80;
42591
+ if (allowPrivateRequests()) return;
42592
+ let resolved;
42593
+ try {
42594
+ const results = await lookup(host, { all: true });
42595
+ resolved = results.map((r) => r.address);
42596
+ } catch {
42597
+ throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
42598
+ }
42599
+ if (resolved.length === 0) {
42600
+ throw new SsrfError(`Blocked request to ${host}: cannot resolve host`);
42601
+ }
42602
+ if (matchesAllowList(host, port, resolved, allowHosts)) return;
42603
+ for (const ip of resolved) {
42604
+ if (isBlockedAddress(ip)) {
42605
+ logBlock(host, ip);
42606
+ throw new SsrfError(
42607
+ `Blocked request to private/internal address ${ip} (host ${host}): set ${ALLOW_PRIVATE_ENV}=true to allow, or pass an allow-list.`
42608
+ );
42609
+ }
42610
+ }
42611
+ }
42612
+ var ALLOW_PRIVATE_ENV, TRUTHY2, SsrfError, BLOCKED_CIDRS, BLOCKED;
42613
+ var init_ssrf = __esm({
42614
+ "src/ssrf.ts"() {
42615
+ "use strict";
42616
+ ALLOW_PRIVATE_ENV = "TINA4_ALLOW_PRIVATE_REQUESTS";
42617
+ TRUTHY2 = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
42618
+ SsrfError = class extends Error {
42619
+ constructor(message) {
42620
+ super(message);
42621
+ this.name = "SsrfError";
42622
+ }
42623
+ };
42624
+ BLOCKED_CIDRS = [
42625
+ "0.0.0.0/8",
42626
+ "10.0.0.0/8",
42627
+ "127.0.0.0/8",
42628
+ "169.254.0.0/16",
42629
+ "172.16.0.0/12",
42630
+ "192.168.0.0/16",
42631
+ "100.64.0.0/10",
42632
+ "::1/128",
42633
+ "::/128",
42634
+ "fc00::/7",
42635
+ "fe80::/10"
42636
+ ];
42637
+ BLOCKED = BLOCKED_CIDRS.map(parseCidr).filter((c) => c !== null);
42638
+ }
42639
+ });
42640
+
42319
42641
  // src/api.ts
42320
42642
  import http from "node:http";
42321
42643
  import https from "node:https";
@@ -42472,6 +42794,7 @@ var init_api = __esm({
42472
42794
  "src/api.ts"() {
42473
42795
  "use strict";
42474
42796
  init_version();
42797
+ init_ssrf();
42475
42798
  ApiStreamError = class extends Error {
42476
42799
  constructor(message, status2 = null) {
42477
42800
  super(message);
@@ -42526,6 +42849,7 @@ var init_api = __esm({
42526
42849
  transportFn;
42527
42850
  cookiesEnabled;
42528
42851
  cookies;
42852
+ allowHosts;
42529
42853
  /**
42530
42854
  * Construct an Api client.
42531
42855
  *
@@ -42565,6 +42889,7 @@ var init_api = __esm({
42565
42889
  this.transportFn = void 0;
42566
42890
  this.cookiesEnabled = false;
42567
42891
  this.cookies = {};
42892
+ this.allowHosts = [];
42568
42893
  if (typeof authHeaderOrOptions === "object" && authHeaderOrOptions !== null) {
42569
42894
  const opts = authHeaderOrOptions;
42570
42895
  this.authHeader = opts.authHeader ?? "";
@@ -42574,6 +42899,7 @@ var init_api = __esm({
42574
42899
  this.retryBackoff = opts.retryBackoff ?? 0.5;
42575
42900
  this.transportFn = opts.transport;
42576
42901
  this.cookiesEnabled = opts.cookies ?? false;
42902
+ this.allowHosts = opts.allowHosts ?? [];
42577
42903
  if (opts.bearerToken != null) {
42578
42904
  this.setBearerToken(opts.bearerToken);
42579
42905
  } else if (opts.username != null && opts.password != null) {
@@ -42887,7 +43213,12 @@ var init_api = __esm({
42887
43213
  * body-phase timeout is applied by the caller (streamBytes) via
42888
43214
  * `res.destroy()`.
42889
43215
  */
42890
- openStreamRequest(method, url, headers, data, connectSec) {
43216
+ async openStreamRequest(method, url, headers, data, connectSec) {
43217
+ try {
43218
+ await guardUrl(url, this.allowHosts);
43219
+ } catch (err) {
43220
+ throw new ApiStreamError(err instanceof Error ? err.message : String(err), null);
43221
+ }
42891
43222
  return new Promise((resolve21, reject) => {
42892
43223
  let parsed;
42893
43224
  try {
@@ -42946,10 +43277,11 @@ var init_api = __esm({
42946
43277
  */
42947
43278
  buildRequest(method, contentType, body, extraHeaders, targetUrl) {
42948
43279
  const headers = { "User-Agent": `Tina4/${TINA4_VERSION}`, ...this.headers };
42949
- if (this.authHeader) {
43280
+ const sameOriginAsBase = targetUrl === void 0 || sameOrigin(targetUrl, this.baseUrl);
43281
+ if (this.authHeader && sameOriginAsBase) {
42950
43282
  headers["Authorization"] = this.authHeader;
42951
43283
  }
42952
- if (this.cookiesEnabled) {
43284
+ if (this.cookiesEnabled && sameOriginAsBase) {
42953
43285
  const cookieHeader = this.cookieHeader();
42954
43286
  if (cookieHeader) {
42955
43287
  headers["Cookie"] = cookieHeader;
@@ -43050,7 +43382,12 @@ var init_api = __esm({
43050
43382
  * bearer token / session cookie never leaks to a host you didn't
43051
43383
  * authenticate to.
43052
43384
  */
43053
- performRequest(method, url, headers, data, redirectsLeft) {
43385
+ async performRequest(method, url, headers, data, redirectsLeft) {
43386
+ try {
43387
+ await guardUrl(url, this.allowHosts);
43388
+ } catch (err) {
43389
+ return { kind: "error", error: err instanceof Error ? err.message : String(err) };
43390
+ }
43054
43391
  return new Promise((resolve21) => {
43055
43392
  let parsed;
43056
43393
  try {
@@ -45790,6 +46127,7 @@ var CURVE, RECORD_SIZE, MAX_PAYLOAD, PushError, Push;
45790
46127
  var init_push = __esm({
45791
46128
  "src/push.ts"() {
45792
46129
  "use strict";
46130
+ init_ssrf();
45793
46131
  CURVE = "prime256v1";
45794
46132
  RECORD_SIZE = 4096;
45795
46133
  MAX_PAYLOAD = RECORD_SIZE - 17;
@@ -45864,10 +46202,19 @@ var init_push = __esm({
45864
46202
  return { rawPublic, rawPrivate };
45865
46203
  }
45866
46204
  async deliver(endpoint, config, keys, body) {
46205
+ try {
46206
+ await guardUrl(endpoint.toString(), this.options.allowHosts);
46207
+ } catch (error2) {
46208
+ if (error2 instanceof SsrfError) throw new PushError(error2.message);
46209
+ throw error2;
46210
+ }
45867
46211
  let response;
45868
46212
  try {
45869
46213
  response = await fetch(endpoint, {
45870
46214
  method: "POST",
46215
+ // A real push service answers the POST directly; a redirect from a push
46216
+ // endpoint is not followed to a private address (ADR-0084).
46217
+ redirect: "manual",
45871
46218
  headers: {
45872
46219
  Authorization: `vapid t=${vapidToken(endpoint.toString(), config.subject, keys.rawPrivate, keys.rawPublic)}, k=${config.publicKey}`,
45873
46220
  "Content-Encoding": "aes128gcm",
@@ -47954,6 +48301,7 @@ var init_testClient = __esm({
47954
48301
  reqHeaders["content-length"] = String(Buffer.byteLength(rawBody));
47955
48302
  }
47956
48303
  const socket = new Socket();
48304
+ Object.defineProperty(socket, "remoteAddress", { value: "127.0.0.1" });
47957
48305
  const rawReq = new IncomingMessage2(socket);
47958
48306
  rawReq.method = method.toUpperCase();
47959
48307
  rawReq.url = path10;
@@ -48326,6 +48674,7 @@ var init_validator = __esm({
48326
48674
  var index_exports = {};
48327
48675
  __export(index_exports, {
48328
48676
  AI_TOOLS: () => AI_TOOLS,
48677
+ ALLOW_PRIVATE_ENV: () => ALLOW_PRIVATE_ENV,
48329
48678
  APPLICATION_FORM: () => APPLICATION_FORM,
48330
48679
  APPLICATION_JSON: () => APPLICATION_JSON,
48331
48680
  APPLICATION_OCTET: () => APPLICATION_OCTET,
@@ -48433,6 +48782,7 @@ __export(index_exports, {
48433
48782
  Session: () => Session,
48434
48783
  Sso: () => Sso,
48435
48784
  SsoError: () => SsoError,
48785
+ SsrfError: () => SsrfError,
48436
48786
  TAKEOVER_KILLED: () => TAKEOVER_KILLED,
48437
48787
  TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
48438
48788
  TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
@@ -48466,6 +48816,7 @@ __export(index_exports, {
48466
48816
  addHtmlHelpers: () => addHtmlHelpers,
48467
48817
  algorithmAvailable: () => algorithmAvailable,
48468
48818
  allEnv: () => allEnv,
48819
+ allowPrivateRequests: () => allowPrivateRequests,
48469
48820
  any: () => any,
48470
48821
  attachCsrfFromEnv: () => attachCsrfFromEnv,
48471
48822
  authMiddleware: () => authMiddleware,
@@ -48531,6 +48882,7 @@ __export(index_exports, {
48531
48882
  graphqlAutoSchemaEnabled: () => graphqlAutoSchemaEnabled,
48532
48883
  graphqlEndpoint: () => graphqlEndpoint,
48533
48884
  graphqlMaxDepth: () => graphqlMaxDepth,
48885
+ guardUrl: () => guardUrl,
48534
48886
  handle: () => handle,
48535
48887
  handleFeedbackTurn: () => handleFeedbackTurn,
48536
48888
  handleFeedbackWidgetJs: () => handleFeedbackWidgetJs,
@@ -48544,6 +48896,7 @@ __export(index_exports, {
48544
48896
  installAll: () => installAll,
48545
48897
  installSelected: () => installSelected,
48546
48898
  isBannerSuppressed: () => isBannerSuppressed,
48899
+ isBlockedAddress: () => isBlockedAddress,
48547
48900
  isDebugMode: () => isDebugMode,
48548
48901
  isDev: () => isDev,
48549
48902
  isInstalled: () => isInstalled,
@@ -48662,6 +49015,7 @@ var init_index = __esm({
48662
49015
  init_service();
48663
49016
  init_cache();
48664
49017
  init_api();
49018
+ init_ssrf();
48665
49019
  init_context();
48666
49020
  init_events();
48667
49021
  init_devAdmin();
@@ -48700,6 +49054,7 @@ var init_index = __esm({
48700
49054
  init_index();
48701
49055
  export {
48702
49056
  AI_TOOLS,
49057
+ ALLOW_PRIVATE_ENV,
48703
49058
  APPLICATION_FORM,
48704
49059
  APPLICATION_JSON,
48705
49060
  APPLICATION_OCTET,
@@ -48807,6 +49162,7 @@ export {
48807
49162
  Session,
48808
49163
  Sso,
48809
49164
  SsoError,
49165
+ SsrfError,
48810
49166
  TAKEOVER_KILLED,
48811
49167
  TAKEOVER_NOTHING,
48812
49168
  TAKEOVER_REFUSALS,
@@ -48840,6 +49196,7 @@ export {
48840
49196
  addHtmlHelpers,
48841
49197
  algorithmAvailable,
48842
49198
  allEnv,
49199
+ allowPrivateRequests,
48843
49200
  any,
48844
49201
  attachCsrfFromEnv,
48845
49202
  authMiddleware,
@@ -48905,6 +49262,7 @@ export {
48905
49262
  graphqlAutoSchemaEnabled,
48906
49263
  graphqlEndpoint,
48907
49264
  graphqlMaxDepth,
49265
+ guardUrl,
48908
49266
  handle,
48909
49267
  handleFeedbackTurn,
48910
49268
  handleFeedbackWidgetJs,
@@ -48918,6 +49276,7 @@ export {
48918
49276
  installAll,
48919
49277
  installSelected,
48920
49278
  isBannerSuppressed,
49279
+ isBlockedAddress,
48921
49280
  isDebugMode,
48922
49281
  isDev,
48923
49282
  isInstalled,