@getstrata/core 0.7.5 → 1.0.1

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 (41) hide show
  1. package/CHANGELOG.md +52 -33
  2. package/README.md +36 -43
  3. package/dist/core/database/baseRepository.d.ts +2 -1
  4. package/dist/core/database/dialect.d.ts +5 -1
  5. package/dist/core/database/errors.d.ts +4 -0
  6. package/dist/core/database/mysqlConnection.d.ts +3 -1
  7. package/dist/core/errors/http.d.ts +4 -1
  8. package/dist/core/http/clientIp.d.ts +6 -3
  9. package/dist/core/http/webErrorResponse.d.ts +3 -1
  10. package/dist/core/runtime/appEnv.d.ts +4 -0
  11. package/dist/core/tenant/tenancyConfig.d.ts +1 -0
  12. package/dist/entries/audit/exportAuditLogs.js +7 -5
  13. package/dist/entries/auth/scimAuthMiddleware.js +9 -7
  14. package/dist/entries/auth/sessionCookie.js +2 -2
  15. package/dist/entries/auth/sessionGuard.js +2 -2
  16. package/dist/entries/contracts/authUserDirectory.js +1 -0
  17. package/dist/entries/database/errors.js +45 -6
  18. package/dist/entries/database/model.js +4 -4
  19. package/dist/entries/database/mysqlConnection.js +18 -2
  20. package/dist/entries/database/query.js +2 -2
  21. package/dist/entries/database/repositoryQuery.js +20 -20
  22. package/dist/entries/database/schema.js +2 -2
  23. package/dist/entries/database/sqliteConnection.js +5 -0
  24. package/dist/entries/http/clientIp.js +24 -6
  25. package/dist/entries/http/corsMiddleware.js +14 -3
  26. package/dist/entries/http/loginThrottleMiddleware.js +25 -8
  27. package/dist/entries/http/memoryThrottleMiddleware.js +25 -8
  28. package/dist/entries/http/response.js +66 -7
  29. package/dist/entries/http/scimThrottleMiddleware.js +23 -6
  30. package/dist/entries/http/throttleMiddleware.js +25 -8
  31. package/dist/entries/http/webErrorResponse.js +66 -7
  32. package/dist/entries/jobs/exportAuditLogsJob.js +7 -5
  33. package/dist/entries/logging/requestLoggingMiddleware.js +29 -10
  34. package/dist/entries/openapi/generator.js +5 -5
  35. package/dist/entries/runtime/appEnv.js +8 -0
  36. package/dist/entries/tenant/databaseTenantContext.js +7 -5
  37. package/dist/entries/tenant/tenancyConfig.js +7 -5
  38. package/dist/entries/tenant/tenantDatabaseScope.js +9 -7
  39. package/dist/framework/public-api.d.ts +3 -3
  40. package/dist/index.js +212 -90
  41. package/package.json +16 -3
package/dist/index.js CHANGED
@@ -120,6 +120,12 @@ class PreconditionFailedError extends HttpError {
120
120
  super(412, message, details);
121
121
  }
122
122
  }
