@getstrata/core 1.0.0 → 1.0.2

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 (35) 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 +7 -5
  14. package/dist/entries/contracts/authUserDirectory.js +1 -0
  15. package/dist/entries/database/errors.js +45 -6
  16. package/dist/entries/database/mysqlConnection.js +18 -2
  17. package/dist/entries/database/sqliteConnection.js +5 -0
  18. package/dist/entries/http/clientIp.js +24 -6
  19. package/dist/entries/http/corsMiddleware.js +14 -3
  20. package/dist/entries/http/loginThrottleMiddleware.js +25 -8
  21. package/dist/entries/http/memoryThrottleMiddleware.js +25 -8
  22. package/dist/entries/http/response.js +66 -7
  23. package/dist/entries/http/scimThrottleMiddleware.js +23 -6
  24. package/dist/entries/http/throttleMiddleware.js +25 -8
  25. package/dist/entries/http/webErrorResponse.js +66 -7
  26. package/dist/entries/jobs/exportAuditLogsJob.js +7 -5
  27. package/dist/entries/logging/requestLoggingMiddleware.js +29 -10
  28. package/dist/entries/openapi/generator.js +1 -1
  29. package/dist/entries/runtime/appEnv.js +8 -0
  30. package/dist/entries/tenant/databaseTenantContext.js +7 -5
  31. package/dist/entries/tenant/tenancyConfig.js +7 -5
  32. package/dist/entries/tenant/tenantDatabaseScope.js +7 -5
  33. package/dist/framework/public-api.d.ts +3 -3
  34. package/dist/index.js +161 -39
  35. package/package.json +16 -3
@@ -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);
@@ -1,11 +1,13 @@
1
1
  // @bun
2
2
  // ../../src/core/http/webErrorResponse.ts
3
3
  import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
4
+ import { appLogger } from "@getstrata/core/logging/logger";
4
5
 
5
6
  // ../../src/core/database/errors.ts
6
7
  import {
7
8
  BadRequestError,
8
9
  ConflictError,
10
+ InternalServerError,
9
11
  toHttpError,
10
12
  UnprocessableEntityError
11
13
  } from "@getstrata/core/errors/http";
@@ -24,14 +26,55 @@ function getPostgresSqlState(error) {
24
26
  }
25
27
  return;
26
28
  }
29
+ var MYSQL_ERRNO_MESSAGES = {
30
+ 1062: () => new ConflictError("A record with these values already exists."),
31
+ 1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
32
+ 1452: () => new UnprocessableEntityError("Referenced record does not exist."),
33
+ 1048: () => new BadRequestError("Required field is missing."),
34
+ 3819: () => new BadRequestError("Value violates a database constraint.")
35
+ };
36
+ function mapSqliteError(error) {
37
+ const code = typeof error.code === "string" ? error.code : "";
38
+ if (!code.startsWith("SQLITE_")) {
39
+ return null;
40
+ }
41
+ if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
42
+ return new ConflictError("A record with these values already exists.");
43
+ }
44
+ if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
45
+ return new UnprocessableEntityError("Referenced record does not exist.");
46
+ }
47
+ if (code === "SQLITE_CONSTRAINT_NOTNULL") {
48
+ return new BadRequestError("Required field is missing.");
49
+ }
50
+ if (code.startsWith("SQLITE_CONSTRAINT")) {
51
+ return new BadRequestError("Value violates a database constraint.");
52
+ }
53
+ return new InternalServerError("Database operation failed.");
54
+ }
55
+ function mapMysqlError(error) {
56
+ const code = typeof error.code === "string" ? error.code : "";
57
+ if (!code.startsWith("ER_")) {
58
+ return null;
59
+ }
60
+ const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
61
+ return factory ? factory() : new InternalServerError("Database operation failed.");
62
+ }
27
63
  function mapDatabaseError(error) {
28
64
  const httpError = toHttpError(error);
29
65
  if (httpError) {
30
66
  return httpError;
31
67
  }
32
68
  if (!isPostgresError(error)) {
33
- const message = error instanceof Error ? error.message : "Database operation failed.";
34
- return new BadRequestError(message);
69
+ return new InternalServerError;
70
+ }
71
+ const sqlite = mapSqliteError(error);
72
+ if (sqlite) {
73
+ return sqlite;
74
+ }
75
+ const mysql = mapMysqlError(error);
76
+ if (mysql) {
77
+ return mysql;
35
78
  }
36
79
  const sqlState = getPostgresSqlState(error);
37
80
  switch (sqlState) {
@@ -52,10 +95,7 @@ function mapDatabaseError(error) {
52
95
  constraint: error.constraint
53
96
  });
54
97
  default:
55
- return new BadRequestError(error.message ?? "Database operation failed.", {
56
- code: error.code,
57
- sqlState
58
- });
98
+ return new InternalServerError("Database operation failed.");
59
99
  }
60
100
  }
61
101
  async function withDatabaseErrorHandling(operation) {
@@ -66,6 +106,11 @@ async function withDatabaseErrorHandling(operation) {
66
106
  }
67
107
  }
68
108
 
109
+ // ../../src/core/runtime/appEnv.ts
110
+ function isProductionEnv(env = process.env) {
111
+ return env.APP_ENV === "production" || env.NODE_ENV === "production";
112
+ }
113
+
69
114
  // ../../src/core/runtime/frontendMode.ts
70
115
  var FRONTEND_MODES = ["api", "server-htmx", "spa-react", "hybrid"];
71
116
  var DEFAULT_SPA_PREFIX = "/app";
