@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
@@ -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)",
@@ -254,13 +254,13 @@ function renderOpenApiDocument(spec) {
254
254
  return `${JSON.stringify(spec, null, 2)}
255
255
  `;
256
256
  }
257
- function toMethodName(method, path, apiPrefix2) {
258
- const relativePath = path.startsWith(apiPrefix2) ? path.slice(apiPrefix2.length) || "/" : path;
257
+ function toMethodName(method, path, apiPrefix) {
258
+ const relativePath = path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) || "/" : path;
259
259
  const segments = relativePath.replace(/\{|\}/g, "").split("/").filter(Boolean).flatMap((segment) => segment.split("-")).map((segment) => segment.replace(/[^a-zA-Z0-9]/g, "")).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1));
260
260
  return `${method.toLowerCase()}${segments.join("")}`;
261
261
  }
262
- function toRequestPath(path, apiPrefix2) {
263
- return path.startsWith(apiPrefix2) ? path.slice(apiPrefix2.length) || "/" : path;
262
+ function toRequestPath(path, apiPrefix) {
263
+ return path.startsWith(apiPrefix) ? path.slice(apiPrefix.length) || "/" : path;
264
264
  }
265
265
  function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
266
266
  const lines = [
@@ -0,0 +1,8 @@
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
+ export {
7
+ isProductionEnv
8
+ };
@@ -3,14 +3,16 @@
3
3
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
4
4
 
5
5
  // ../../src/core/tenant/tenancyConfig.ts
6
+ var TENANCY_DRIVERS = ["rls", "column", "none"];
6
7
  function readTenancyDriver(env = process.env) {
7
- if (env.TENANCY_DRIVER === "none") {
8
- return "none";
8
+ const raw = env.TENANCY_DRIVER?.trim();
9
+ if (raw === undefined || raw === "") {
10
+ return "rls";
9
11
  }
10
- if (env.TENANCY_DRIVER === "column") {
11
- return "column";
12
+ if (TENANCY_DRIVERS.includes(raw)) {
13
+ return raw;
12
14
  }
13
- return "rls";
15
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
14
16
  }
15
17
  function isTenancyEnabled(env = process.env) {
16
18
  return readTenancyDriver(env) !== "none";
@@ -1,13 +1,15 @@
1
1
  // @bun
2
2
  // ../../src/core/tenant/tenancyConfig.ts
3
+ var TENANCY_DRIVERS = ["rls", "column", "none"];
3
4
  function readTenancyDriver(env = process.env) {
4
- if (env.TENANCY_DRIVER === "none") {
5
- return "none";
5
+ const raw = env.TENANCY_DRIVER?.trim();
6
+ if (raw === undefined || raw === "") {
7
+ return "rls";
6
8
  }
7
- if (env.TENANCY_DRIVER === "column") {
8
- return "column";
9
+ if (TENANCY_DRIVERS.includes(raw)) {
10
+ return raw;
9
11
  }
10
- return "rls";
12
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
11
13
  }
12
14
  function isTenancyEnabled(env = process.env) {
13
15
  return readTenancyDriver(env) !== "none";
@@ -29,14 +29,16 @@ function hasActiveDatabaseConnection() {
29
29
  }
30
30
 
31
31
  // ../../src/core/tenant/tenancyConfig.ts
32
+ var TENANCY_DRIVERS = ["rls", "column", "none"];
32
33
  function readTenancyDriver(env = process.env) {
33
- if (env.TENANCY_DRIVER === "none") {
34
- return "none";
34
+ const raw = env.TENANCY_DRIVER?.trim();
35
+ if (raw === undefined || raw === "") {
36
+ return "rls";
35
37
  }
36
- if (env.TENANCY_DRIVER === "column") {
37
- return "column";
38
+ if (TENANCY_DRIVERS.includes(raw)) {
39
+ return raw;
38
40
  }
39
- return "rls";
41
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
40
42
  }
41
43
  function isTenancyEnabled(env = process.env) {
42
44
  return readTenancyDriver(env) !== "none";
@@ -64,8 +66,8 @@ async function runWithTenantDatabase(tenant, callback) {
64
66
  return await runWithTenant(tenant, callback);
65
67
  }
66
68
  if (hasActiveDatabaseConnection()) {
67
- const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
68
- await applyTenantContextToTransaction(activeConnection2, tenant.id);
69
+ const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
70
+ await applyTenantContextToTransaction(activeConnection, tenant.id);
69
71
  return await runWithTenant(tenant, callback);
70
72
  }
71
73
  return await getDefaultDatabasePool().begin(async (transaction) => {
@@ -35,7 +35,7 @@ export { createDatabaseConnection } from "../core/database/connection.ts";
35
35
  export { getActiveDatabaseConnection, hasActiveDatabaseConnection, runWithDatabaseConnection, } from "../core/database/connectionContext.ts";
36
36
  export { getDefaultDatabasePool, getDefaultDatabaseQuery, registerDefaultDatabasePool, } from "../core/database/defaultConnection.ts";
37
37
  export type { SqlDialect } from "../core/database/dialect.ts";
38
- export { currentSqlDialect, dialectFor, resetSqlDialect, runWithSqlDialect, useSqlDialect, } from "../core/database/dialect.ts";
38
+ export { currentSqlDialect, dialectFor, resetSqlDialect, runWithSqlDialect, sqlTimestamp, useSqlDialect, } from "../core/database/dialect.ts";
39
39
  export { Factory } from "../core/database/factory.ts";
40
40
  export { foreignKeyFromTable, pivotTableName, singularize } from "../core/database/inflection.ts";
41
41
  export { withMigrationLock } from "../core/database/migrations/advisoryLock.ts";
@@ -43,7 +43,7 @@ export { freshDatabase, getMigrationStatus, loadMigrationsFromDirectory, migrate
43
43
  export type { Migration, MigrationDatabase, MigrationStatus, } from "../core/database/migrations/types.ts";
44
44
  export type { CastType, GlobalScopeFn, ModelConstructor } from "../core/database/model.ts";
45
45
  export { applyCasts, BelongsToManyRelationQuery, BelongsToRelationQuery, dehydrateValue, filterMassAssignable, HasManyRelationQuery, HasOneRelationQuery, hydrateValue, Model, ModelQuery, MorphManyRelationQuery, MorphOneRelationQuery, MorphToRelationQuery, registerModelClass, registerModelRepository, } from "../core/database/model.ts";
46
- export { createMysqlConnection, createMysqlConnectionFromPool, } from "../core/database/mysqlConnection.ts";
46
+ export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool, } from "../core/database/mysqlConnection.ts";
47
47
  export { getNamedConnection, hasNamedConnection, registerNamedConnection, resetNamedConnections, runOnNamedConnection, unregisterNamedConnection, } from "../core/database/namedConnections.ts";
48
48
  export { createDatabaseQueryProxy } from "../core/database/queryProxy.ts";
49
49
  export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, MorphManyRelation, MorphOneRelation, MorphToRelation, } from "../core/database/relationships.ts";
@@ -60,7 +60,7 @@ export { runInTransaction } from "../core/database/transaction.ts";
60
60
  export type { QueryJoin, QueryJoinOn, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, } from "../core/database/types.ts";
61
61
  export type { WhereNode } from "../core/database/whereBuilder.ts";
62
62
  export { WhereBuilder } from "../core/database/whereBuilder.ts";
63
- export { BadRequestError, ConflictError, ForbiddenError, HttpError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../core/errors/http.ts";
63
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../core/errors/http.ts";
64
64
  export type { EventListener } from "../core/events/eventBus.ts";
65
65
  export { EventBus, eventBus } from "../core/events/eventBus.ts";
66
66
  export { modelEventName } from "../core/events/index.ts";