123
+
124
+ class InternalServerError extends HttpError {
125
+ constructor(message = "Internal server error.", details) {
126
+ super(500, message, details);
127
+ }
128
+ }
123
129
  function isHttpErrorLike(error) {
124
130
  if (typeof error !== "object" || error === null) {
125
131
  return false;
@@ -326,11 +332,11 @@ function abilityCatalog() {
326
332
 
327
333
  // ../../src/core/auth/guard.ts
328
334
  function devHeaderAbilities(role) {
329
- const catalog2 = abilityCatalog();
335
+ const catalog = abilityCatalog();
330
336
  if (role === "admin") {
331
- return [...catalog2.admin];
337
+ return [...catalog.admin];
332
338
  }
333
- return [...catalog2.member];
339
+ return [...catalog.member];
334
340
  }
335
341
 
336
342
  class GuestGuard {
@@ -1144,7 +1150,7 @@ function resolveScimTenantFromToken(token) {
1144
1150
  function resolveRepositoryConnection() {
1145
1151
  return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
1146
1152
  }
1147
- var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
1153
+ var repositoryConnection = new Proxy(function repositoryConnection() {}, {
1148
1154
  apply(_target, _thisArg, args) {
1149
1155
  return resolveRepositoryConnection()(...args);
1150
1156
  },
@@ -1156,14 +1162,16 @@ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
1156
1162
  });
1157
1163
 
1158
1164
  // ../../src/core/tenant/tenancyConfig.ts
1165
+ var TENANCY_DRIVERS = ["rls", "column", "none"];
1159
1166
  function readTenancyDriver(env = process.env) {
1160
- if (env.TENANCY_DRIVER === "none") {
1161
- return "none";
1167
+ const raw = env.TENANCY_DRIVER?.trim();
1168
+ if (raw === undefined || raw === "") {
1169
+ return "rls";
1162
1170
  }
1163
- if (env.TENANCY_DRIVER === "column") {
1164
- return "column";
1171
+ if (TENANCY_DRIVERS.includes(raw)) {
1172
+ return raw;
1165
1173
  }
1166
- return "rls";
1174
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
1167
1175
  }
1168
1176
  function isTenancyEnabled(env = process.env) {
1169
1177
  return readTenancyDriver(env) !== "none";
@@ -1202,8 +1210,8 @@ async function runWithTenantDatabase(tenant, callback) {
1202
1210
  return await runWithTenant(tenant, callback);
1203
1211
  }
1204
1212
  if (hasActiveDatabaseConnection()) {
1205
- const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
1206
- await applyTenantContextToTransaction(activeConnection2, tenant.id);
1213
+ const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
1214
+ await applyTenantContextToTransaction(activeConnection, tenant.id);
1207
1215
  return await runWithTenant(tenant, callback);
1208
1216
  }
1209
1217
  return await getDefaultDatabasePool().begin(async (transaction) => {
@@ -1864,14 +1872,55 @@ function getPostgresSqlState(error) {
1864
1872
  }
1865
1873
  return;
1866
1874
  }
1875
+ var MYSQL_ERRNO_MESSAGES = {
1876
+ 1062: () => new ConflictError("A record with these values already exists."),
1877
+ 1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
1878
+ 1452: () => new UnprocessableEntityError("Referenced record does not exist."),
1879
+ 1048: () => new BadRequestError("Required field is missing."),
1880
+ 3819: () => new BadRequestError("Value violates a database constraint.")
1881
+ };
1882
+ function mapSqliteError(error) {
1883
+ const code = typeof error.code === "string" ? error.code : "";
1884
+ if (!code.startsWith("SQLITE_")) {
1885
+ return null;
1886
+ }
1887
+ if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
1888
+ return new ConflictError("A record with these values already exists.");
1889
+ }
1890
+ if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
1891
+ return new UnprocessableEntityError("Referenced record does not exist.");
1892
+ }
1893
+ if (code === "SQLITE_CONSTRAINT_NOTNULL") {
1894
+ return new BadRequestError("Required field is missing.");
1895
+ }
1896
+ if (code.startsWith("SQLITE_CONSTRAINT")) {
1897
+ return new BadRequestError("Value violates a database constraint.");
1898
+ }
1899
+ return new InternalServerError("Database operation failed.");
1900
+ }
1901
+ function mapMysqlError(error) {
1902
+ const code = typeof error.code === "string" ? error.code : "";
1903
+ if (!code.startsWith("ER_")) {
1904
+ return null;
1905
+ }
1906
+ const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
1907
+ return factory ? factory() : new InternalServerError("Database operation failed.");
1908
+ }
1867
1909
  function mapDatabaseError(error) {
1868
1910
  const httpError = toHttpError(error);
1869
1911
  if (httpError) {
1870
1912
  return httpError;
1871
1913
  }
1872
1914
  if (!isPostgresError(error)) {
1873
- const message = error instanceof Error ? error.message : "Database operation failed.";
1874
- return new BadRequestError(message);
1915
+ return new InternalServerError;
1916
+ }
1917
+ const sqlite = mapSqliteError(error);
1918
+ if (sqlite) {
1919
+ return sqlite;
1920
+ }
1921
+ const mysql = mapMysqlError(error);
1922
+ if (mysql) {
1923
+ return mysql;
1875
1924
  }
1876
1925
  const sqlState = getPostgresSqlState(error);
1877
1926
  switch (sqlState) {
@@ -1892,10 +1941,7 @@ function mapDatabaseError(error) {
1892
1941
  constraint: error.constraint
1893
1942
  });
1894
1943
  default:
1895
- return new BadRequestError(error.message ?? "Database operation failed.", {
1896
- code: error.code,
1897
- sqlState
1898
- });
1944
+ return new InternalServerError("Database operation failed.");
1899
1945
  }
1900
1946
  }
1901
1947
  async function withDatabaseErrorHandling(operation) {
@@ -1964,6 +2010,9 @@ var postgresDialect = {
1964
2010
  nowExpression() {
1965
2011
  return "NOW()";
1966
2012
  },
2013
+ timestampValue(value) {
2014
+ return value.toISOString();
2015
+ },
1967
2016
  returningClause(columns) {
1968
2017
  return ` RETURNING ${columns}`;
1969
2018
  },
@@ -1988,6 +2037,9 @@ var mysqlDialect = {
1988
2037
  nowExpression() {
1989
2038
  return "CURRENT_TIMESTAMP";
1990
2039
  },
2040
+ timestampValue(value) {
2041
+ return value.toISOString().slice(0, 19).replace("T", " ");
2042
+ },
1991
2043
  returningClause() {
1992
2044
  return "";
1993
2045
  },
@@ -2010,7 +2062,10 @@ var sqliteDialect = {
2010
2062
  return `"${assertSafeIdentifier(identifier)}"`;
2011
2063
  },
2012
2064
  nowExpression() {
2013
- return "CURRENT_TIMESTAMP";
2065
+ return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
2066
+ },
2067
+ timestampValue(value) {
2068
+ return value.toISOString();
2014
2069
  },
2015
2070
  returningClause(columns) {
2016
2071
  return ` RETURNING ${columns}`;
@@ -2042,6 +2097,9 @@ function useSqlDialect(driver) {
2042
2097
  dialectOverride = dialectFor(driver);
2043
2098
  return dialectOverride;
2044
2099
  }
2100
+ function sqlTimestamp(value = new Date) {
2101
+ return currentSqlDialect().timestampValue(value);
2102
+ }
2045
2103
  function resetSqlDialect() {
2046
2104
  dialectOverride = null;
2047
2105
  }
@@ -2316,8 +2374,8 @@ function buildSelectList(table, select, params = []) {
2316
2374
  }
2317
2375
  return select.map((item) => {
2318
2376
  if (item.kind === "column") {
2319
- const column2 = qualifyColumn(item.table, item.column);
2320
- return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
2377
+ const column = qualifyColumn(item.table, item.column);
2378
+ return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
2321
2379
  }
2322
2380
  if (item.kind === "literalText") {
2323
2381
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
@@ -2966,50 +3024,50 @@ class RepositoryQuery {
2966
3024
  }
2967
3025
  async hydrateEagerLoad(rows, result, load) {
2968
3026
  if (load.kind === "hasMany") {
2969
- const relation2 = load.relation;
2970
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
3027
+ const relation = load.relation;
3028
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
2971
3029
  for (const row of result) {
2972
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
3030
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
2973
3031
  }
2974
3032
  return;
2975
3033
  }
2976
3034
  if (load.kind === "morphMany") {
2977
- const relation2 = load.relation;
2978
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
3035
+ const relation = load.relation;
3036
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
2979
3037
  for (const row of result) {
2980
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
3038
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
2981
3039
  }
2982
3040
  return;
2983
3041
  }
2984
3042
  if (load.kind === "morphOne") {
2985
- const relation2 = load.relation;
2986
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
3043
+ const relation = load.relation;
3044
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
2987
3045
  for (const row of result) {
2988
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]);
3046
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
2989
3047
  }
2990
3048
  return;
2991
3049
  }
2992
3050
  if (load.kind === "hasManyThrough") {
2993
- const relation2 = load.relation;
2994
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
3051
+ const relation = load.relation;
3052
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
2995
3053
  for (const row of result) {
2996
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
3054
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
2997
3055
  }
2998
3056
  return;
2999
3057
  }
3000
3058
  if (load.kind === "belongsToMany") {
3001
- const relation2 = load.relation;
3002
- const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
3059
+ const relation = load.relation;
3060
+ const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
3003
3061
  for (const row of result) {
3004
- row[load.as] = getByRelationKey(grouped2, row[relation2.parentKey]) ?? [];
3062
+ row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
3005
3063
  }
3006
3064
  return;
3007
3065
  }
3008
3066
  if (load.kind === "morphTo") {
3009
- const relation2 = load.relation;
3010
- const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
3067
+ const relation = load.relation;
3068
+ const grouped = await this.repository.loadMorphToForChildren(rows, relation, load.morphRepositories ?? new Map, load.options);
3011
3069
  for (const row of result) {
3012
- row[load.as] = getByRelationKey(grouped2, row[relation2.morphIdKey]);
3070
+ row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
3013
3071
  }
3014
3072
  return;
3015
3073
  }
@@ -4946,8 +5004,8 @@ class Model {
4946
5004
  }
4947
5005
  if (updating) {
4948
5006
  const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
4949
- const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
4950
- this.attributes = ModelClass.hydrateAttributes(record2);
5007
+ const record = await this.repository.updateByIdOrThrow(this.id, changes);
5008
+ this.attributes = ModelClass.hydrateAttributes(record);
4951
5009
  await runObservers(this, "updated");
4952
5010
  await runObservers(this, "saved");
4953
5011
  return this;
@@ -5177,11 +5235,26 @@ function createMysqlConnectionFromPool(pool) {
5177
5235
  }
5178
5236
  };
5179
5237
  }
5238
+ var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
5239
+ function pinSessionToUtc(connection) {
5240
+ connection.query(MYSQL_SESSION_UTC, (error) => {
5241
+ if (error) {
5242
+ console.warn(`[mysql] Could not set the session time zone to UTC; DATETIME comparisons may drift: ${error instanceof Error ? error.message : String(error)}`);
5243
+ }
5244
+ });
5245
+ }
5246
+ function createMysqlPool(url) {
5247
+ const pool = mysql.createPool({ uri: url, timezone: "Z" });
5248
+ pool.on("connection", (connection) => {
5249
+ pinSessionToUtc(connection);
5250
+ });
5251
+ return pool;
5252
+ }
5180
5253
  function createMysqlConnection(url) {
5181
5254
  if (!url.trim()) {
5182
5255
  throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
5183
5256
  }
5184
- return createMysqlConnectionFromPool(mysql.createPool(url));
5257
+ return createMysqlConnectionFromPool(createMysqlPool(url));
5185
5258
  }
5186
5259
  // ../../src/core/database/namedConnections.ts
5187
5260
  var REGISTRY_KEY = Symbol.for("@getstrata/namedConnections");
@@ -5845,6 +5918,11 @@ function createSqliteConnection(filename) {
5845
5918
  }
5846
5919
  const db = new Database(filename, { create: true });
5847
5920
  db.exec("PRAGMA foreign_keys = ON");
5921
+ db.exec("PRAGMA busy_timeout = 5000");
5922
+ if (filename !== ":memory:") {
5923
+ db.exec("PRAGMA journal_mode = WAL");
5924
+ db.exec("PRAGMA synchronous = NORMAL");
5925
+ }
5848
5926
  return {
5849
5927
  async unsafe(query, params = []) {
5850
5928
  const statement = db.query(query);
@@ -6257,20 +6335,48 @@ function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
6257
6335
  return await next();
6258
6336
  };
6259
6337
  }
6338
+ // ../../src/core/http/requestMetaContext.ts
6339
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
6340
+ function runWithRequestMeta(meta, callback) {
6341
+ return requestMetaContext.run(meta, callback);
6342
+ }
6343
+ function currentRequestMeta() {
6344
+ return requestMetaContext.getStore() ?? {
6345
+ ipAddress: null,
6346
+ userAgent: null
6347
+ };
6348
+ }
6349
+
6260
6350
  // ../../src/core/http/clientIp.ts
6261
6351
  function trustForwardedFor(env = process.env) {
6262
6352
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
6263
6353
  }
6264
- function readClientIp(request, env = process.env) {
6265
- if (!trustForwardedFor(env)) {
6266
- return;
6354
+ function isPrivateAddress(address) {
6355
+ const ip = address.replace(/^::ffff:/i, "");
6356
+ return ip === "::1" || ip === "localhost" || /^127\./.test(ip) || /^10\./.test(ip) || /^192\.168\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip) || /^169\.254\./.test(ip) || /^f[cd][0-9a-f]{2}:/i.test(ip) || /^fe80:/i.test(ip);
6357
+ }
6358
+ function forwardedClientIp(request) {
6359
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
6360
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
6361
+ const hop = hops[index];
6362
+ if (hop && !isPrivateAddress(hop)) {
6363
+ return hop;
6364
+ }
6267
6365
  }
6268
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
6269
- if (forwarded) {
6270
- return forwarded;
6366
+ if (hops.length > 0) {
6367
+ return hops[hops.length - 1];
6271
6368
  }
6272
6369
  return request.headers.get("x-real-ip")?.trim() || undefined;
6273
6370
  }
6371
+ function readClientIp(request, env = process.env) {
6372
+ if (trustForwardedFor(env)) {
6373
+ const forwarded = forwardedClientIp(request);
6374
+ if (forwarded) {
6375
+ return forwarded;
6376
+ }
6377
+ }
6378
+ return currentRequestMeta().ipAddress ?? undefined;
6379
+ }
6274
6380
  // ../../src/core/crypto/nonCryptographicHash.ts
6275
6381
  function nonCryptographicDigest(input) {
6276
6382
  return Bun.hash(input).toString(16);
@@ -6572,10 +6678,18 @@ function readRequestCookie(request, name) {
6572
6678
  function readBunRequestCookie(request, name) {
6573
6679
  return request.cookies.get(name) ?? readRequestCookie(request, name);
6574
6680
  }
6681
+ // ../../src/core/runtime/appEnv.ts
6682
+ function isProductionEnv(env = process.env) {
6683
+ return env.APP_ENV === "production" || env.NODE_ENV === "production";
6684
+ }
6685
+
6575
6686
  // ../../src/core/http/corsMiddleware.ts
6687
+ function defaultAllowedOrigins() {
6688
+ return isProductionEnv() ? "" : "*";
6689
+ }
6576
6690
  function resolveCorsConfig() {
6577
6691
  return {
6578
- allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
6692
+ allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
6579
6693
  allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
6580
6694
  allowedHeaders: [
6581
6695
  "Authorization",
@@ -6615,30 +6729,19 @@ function buildCorsHeaders(request) {
6615
6729
  const origin = request.headers.get("origin");
6616
6730
  const corsConfig = resolveCorsConfig();
6617
6731
  const allowedOrigins = corsConfig.allowedOrigins;
6618
- const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
6732
+ headers.set("Vary", "Origin");
6733
+ const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
6734
+ if (!allowOrigin) {
6735
+ return headers;
6736
+ }
6619
6737
  headers.set("Access-Control-Allow-Origin", allowOrigin);
6620
6738
  headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
6621
6739
  headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
6622
6740
  headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
6623
- headers.set("Vary", "Origin");
6624
6741
  return headers;
6625
6742
  }
6626
6743
  // ../../src/core/http/csrfToken.ts
6627
6744
  import { timingSafeEqual as timingSafeEqual3 } from "crypto";
6628
-
6629
- // ../../src/core/http/requestMetaContext.ts
6630
- var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
6631
- function runWithRequestMeta(meta, callback) {
6632
- return requestMetaContext.run(meta, callback);
6633
- }
6634
- function currentRequestMeta() {
6635
- return requestMetaContext.getStore() ?? {
6636
- ipAddress: null,
6637
- userAgent: null
6638
- };
6639
- }
6640
-
6641
- // ../../src/core/http/csrfToken.ts
6642
6745
  var CSRF_COOKIE = appCookieName("csrf");
6643
6746
  var CSRF_TTL_MS = 60 * 60 * 1000;
6644
6747
  function csrfCookieName() {
@@ -6920,9 +7023,9 @@ class FormRequest {
6920
7023
  }
6921
7024
  }
6922
7025
  // ../../src/core/http/authMiddleware.ts
6923
- function createAuthMiddleware(auth2) {
7026
+ function createAuthMiddleware(auth) {
6924
7027
  return async (request, next) => {
6925
- const user = await auth2.resolve(request);
7028
+ const user = await auth.resolve(request);
6926
7029
  return await runWithAuthUser(user, async () => {
6927
7030
  const response = await next();
6928
7031
  if (user) {
@@ -6939,9 +7042,9 @@ function createAuthMiddleware(auth2) {
6939
7042
  };
6940
7043
  }
6941
7044
  // ../../src/core/http/authorizeMiddleware.ts
6942
- function createAuthorizeMiddleware(gate, auth2, resource, action) {
7045
+ function createAuthorizeMiddleware(gate, auth, resource, action) {
6943
7046
  return async (request, next) => {
6944
- const user = currentAuthUser() ?? await auth2.resolve(request);
7047
+ const user = currentAuthUser() ?? await auth.resolve(request);
6945
7048
  if (!gate.allows(resource, action, user)) {
6946
7049
  const error = new ForbiddenError;
6947
7050
  return Response.json({ error: error.message }, { status: error.status });
@@ -7115,9 +7218,9 @@ async function parseMultipartUpload(request, fieldName = "file") {
7115
7218
  };
7116
7219
  }
7117
7220
  // ../../src/core/http/requireAuthMiddleware.ts
7118
- function createRequireAuthMiddleware(auth2) {
7221
+ function createRequireAuthMiddleware(auth) {
7119
7222
  return async (request, next) => {
7120
- if (!await auth2.check(request)) {
7223
+ if (!await auth.check(request)) {
7121
7224
  const error = new UnauthorizedError;
7122
7225
  return Response.json({ error: error.message }, { status: error.status });
7123
7226
  }
@@ -7412,14 +7515,27 @@ function errorPageTitle(status, message) {
7412
7515
  return message;
7413
7516
  }
7414
7517
  function publicErrorMessage(status, message) {
7415
- if (status >= 500 && false) {}
7518
+ if (status >= 500 && isProductionEnv()) {
7519
+ return "Something went wrong.";
7520
+ }
7416
7521
  return message;
7417
7522
  }
7523
+ function logServerError(error, mappedError) {
7524
+ if (mappedError.status < 500) {
7525
+ return;
7526
+ }
7527
+ appLogger.error("Unhandled request error", {
7528
+ status: mappedError.status,
7529
+ error: error instanceof Error ? error.message : String(error),
7530
+ stack: error instanceof Error ? error.stack : undefined
7531
+ });
7532
+ }
7418
7533
  async function webErrorResponse(error, request) {
7419
7534
  if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
7420
7535
  return null;
7421
7536
  }
7422
7537
  const mappedError = toHttpError(error) ?? mapDatabaseError(error);
7538
+ logServerError(error, mappedError);
7423
7539
  if (mappedError.status === 401) {
7424
7540
  return Response.redirect(loginRedirectLocation(request), 302);
7425
7541
  }
@@ -7448,6 +7564,7 @@ function noContentResponse() {
7448
7564
  }
7449
7565
  function errorResponse(error) {
7450
7566
  const mappedError = toHttpError(error) ?? mapDatabaseError(error);
7567
+ logServerError(error, mappedError);
7451
7568
  return Response.json({
7452
7569
  error: mappedError.message,
7453
7570
  ...mappedError.details === undefined ? {} : { details: mappedError.details }
@@ -7513,8 +7630,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
7513
7630
  const id = parsePositiveIntParam(String(request.params[param]), String(param));
7514
7631
  const model = requireResolvedModel(await resolver(id, request));
7515
7632
  const gate = resolveApplicationPolicyGate();
7516
- const auth2 = resolveApplicationAuth();
7517
- const user = currentAuthUser() ?? await auth2.resolve(request);
7633
+ const auth = resolveApplicationAuth();
7634
+ const user = currentAuthUser() ?? await auth.resolve(request);
7518
7635
  gate.authorize(authorization.resource, authorization.action, user, model);
7519
7636
  if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
7520
7637
  assertIfMatch(request, etagFromResource(model), {
@@ -7536,8 +7653,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
7536
7653
  }
7537
7654
  const model = requireResolvedModel(await resolver(key, request));
7538
7655
  const gate = resolveApplicationPolicyGate();
7539
- const auth2 = resolveApplicationAuth();
7540
- const user = currentAuthUser() ?? await auth2.resolve(request);
7656
+ const auth = resolveApplicationAuth();
7657
+ const user = currentAuthUser() ?? await auth.resolve(request);
7541
7658
  gate.authorize(authorization.resource, authorization.action, user, model);
7542
7659
  if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
7543
7660
  assertIfMatch(request, etagFromResource(model), {
@@ -7859,9 +7976,9 @@ function createIntendedUrlCookieFromRequest(request) {
7859
7976
  }
7860
7977
 
7861
7978
  // ../../src/core/http/requireWebAuthMiddleware.ts
7862
- function createRequireWebAuthMiddleware(auth2) {
7979
+ function createRequireWebAuthMiddleware(auth) {
7863
7980
  return async (request, next) => {
7864
- const user = await auth2.resolve(request);
7981
+ const user = await auth.resolve(request);
7865
7982
  if (user) {
7866
7983
  return await runWithAuthUser(user, () => next());
7867
7984
  }
@@ -8150,9 +8267,10 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
8150
8267
  // ../../src/core/logging/requestLoggingMiddleware.ts
8151
8268
  function createRequestLoggingMiddleware() {
8152
8269
  return async (request, next) => {
8270
+ const ipAddress = readClientIp(request) ?? null;
8153
8271
  return await runWithRequestMeta({
8154
8272
  ...currentRequestMeta(),
8155
- ipAddress: readClientIp(request) ?? null,
8273
+ ipAddress,
8156
8274
  userAgent: request.headers.get("user-agent"),
8157
8275
  request
8158
8276
  }, async () => {
@@ -8165,7 +8283,8 @@ function createRequestLoggingMiddleware() {
8165
8283
  method: request.method,
8166
8284
  path: new URL(request.url).pathname,
8167
8285
  status: response.status,
8168
- durationMs
8286
+ durationMs,
8287
+ ipAddress
8169
8288
  });
8170
8289
  return response;
8171
8290
  });
@@ -8290,15 +8409,15 @@ function buildMarkdownMailMessage(input) {
8290
8409
  html: rendered.html
8291
8410
  };
8292
8411
  }
8293
- async function sendMarkdownMail(mailer2, input) {
8294
- await mailer2.send(buildMarkdownMailMessage(input));
8412
+ async function sendMarkdownMail(mailer, input) {
8413
+ await mailer.send(buildMarkdownMailMessage(input));
8295
8414
  }
8296
8415
  // ../../src/core/notifications/dispatcher.ts
8297
8416
  class NotificationDispatcher {
8298
8417
  mailer;
8299
8418
  databaseStore;
8300
- constructor(mailer2, databaseStore = null) {
8301
- this.mailer = mailer2;
8419
+ constructor(mailer, databaseStore = null) {
8420
+ this.mailer = mailer;
8302
8421
  this.databaseStore = databaseStore;
8303
8422
  }
8304
8423
  async send(notifiable, notification) {
@@ -8350,8 +8469,8 @@ class NotificationDispatcher {
8350
8469
  });
8351
8470
  }
8352
8471
  }
8353
- function createNotificationDispatcher(mailer2, databaseStore) {
8354
- return new NotificationDispatcher(mailer2, databaseStore ?? null);
8472
+ function createNotificationDispatcher(mailer, databaseStore) {
8473
+ return new NotificationDispatcher(mailer, databaseStore ?? null);
8355
8474
  }
8356
8475
  // ../../src/core/notifications/notification.ts
8357
8476
  class Notification {
@@ -8481,9 +8600,9 @@ function readSharedJobRegistry() {
8481
8600
  if (globalRegistry) {
8482
8601
  return globalRegistry;
8483
8602
  }
8484
- const registry2 = new JobRegistry;
8485
- globalThis[JOB_REGISTRY_KEY] = registry2;
8486
- return registry2;
8603
+ const registry = new JobRegistry;
8604
+ globalThis[JOB_REGISTRY_KEY] = registry;
8605
+ return registry;
8487
8606
  }
8488
8607
  var jobRegistry = readSharedJobRegistry();
8489
8608
 
@@ -9298,9 +9417,9 @@ class EtaViewEngine {
9298
9417
  autoTrim: false
9299
9418
  });
9300
9419
  this.resolveLayoutData = resolveLayoutData;
9301
- const readFile2 = this.eta.readFile?.bind(this.eta);
9420
+ const readFile = this.eta.readFile?.bind(this.eta);
9302
9421
  this.eta.readFile = (path) => {
9303
- const source = readFile2 ? readFile2(path) : "";
9422
+ const source = readFile ? readFile(path) : "";
9304
9423
  assertEtaHtmlSource(relative(viewsDirectory, path) || path, source);
9305
9424
  return source;
9306
9425
  };
@@ -9406,6 +9525,7 @@ export {
9406
9525
  HasManyRelationQuery,
9407
9526
  HasOneRelationQuery,
9408
9527
  HttpError,
9528
+ InternalServerError,
9409
9529
  Job,
9410
9530
  JsonResource,
9411
9531
  JwtGuard,
@@ -9496,6 +9616,7 @@ export {
9496
9616
  createMetricsMiddleware,
9497
9617
  createMysqlConnection,
9498
9618
  createMysqlConnectionFromPool,
9619
+ createMysqlPool,
9499
9620
  createNotificationDispatcher,
9500
9621
  createProductionQueue,
9501
9622
  createQueue,
@@ -9678,6 +9799,7 @@ export {
9678
9799
  signedUrl,
9679
9800
  singularize,
9680
9801
  spaContentSecurityPolicy,
9802
+ sqlTimestamp,
9681
9803
  storageFacade as storage,
9682
9804
  strictApiContentSecurityPolicy,
9683
9805
  stringRule,