@@ -316,14 +361,27 @@ function errorPageTitle(status, message) {
316
361
  return message;
317
362
  }
318
363
  function publicErrorMessage(status, message) {
319
- if (status >= 500 && false) {}
364
+ if (status >= 500 && isProductionEnv()) {
365
+ return "Something went wrong.";
366
+ }
320
367
  return message;
321
368
  }
369
+ function logServerError(error, mappedError) {
370
+ if (mappedError.status < 500) {
371
+ return;
372
+ }
373
+ appLogger.error("Unhandled request error", {
374
+ status: mappedError.status,
375
+ error: error instanceof Error ? error.message : String(error),
376
+ stack: error instanceof Error ? error.stack : undefined
377
+ });
378
+ }
322
379
  async function webErrorResponse(error, request) {
323
380
  if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
324
381
  return null;
325
382
  }
326
383
  const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
384
+ logServerError(error, mappedError);
327
385
  if (mappedError.status === 401) {
328
386
  return Response.redirect(loginRedirectLocation(request), 302);
329
387
  }
@@ -337,6 +395,7 @@ async function webErrorResponse(error, request) {
337
395
  });
338
396
  }
339
397
  export {
398
+ logServerError,
340
399
  normalizeFieldErrors,
341
400
  webErrorResponse
342
401
  };
@@ -191,14 +191,16 @@ async function safeFetch(input, init = {}, options = {}) {
191
191
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
192
192
 
193
193
  // ../../src/core/tenant/tenancyConfig.ts
194
+ var TENANCY_DRIVERS = ["rls", "column", "none"];
194
195
  function readTenancyDriver(env = process.env) {
195
- if (env.TENANCY_DRIVER === "none") {
196
- return "none";
196
+ const raw = env.TENANCY_DRIVER?.trim();
197
+ if (raw === undefined || raw === "") {
198
+ return "rls";
197
199
  }
198
- if (env.TENANCY_DRIVER === "column") {
199
- return "column";
200
+ if (TENANCY_DRIVERS.includes(raw)) {
201
+ return raw;
200
202
  }
201
- return "rls";
203
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
202
204
  }
203
205
  function isTenancyEnabled(env = process.env) {
204
206
  return readTenancyDriver(env) !== "none";
@@ -1,21 +1,38 @@
1
1
  // @bun
2
2
  // ../../src/core/logging/requestLoggingMiddleware.ts
3
- import { currentRequestMeta, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
3
+ import { currentRequestMeta as currentRequestMeta2, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
4
4
 
5
5
  // ../../src/core/http/clientIp.ts
6
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
6
7
  function trustForwardedFor(env = process.env) {
7
8
  return (env.TRUST_FORWARDED_FOR ?? "false") === "true";
8
9
  }
9
- function readClientIp(request, env = process.env) {
10
- if (!trustForwardedFor(env)) {
11
- return;
10
+ function isPrivateAddress(address) {
11
+ const ip = address.replace(/^::ffff:/i, "");
12
+ 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);
13
+ }
14
+ function forwardedClientIp(request) {
15
+ const hops = (request.headers.get("x-forwarded-for") ?? "").split(",").map((hop) => hop.trim()).filter(Boolean);
16
+ for (let index = hops.length - 1;index >= 0; index -= 1) {
17
+ const hop = hops[index];
18
+ if (hop && !isPrivateAddress(hop)) {
19
+ return hop;
20
+ }
12
21
  }
13
- const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
14
- if (forwarded) {
15
- return forwarded;
22
+ if (hops.length > 0) {
23
+ return hops[hops.length - 1];
16
24
  }
17
25
  return request.headers.get("x-real-ip")?.trim() || undefined;
18
26
  }
27
+ function readClientIp(request, env = process.env) {
28
+ if (trustForwardedFor(env)) {
29
+ const forwarded = forwardedClientIp(request);
30
+ if (forwarded) {
31
+ return forwarded;
32
+ }
33
+ }
34
+ return currentRequestMeta().ipAddress ?? undefined;
35
+ }
19
36
 
20
37
  // ../../src/core/logging/logger.ts
21
38
  class Logger {
@@ -56,9 +73,10 @@ var appLogger = new Logger("app");
56
73
  // ../../src/core/logging/requestLoggingMiddleware.ts
57
74
  function createRequestLoggingMiddleware() {
58
75
  return async (request, next) => {
76
+ const ipAddress = readClientIp(request) ?? null;
59
77
  return await runWithRequestMeta({
60
- ...currentRequestMeta(),
61
- ipAddress: readClientIp(request) ?? null,
78
+ ...currentRequestMeta2(),
79
+ ipAddress,
62
80
  userAgent: request.headers.get("user-agent"),
63
81
  request
64
82
  }, async () => {
@@ -71,7 +89,8 @@ function createRequestLoggingMiddleware() {
71
89
  method: request.method,
72
90
  path: new URL(request.url).pathname,
73
91
  status: response.status,
74
- durationMs
92
+ durationMs,
93
+ ipAddress
75
94
  });
76
95
  return response;
77
96
  });
@@ -103,7 +103,7 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
103
103
  "GET /health": "Liveness probe",
104
104
  "GET /ready": "Readiness probe",
105
105
  "GET /metrics": "Prometheus metrics",
106
- "GET /api/user": "Current authenticated HiroApp user",
106
+ "GET /api/user": "Current authenticated user",
107
107
  "POST /api/login": "Login with email and password",
108
108
  "POST /api/auth/token": "Mint a short-lived JWT with email and password",
109
109
  "POST /api/apply/login": "Candidate portal login (opaque token)",