@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
@@ -298,8 +298,8 @@ function buildSelectList(table, select, params = []) {
298
298
  }
299
299
  return select.map((item) => {
300
300
  if (item.kind === "column") {
301
- const column2 = qualifyColumn(item.table, item.column);
302
- return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
301
+ const column = qualifyColumn(item.table, item.column);
302
+ return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
303
303
  }
304
304
  if (item.kind === "literalText") {
305
305
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
@@ -1919,8 +1919,8 @@ class Model {
1919
1919
  }
1920
1920
  if (updating) {
1921
1921
  const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
1922
- const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
1923
- this.attributes = ModelClass.hydrateAttributes(record2);
1922
+ const record = await this.repository.updateByIdOrThrow(this.id, changes);
1923
+ this.attributes = ModelClass.hydrateAttributes(record);
1924
1924
  await runObservers(this, "updated");
1925
1925
  await runObservers(this, "saved");
1926
1926
  return this;
@@ -23,13 +23,29 @@ function createMysqlConnectionFromPool(pool) {
23
23
  }
24
24
  };
25
25
  }
26
+ var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
27
+ function pinSessionToUtc(connection) {
28
+ connection.query(MYSQL_SESSION_UTC, (error) => {
29
+ if (error) {
30
+ console.warn(`[mysql] Could not set the session time zone to UTC; DATETIME comparisons may drift: ${error instanceof Error ? error.message : String(error)}`);
31
+ }
32
+ });
33
+ }
34
+ function createMysqlPool(url) {
35
+ const pool = mysql.createPool({ uri: url, timezone: "Z" });
36
+ pool.on("connection", (connection) => {
37
+ pinSessionToUtc(connection);
38
+ });
39
+ return pool;
40
+ }
26
41
  function createMysqlConnection(url) {
27
42
  if (!url.trim()) {
28
43
  throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
29
44
  }
30
- return createMysqlConnectionFromPool(mysql.createPool(url));
45
+ return createMysqlConnectionFromPool(createMysqlPool(url));
31
46
  }
32
47
  export {
33
48
  createMysqlConnection,
34
- createMysqlConnectionFromPool
49
+ createMysqlConnectionFromPool,
50
+ createMysqlPool
35
51
  };
@@ -275,8 +275,8 @@ function buildSelectList(table, select, params = []) {
275
275
  }
276
276
  return select.map((item) => {
277
277
  if (item.kind === "column") {
278
- const column2 = qualifyColumn(item.table, item.column);
279
- return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
278
+ const column = qualifyColumn(item.table, item.column);
279
+ return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
280
280
  }
281
281
  if (item.kind === "literalText") {
282
282
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
@@ -275,8 +275,8 @@ function buildSelectList(table, select, params = []) {
275
275
  }
276
276
  return select.map((item) => {
277
277
  if (item.kind === "column") {
278
- const column2 = qualifyColumn(item.table, item.column);
279
- return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
278
+ const column = qualifyColumn(item.table, item.column);
279
+ return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
280
280
  }
281
281
  if (item.kind === "literalText") {
282
282
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
@@ -925,50 +925,50 @@ class RepositoryQuery {
925
925
  }
926
926
  async hydrateEagerLoad(rows, result, load) {
927
927
  if (load.kind === "hasMany") {
928
- const relation2 = load.relation;
929
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
928
+ const relation = load.relation;
929
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation, load.options);
930
930
  for (const row of result) {
931
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
931
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
932
932
  }
933
933
  return;
934
934
  }
935
935
  if (load.kind === "morphMany") {
936
- const relation2 = load.relation;
937
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
936
+ const relation = load.relation;
937
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation, load.options);
938
938
  for (const row of result) {
939
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
939
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
940
940
  }
941
941
  return;
942
942
  }
943
943
  if (load.kind === "morphOne") {
944
- const relation2 = load.relation;
945
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
944
+ const relation = load.relation;
945
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation, load.options);
946
946
  for (const row of result) {
947
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]);
947
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]);
948
948
  }
949
949
  return;
950
950
  }
951
951
  if (load.kind === "hasManyThrough") {
952
- const relation2 = load.relation;
953
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
952
+ const relation = load.relation;
953
+ const grouped = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation, load.options);
954
954
  for (const row of result) {
955
- row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
955
+ row[load.as] = getByRelationKey(grouped, row[relation.localKey]) ?? [];
956
956
  }
957
957
  return;
958
958
  }
959
959
  if (load.kind === "belongsToMany") {
960
- const relation2 = load.relation;
961
- const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
960
+ const relation = load.relation;
961
+ const grouped = await this.repository.loadBelongsToManyForParents(rows, relation, load.repository, load.options);
962
962
  for (const row of result) {
963
- row[load.as] = getByRelationKey(grouped2, row[relation2.parentKey]) ?? [];
963
+ row[load.as] = getByRelationKey(grouped, row[relation.parentKey]) ?? [];
964
964
  }
965
965
  return;
966
966
  }
967
967
  if (load.kind === "morphTo") {
968
- const relation2 = load.relation;
969
- const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
968
+ const relation = load.relation;
969
+ const grouped = await this.repository.loadMorphToForChildren(rows, relation, load.morphRepositories ?? new Map, load.options);
970
970
  for (const row of result) {
971
- row[load.as] = getByRelationKey(grouped2, row[relation2.morphIdKey]);
971
+ row[load.as] = getByRelationKey(grouped, row[relation.morphIdKey]);
972
972
  }
973
973
  return;
974
974
  }
@@ -542,8 +542,8 @@ function buildSelectList(table, select, params = []) {
542
542
  }
543
543
  return select.map((item) => {
544
544
  if (item.kind === "column") {
545
- const column2 = qualifyColumn(item.table, item.column);
546
- return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
545
+ const column = qualifyColumn(item.table, item.column);
546
+ return item.as ? `${column} AS ${quoteIdentifier(item.as)}` : column;
547
547
  }
548
548
  if (item.kind === "literalText") {
549
549
  return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
@@ -14,6 +14,11 @@ function createSqliteConnection(filename) {
14
14
  }
15
15
  const db = new Database(filename, { create: true });
16
16
  db.exec("PRAGMA foreign_keys = ON");
17
+ db.exec("PRAGMA busy_timeout = 5000");
18
+ if (filename !== ":memory:") {
19
+ db.exec("PRAGMA journal_mode = WAL");
20
+ db.exec("PRAGMA synchronous = NORMAL");
21
+ }
17
22
  return {
18
23
  async unsafe(query, params = []) {
19
24
  const statement = db.query(query);
@@ -1,19 +1,37 @@
1
1
  // @bun
2
2
  // ../../src/core/http/clientIp.ts
3
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
3
4
  function trustForwardedFor(env = process.env) {
4
5
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
5
6
  }
6
- function readClientIp(request, env = process.env) {
7
- if (!trustForwardedFor(env)) {
8
- return;
7
+ function isPrivateAddress(address) {
8
+ const ip = address.replace(/^::ffff:/i, "");
9
+ 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);
10
+ }
11
+ function forwardedClientIp(request) {
12
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
13
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
14
+ const hop = hops[index];
15
+ if (hop && !isPrivateAddress(hop)) {
16
+ return hop;
17
+ }
9
18
  }
10
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
11
- if (forwarded) {
12
- return forwarded;
19
+ if (hops.length > 0) {
20
+ return hops[hops.length - 1];
13
21
  }
14
22
  return request.headers.get("x-real-ip")?.trim() || undefined;
15
23
  }
24
+ function readClientIp(request, env = process.env) {
25
+ if (trustForwardedFor(env)) {
26
+ const forwarded = forwardedClientIp(request);
27
+ if (forwarded) {
28
+ return forwarded;
29
+ }
30
+ }
31
+ return currentRequestMeta().ipAddress ?? undefined;
32
+ }
16
33
  export {
34
+ isPrivateAddress,
17
35
  readClientIp,
18
36
  trustForwardedFor
19
37
  };
@@ -1,8 +1,16 @@
1
1
  // @bun
2
+ // ../../src/core/runtime/appEnv.ts
3
+ function isProductionEnv(env = process.env) {
4
+ return env.APP_ENV === "production" || env.NODE_ENV === "production";
5
+ }
6
+
2
7
  // ../../src/core/http/corsMiddleware.ts
8
+ function defaultAllowedOrigins() {
9
+ return isProductionEnv() ? "" : "*";
10
+ }
3
11
  function resolveCorsConfig() {
4
12
  return {
5
- allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
13
+ allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? defaultAllowedOrigins()).split(",").map((origin) => origin.trim()).filter(Boolean),
6
14
  allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
7
15
  allowedHeaders: [
8
16
  "Authorization",
@@ -42,12 +50,15 @@ function buildCorsHeaders(request) {
42
50
  const origin = request.headers.get("origin");
43
51
  const corsConfig = resolveCorsConfig();
44
52
  const allowedOrigins = corsConfig.allowedOrigins;
45
- const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
53
+ headers.set("Vary", "Origin");
54
+ const allowOrigin = allowedOrigins.includes("*") ? "*" : origin && allowedOrigins.includes(origin) ? origin : null;
55
+ if (!allowOrigin) {
56
+ return headers;
57
+ }
46
58
  headers.set("Access-Control-Allow-Origin", allowOrigin);
47
59
  headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
48
60
  headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
49
61
  headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
50
- headers.set("Vary", "Origin");
51
62
  return headers;
52
63
  }
53
64
  export {
@@ -57,19 +57,36 @@ function sdkClientClassName() {
57
57
  }
58
58
 
59
59
  // ../../src/core/http/clientIp.ts
60
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
60
61
  function trustForwardedFor(env = process.env) {
61
62
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
62
63
  }
63
- function readClientIp(request, env = process.env) {
64
- if (!trustForwardedFor(env)) {
65
- return;
64
+ function isPrivateAddress(address) {
65
+ const ip = address.replace(/^::ffff:/i, "");
66
+ 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);
67
+ }
68
+ function forwardedClientIp(request) {
69
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
70
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
71
+ const hop = hops[index];
72
+ if (hop && !isPrivateAddress(hop)) {
73
+ return hop;
74
+ }
66
75
  }
67
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
68
- if (forwarded) {
69
- return forwarded;
76
+ if (hops.length > 0) {
77
+ return hops[hops.length - 1];
70
78
  }
71
79
  return request.headers.get("x-real-ip")?.trim() || undefined;
72
80
  }
81
+ function readClientIp(request, env = process.env) {
82
+ if (trustForwardedFor(env)) {
83
+ const forwarded = forwardedClientIp(request);
84
+ if (forwarded) {
85
+ return forwarded;
86
+ }
87
+ }
88
+ return currentRequestMeta().ipAddress ?? undefined;
89
+ }
73
90
 
74
91
  // ../../src/core/runtime/frontendMode.ts
75
92
  var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
@@ -111,7 +128,7 @@ function readSpaPrefix() {
111
128
  }
112
129
 
113
130
  // ../../src/core/view/webErrorView.ts
114
- import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
131
+ import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
115
132
 
116
133
  // ../../src/core/view/htmlResponse.ts
117
134
  function withCharset(contentType) {
@@ -212,7 +229,7 @@ async function renderWebErrorHtml(input) {
212
229
  try {
213
230
  return await render({
214
231
  ...input,
215
- request: input.request ?? currentRequestMeta().request
232
+ request: input.request ?? currentRequestMeta2().request
216
233
  });
217
234
  } catch {
218
235
  return renderKernelErrorChrome(input);
@@ -54,19 +54,36 @@ function sdkClientClassName() {
54
54
  }
55
55
 
56
56
  // ../../src/core/http/clientIp.ts
57
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
57
58
  function trustForwardedFor(env = process.env) {
58
59
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
59
60
  }
60
- function readClientIp(request, env = process.env) {
61
- if (!trustForwardedFor(env)) {
62
- return;
61
+ function isPrivateAddress(address) {
62
+ const ip = address.replace(/^::ffff:/i, "");
63
+ 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);
64
+ }
65
+ function forwardedClientIp(request) {
66
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
67
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
68
+ const hop = hops[index];
69
+ if (hop && !isPrivateAddress(hop)) {
70
+ return hop;
71
+ }
63
72
  }
64
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
65
- if (forwarded) {
66
- return forwarded;
73
+ if (hops.length > 0) {
74
+ return hops[hops.length - 1];
67
75
  }
68
76
  return request.headers.get("x-real-ip")?.trim() || undefined;
69
77
  }
78
+ function readClientIp(request, env = process.env) {
79
+ if (trustForwardedFor(env)) {
80
+ const forwarded = forwardedClientIp(request);
81
+ if (forwarded) {
82
+ return forwarded;
83
+ }
84
+ }
85
+ return currentRequestMeta().ipAddress ?? undefined;
86
+ }
70
87
 
71
88
  // ../../src/core/runtime/frontendMode.ts
72
89
  var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
@@ -108,7 +125,7 @@ function readSpaPrefix() {
108
125
  }
109
126
 
110
127
  // ../../src/core/view/webErrorView.ts
111
- import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
128
+ import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
112
129
 
113
130
  // ../../src/core/view/htmlResponse.ts
114
131
  function withCharset(contentType) {
@@ -209,7 +226,7 @@ async function renderWebErrorHtml(input) {
209
226
  try {
210
227
  return await render({
211
228
  ...input,
212
- request: input.request ?? currentRequestMeta().request
229
+ request: input.request ?? currentRequestMeta2().request
213
230
  });
214
231
  } catch {
215
232
  return renderKernelErrorChrome(input);
@@ -6,6 +6,7 @@ import { toHttpError as toHttpError3 } from "@getstrata/core/errors/http";
6
6
  import {
7
7
  BadRequestError,
8
8
  ConflictError,
9
+ InternalServerError,
9
10
  toHttpError,
10
11
  UnprocessableEntityError
11
12
  } from "@getstrata/core/errors/http";
@@ -24,14 +25,55 @@ function getPostgresSqlState(error) {
24
25
  }
25
26
  return;
26
27
  }
28
+ var MYSQL_ERRNO_MESSAGES = {
29
+ 1062: () => new ConflictError("A record with these values already exists."),
30
+ 1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
31
+ 1452: () => new UnprocessableEntityError("Referenced record does not exist."),
32
+ 1048: () => new BadRequestError("Required field is missing."),
33
+ 3819: () => new BadRequestError("Value violates a database constraint.")
34
+ };
35
+ function mapSqliteError(error) {
36
+ const code = typeof error.code === "string" ? error.code : "";
37
+ if (!code.startsWith("SQLITE_")) {
38
+ return null;
39
+ }
40
+ if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
41
+ return new ConflictError("A record with these values already exists.");
42
+ }
43
+ if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
44
+ return new UnprocessableEntityError("Referenced record does not exist.");
45
+ }
46
+ if (code === "SQLITE_CONSTRAINT_NOTNULL") {
47
+ return new BadRequestError("Required field is missing.");
48
+ }
49
+ if (code.startsWith("SQLITE_CONSTRAINT")) {
50
+ return new BadRequestError("Value violates a database constraint.");
51
+ }
52
+ return new InternalServerError("Database operation failed.");
53
+ }
54
+ function mapMysqlError(error) {
55
+ const code = typeof error.code === "string" ? error.code : "";
56
+ if (!code.startsWith("ER_")) {
57
+ return null;
58
+ }
59
+ const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
60
+ return factory ? factory() : new InternalServerError("Database operation failed.");
61
+ }
27
62
  function mapDatabaseError(error) {
28
63
  const httpError = toHttpError(error);
29
64
  if (httpError) {
30
65
  return httpError;
31
66
  }
32
67
  if (!isPostgresError(error)) {
33
- const message = error instanceof Error ? error.message : "Database operation failed.";
34
- return new BadRequestError(message);
68
+ return new InternalServerError;
69
+ }
70
+ const sqlite = mapSqliteError(error);
71
+ if (sqlite) {
72
+ return sqlite;
73
+ }
74
+ const mysql = mapMysqlError(error);
75
+ if (mysql) {
76
+ return mysql;
35
77
  }
36
78
  const sqlState = getPostgresSqlState(error);
37
79
  switch (sqlState) {
@@ -52,10 +94,7 @@ function mapDatabaseError(error) {
52
94
  constraint: error.constraint
53
95
  });
54
96
  default:
55
- return new BadRequestError(error.message ?? "Database operation failed.", {
56
- code: error.code,
57
- sqlState
58
- });
97
+ return new InternalServerError("Database operation failed.");
59
98
  }
60
99
  }
61
100
  async function withDatabaseErrorHandling(operation) {
@@ -68,6 +107,12 @@ async function withDatabaseErrorHandling(operation) {
68
107
 
69
108
  // ../../src/core/http/webErrorResponse.ts
70
109
  import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
110
+ import { appLogger } from "@getstrata/core/logging/logger";
111
+
112
+ // ../../src/core/runtime/appEnv.ts
113
+ function isProductionEnv(env = process.env) {
114
+ return env.APP_ENV === "production" || env.NODE_ENV === "production";
115
+ }
71
116
 
72
117
  // ../../src/core/runtime/frontendMode.ts
73
118
  var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
@@ -319,14 +364,27 @@ function errorPageTitle(status, message) {
319
364
  return message;
320
365
  }
321
366
  function publicErrorMessage(status, message) {
322
- if (status >= 500 && false) {}
367
+ if (status >= 500 && isProductionEnv()) {
368
+ return "Something went wrong.";
369
+ }
323
370
  return message;
324
371
  }
372
+ function logServerError(error, mappedError) {
373
+ if (mappedError.status < 500) {
374
+ return;
375
+ }
376
+ appLogger.error("Unhandled request error", {
377
+ status: mappedError.status,
378
+ error: error instanceof Error ? error.message : String(error),
379
+ stack: error instanceof Error ? error.stack : undefined
380
+ });
381
+ }
325
382
  async function webErrorResponse(error, request) {
326
383
  if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
327
384
  return null;
328
385
  }
329
386
  const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
387
+ logServerError(error, mappedError);
330
388
  if (mappedError.status === 401) {
331
389
  return Response.redirect(loginRedirectLocation(request), 302);
332
390
  }
@@ -355,6 +413,7 @@ function noContentResponse() {
355
413
  }
356
414
  function errorResponse(error) {
357
415
  const mappedError = toHttpError3(error) ?? mapDatabaseError(error);
416
+ logServerError(error, mappedError);
358
417
  return Response.json({
359
418
  error: mappedError.message,
360
419
  ...mappedError.details === undefined ? {} : { details: mappedError.details }
@@ -57,19 +57,36 @@ function sdkClientClassName() {
57
57
  }
58
58
 
59
59
  // ../../src/core/http/clientIp.ts
60
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
60
61
  function trustForwardedFor(env = process.env) {
61
62
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
62
63
  }
63
- function readClientIp(request, env = process.env) {
64
- if (!trustForwardedFor(env)) {
65
- return;
64
+ function isPrivateAddress(address) {
65
+ const ip = address.replace(/^::ffff:/i, "");
66
+ 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);
67
+ }
68
+ function forwardedClientIp(request) {
69
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
70
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
71
+ const hop = hops[index];
72
+ if (hop && !isPrivateAddress(hop)) {
73
+ return hop;
74
+ }
66
75
  }
67
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
68
- if (forwarded) {
69
- return forwarded;
76
+ if (hops.length > 0) {
77
+ return hops[hops.length - 1];
70
78
  }
71
79
  return request.headers.get("x-real-ip")?.trim() || undefined;
72
80
  }
81
+ function readClientIp(request, env = process.env) {
82
+ if (trustForwardedFor(env)) {
83
+ const forwarded = forwardedClientIp(request);
84
+ if (forwarded) {
85
+ return forwarded;
86
+ }
87
+ }
88
+ return currentRequestMeta().ipAddress ?? undefined;
89
+ }
73
90
 
74
91
  // ../../src/core/http/scimThrottleMiddleware.ts
75
92
  var memoryBuckets = new Map;
@@ -59,19 +59,36 @@ function sdkClientClassName() {
59
59
  }
60
60
 
61
61
  // ../../src/core/http/clientIp.ts
62
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
62
63
  function trustForwardedFor(env = process.env) {
63
64
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
64
65
  }
65
- function readClientIp(request, env = process.env) {
66
- if (!trustForwardedFor(env)) {
67
- return;
66
+ function isPrivateAddress(address) {
67
+ const ip = address.replace(/^::ffff:/i, "");
68
+ 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);
69
+ }
70
+ function forwardedClientIp(request) {
71
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
72
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
73
+ const hop = hops[index];
74
+ if (hop && !isPrivateAddress(hop)) {
75
+ return hop;
76
+ }
68
77
  }
69
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
70
- if (forwarded) {
71
- return forwarded;
78
+ if (hops.length > 0) {
79
+ return hops[hops.length - 1];
72
80
  }
73
81
  return request.headers.get("x-real-ip")?.trim() || undefined;
74
82
  }
83
+ function readClientIp(request, env = process.env) {
84
+ if (trustForwardedFor(env)) {
85
+ const forwarded = forwardedClientIp(request);
86
+ if (forwarded) {
87
+ return forwarded;
88
+ }
89
+ }
90
+ return currentRequestMeta().ipAddress ?? undefined;
91
+ }
75
92
 
76
93
  // ../../src/core/runtime/frontendMode.ts
77
94
  var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
@@ -113,7 +130,7 @@ function readSpaPrefix() {
113
130
  }
114
131
 
115
132
  // ../../src/core/view/webErrorView.ts
116
- import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
133
+ import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
117
134
 
118
135
  // ../../src/core/view/htmlResponse.ts
119
136
  function withCharset(contentType) {
@@ -214,7 +231,7 @@ async function renderWebErrorHtml(input) {
214
231
  try {
215
232
  return await render({
216
233
  ...input,
217
- request: input.request ?? currentRequestMeta().request
234
+ request: input.request ?? currentRequestMeta2().request
218
235
  });
219
236
  } catch {
220
237
  return renderKernelErrorChrome(input);