@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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Publish the `contracts/authUserDirectory` subpath. Generated cookie and token apps import `AuthUserDirectory` from it, and without the export their `tsc --noEmit` failed with TS2307.
6
+ - `SqlDialect` gains `timestampValue()`, plus a `sqlTimestamp()` helper. MySQL `DATETIME` rejects the ISO-8601 `T` separator and trailing `Z`, which broke cookie session inserts on MySQL.
7
+ - `DatabaseConnection.close?()` now returns `void | Promise<void>`, matching the synchronous `close()` on the SQLite connection it is supposed to describe.
8
+ - `engines.bun` is declared.
9
+ - MySQL pools are UTC end to end: `createMysqlConnection()` passes `timezone: "Z"` to mysql2 and runs `SET time_zone = '+00:00'` on every new connection. Before, `DATETIME` values written as UTC by `timestampValue()` were read back shifted by the host offset, and `expires_at > NOW()` depended on the server time zone. `createMysqlPool(url)` exposes the configured raw pool.
10
+ - SQLite `nowExpression()` is `strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` instead of `CURRENT_TIMESTAMP`, matching the ISO-8601 text that `timestampValue()` writes. With `CURRENT_TIMESTAMP` the `T` separator sorted after the space, so an expired session compared as valid until the next UTC day.
11
+ - Unknown exceptions map to `InternalServerError` (500, generic message) instead of a 400 that echoed the raw error text. SQLite (`SQLITE_CONSTRAINT_*`) and MySQL (`ER_DUP_ENTRY`, `ER_NO_REFERENCED_ROW_2`, `ER_ROW_IS_REFERENCED_2`, `ER_BAD_NULL_ERROR`, `ER_CHECK_CONSTRAINT_VIOLATED`) constraint errors map to 409/422/400 like Postgres SQLSTATEs. 5xx responses are logged with the original message and stack.
12
+ - Client IP: `readClientIp()` falls back to the socket address the web server records in the request context, and with `TRUST_FORWARDED_FOR=true` takes the rightmost public `X-Forwarded-For` hop instead of the first (client-controlled) one. Throttles no longer collapse every client into an `unknown` bucket.
13
+ - CORS: with `CORS_ALLOWED_ORIGINS` unset, production sends no `Access-Control-Allow-Origin` (same-origin only) instead of reflecting any origin; outside production the default stays `*`. Listed origins are reflected; unlisted ones get no header.
14
+ - `TENANCY_DRIVER` must be `none`, `column`, or `rls`. Unknown values throw instead of enabling rls.
15
+ - SQLite connections enable WAL, `busy_timeout = 5000`, and `synchronous = NORMAL` for file databases.
16
+ - New subpath `runtime/appEnv` with `isProductionEnv()` (`APP_ENV` or `NODE_ENV`), used for the cookie `Secure` flag and for hiding 500 messages in HTML.
17
+
18
+ ## 1.0.0
19
+
20
+ - First stable release of the public `@getstrata/core` API.
21
+
3
22
  ## 0.7.5
4
23
 
5
24
  - `TENANCY_DRIVER=column`: tenant ALS without Postgres `SET LOCAL` / `set_config`. `isRlsTenancy()` is the RLS-only check.
@@ -15,7 +34,7 @@
15
34
 
16
35
  ## 0.7.2
17
36
 
18
- - `withJsonErrorHandling` always maps thrown errors to JSON. `requestPrefersJson` treats `/api/` as JSON even when `Accept` is HTML, so hybrid staff HTML cannot remap JSON API errors.
37
+ - `withJsonErrorHandling` always maps thrown errors to JSON. `requestPrefersJson` treats `/api/` as JSON even when `Accept` is HTML, so hybrid HTML cannot remap JSON API errors.
19
38
 
20
39
  ## 0.7.1
21
40
 
@@ -24,20 +43,20 @@
24
43
 
25
44
  ## 0.7.0
26
45
 
27
- - `FRONTEND_MODE=hybrid` turns on staff HTML and the candidate SPA together. Views are on for `server-htmx` and `hybrid`. The SPA is on for `spa-react` and `hybrid`.
46
+ - `FRONTEND_MODE=hybrid` turns on HTML at `/` and a SPA under `SPA_PREFIX` together. Views are on for `server-htmx` and `hybrid`. The SPA is on for `spa-react` and `hybrid`.
28
47
  - `parseFrontendMode`, `FRONTEND_MODES`, and `FRONTEND_MODE_PATTERN` are the allowed-value list. Apps should reuse that pattern in env schemas.
29
48
  - Named database connections: `registerNamedConnection`, `runOnNamedConnection`, SQLite (`bun:sqlite`), and MySQL (`mysql2`). `runWithSqlDialect` keeps the dialect across `await` via AsyncLocalStorage.
30
49
  - New subpaths: `database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`, `database/connectionContext`.
31
- - OpenAPI treats `POST /api/apply/login` as unauthenticated.
50
+ - OpenAPI treats unauthenticated login routes as public.
32
51
 
33
52
  ## 0.6.0
34
53
 
35
54
  - **Breaking:** cookie session signatures use HMAC-SHA256. Existing HMAC cookies signed with the previous digest will not verify. Rotate `SESSION_SECRET` or sign users in again.
36
- - **Breaking:** default `MEMBER_ABILITIES` are profile and token scopes (`profile:read`, `auth:tokens:*`), not org/project/task. Apps replace the catalog with `configureAbilityCatalog`. HiroApp maps admin / recruiter / candidate.
55
+ - **Breaking:** default `MEMBER_ABILITIES` are profile and token scopes (`profile:read`, `auth:tokens:*`), not org/project/task. Apps replace the catalog with `configureAbilityCatalog`. Generated HiroApp maps admin / member.
37
56
  - Named auth guards: opaque Bearer tokens, JWT HS256, HTTP Basic, and cookie sessions. `AuthManager` picks a guard from the `Authorization` scheme. CSRF is skipped for Bearer and Basic.
38
- - OpenAPI treats HiroApp login, JWT mint (`POST /api/auth/token`), and public careers as unauthenticated. Partner ping and audit export require credentials.
57
+ - OpenAPI treats HiroApp login and JWT mint (`POST /api/auth/token`) as unauthenticated. Partner ping and audit export require credentials.
39
58
  - SQL dialect helpers (`pgsql`, `mysql`, `sqlite`) for placeholders, quoting, `ILIKE`/`LIKE`, `RETURNING`, and `NULLS LAST`. Full-text `tsMatch` stays Postgres-only and throws on other engines.
40
- - HiroApp is the only in-repo example product. The leftover `src/db` schema is a test fixture, not a second app.
59
+ - HiroApp is the in-repo generated example. The leftover `src/db` schema is a test fixture, not a second app.
41
60
  - New subpaths: `auth/jwt`, `auth/jwtGuard`, `auth/basicAuthGuard`, `auth/tokenAbilityChecker`, `database/dialect`, `http/statelessAuth`.
42
61
 
43
62
  ## 0.5.101
@@ -56,12 +75,12 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names. `hashe
56
75
 
57
76
  ## 0.5.99
58
77
 
59
- HiroApp dogfood of 0.5.98 found lookalike APIs. This release matches the previous PHP framework call shape and SQL, not just export names.
78
+ HiroApp dogfood of 0.5.98 found lookalike APIs. This release matches the existing call shape and SQL, not just export names.
60
79
 
61
80
  - Relation queries are thenable: `await user.applications()` delegates to `get()`.
62
81
  - `Model.with()` / `where()` / `whereHas()` return a `ModelQuery` that hydrates models and chains into `where` / `find` / `findOrFail` / `first` / `get`. `first()` / `find()` use `LIMIT` / PK lookup.
63
82
  - `belongsTo.where()` threads constraints into `whereHas` EXISTS (HiroApp application search).
64
- - `{ ilike }` uses the value as-is (the previous PHP framework). Pass `%term%` yourself; the operator no longer wraps extra `%`.
83
+ - `{ ilike }` uses the value as-is. Pass `%term%` yourself; the operator no longer wraps extra `%`.
65
84
  - Morph type defaults to `$morphClass` / class name, not `table.name`. Override with `$morphClass = "App\\Models\\User"` or the last `morphMany` argument. `morphTo()` no longer silently defaults to `imageable_*`.
66
85
  - `primaryKey()` defaults to the table PK (`id`).
67
86
  - Nested `load("a.b")` / `with("a.b")` skip already-loaded heads and batch the next level.
@@ -76,13 +95,13 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
76
95
 
77
96
  ## 0.5.98
78
97
 
79
- - Eloquent-shaped relations: `this.hasMany(Related)` returns a relation query (`get`/`where`/`create`/`attach`). `load()` / `loaded()` and `Model.with()` replace PHP `__get`. `whereHas`/`has`/`doesntHave`, morph* methods, and nested `with("a.b")` are supported.
98
+ - Model relations: `this.hasMany(Related)` returns a relation query (`get`/`where`/`create`/`attach`). `load()` / `loaded()` and `Model.with()` load related rows. `whereHas`/`has`/`doesntHave`, morph* methods, and nested `with("a.b")` are supported.
80
99
  - Model `$hidden`/`$visible`/`$appends`, `toArray()`/`toJSON()`, `makeHidden`/`makeVisible`/`append`, and `observe()`.
81
100
  - `Model.where()`, `firstOrNew()`, `firstOrCreate()`, and `updateOrCreate()`.
82
101
  - Factory `count`/`state`/`sequence`/`for`/`has`/`recycle` plus `afterMaking`/`afterCreating`.
83
102
  - `JsonResource` (`wrap`, `whenLoaded`, `additional`, `collection`).
84
- - the previous PHP framework aliases: container `make`/`instance`, EventBus `on`/`emit`, query `whereNull`/`whereIn`/`whereExists`.
85
- - Parity audit reports design score separately. Horizon/Nova/CLI stay Bun-native stand-ins.
103
+ - Convenience aliases: container `make`/`instance`, EventBus `on`/`emit`, query `whereNull`/`whereIn`/`whereExists`.
104
+ - Parity audit reports design score separately. Queue dashboards, admin UIs, and CLI stay Bun-native.
86
105
 
87
106
  ## 0.5.97
88
107
 
@@ -99,7 +118,7 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
99
118
 
100
119
  ## 0.5.90
101
120
 
102
- - **Breaking identity defaults:** `appKeyPrefix()` is `strata` and `appDisplayName()` is `Strata` when `APP_KEY_PREFIX` / `APP_NAME` are unset (were `strata` / `the previous in-repo app`). the previous in-repo app pins those env vars.
121
+ - **Breaking identity defaults:** `appKeyPrefix()` is `strata` and `appDisplayName()` is `Strata` when `APP_KEY_PREFIX` / `APP_NAME` are unset (were `strata` / `HiroApp`). HiroApp pins those env vars.
103
122
  - `Schedule.command()` rejects cron strings other than `* * * * *` and `*/N * * * *` instead of silently never running them.
104
123
  - OpenAPI marks `GET /users/me/*` as bearer-authenticated.
105
124
  - OpenAPI `/users/me/current-organization` summaries no longer say team invitations.
@@ -111,11 +130,11 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
111
130
  ## 0.5.88
112
131
 
113
132
  - Exported cookie name constants (`SESSION_COOKIE`, `CSRF_COOKIE`, `FLASH_COOKIE`, `INTENDED_URL_COOKIE`, `PASSWORD_CONFIRM_COOKIE`) are `appCookieName(...)` so they match the default prefix instead of a hardcoded `strata_` string.
114
- - `@getstrata/core/jobs/dispatchWebhookJob` is a deprecated compatibility re-export of the the previous in-repo app webhook job.
133
+ - `@getstrata/core/jobs/dispatchWebhookJob` is a deprecated compatibility re-export of the HiroApp webhook job.
115
134
 
116
135
  ## 0.5.87
117
136
 
118
- - `AuthUserDirectory.hasActiveBrowserSession?(userId, issuedAt)` is optional. `SessionGuard` calls it after `session_valid_after` and rejects the HMAC cookie when it returns false so the previous in-repo app can revoke a single browser session by deleting the `sessions` row.
137
+ - `AuthUserDirectory.hasActiveBrowserSession?(userId, issuedAt)` is optional. `SessionGuard` calls it after `session_valid_after` and rejects the HMAC cookie when it returns false so HiroApp can revoke a single browser session by deleting the `sessions` row.
119
138
 
120
139
  ## 0.5.86
121
140
 
@@ -140,11 +159,11 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
140
159
 
141
160
  ## 0.5.81
142
161
 
143
- - `@getstrata/core/auth/intendedUrlCookie` (`createIntendedUrlCookie`, `readIntendedUrl`, `clearIntendedUrlCookie`, `createIntendedUrlCookieFromRequest`). HTML auth intended URL after HTML email verification: register with `redirect=` and the previous PHP framework `verified` HTML redirects stash `${APP_KEY_PREFIX}_intended` (`INTENDED_URL_COOKIE_NAME`, default `strata_intended`; TTL `INTENDED_URL_TTL_SECONDS`, default 86400s). `GET /verify-email` honors and clears it.
162
+ - `@getstrata/core/auth/intendedUrlCookie` (`createIntendedUrlCookie`, `readIntendedUrl`, `clearIntendedUrlCookie`, `createIntendedUrlCookieFromRequest`). HTML auth intended URL after HTML email verification: register with `redirect=` and verified HTML redirects stash `${APP_KEY_PREFIX}_intended` (`INTENDED_URL_COOKIE_NAME`, default `strata_intended`; TTL `INTENDED_URL_TTL_SECONDS`, default 86400s). `GET /verify-email` honors and clears it.
144
163
 
145
164
  ## 0.5.80
146
165
 
147
- - `appCookieName()` / `appDevSecret()` on `@getstrata/core/runtime/appKeyPrefix`. HMAC session, CSRF, flash, password-confirm, signed-URL, OAuth-state, and token-pepper fallbacks follow `APP_KEY_PREFIX` (the previous in-repo app defaults unchanged).
166
+ - `appCookieName()` / `appDevSecret()` on `@getstrata/core/runtime/appKeyPrefix`. HMAC session, CSRF, flash, password-confirm, signed-URL, OAuth-state, and token-pepper fallbacks follow `APP_KEY_PREFIX` (HiroApp defaults unchanged).
148
167
 
149
168
  ## 0.5.79
150
169
 
@@ -164,12 +183,12 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
164
183
 
165
184
  ## 0.5.75
166
185
 
167
- - the previous PHP framework `password.confirm`: `@getstrata/core/auth/passwordConfirmCookie` (`createPasswordConfirmCookie`, `hasFreshPasswordConfirmation`, `clearPasswordConfirmCookie`) and `createRequirePasswordConfirmMiddleware()` (HTML 302 `/confirm-password`, JSON 423). Cookie name `PASSWORD_CONFIRM_COOKIE_NAME` (default `strata_password_confirmed`), TTL `PASSWORD_CONFIRM_TIMEOUT` (default 10800s).
186
+ - Password confirm: `@getstrata/core/auth/passwordConfirmCookie` (`createPasswordConfirmCookie`, `hasFreshPasswordConfirmation`, `clearPasswordConfirmCookie`) and `createRequirePasswordConfirmMiddleware()` (HTML 302 `/confirm-password`, JSON 423). Cookie name `PASSWORD_CONFIRM_COOKIE_NAME` (default `strata_password_confirmed`), TTL `PASSWORD_CONFIRM_TIMEOUT` (default 10800s).
168
187
 
169
188
  ## 0.5.74
170
189
 
171
190
  - `AuthUser.emailVerifiedAt` and `@getstrata/core/auth/emailVerification` (`isEmailVerificationRequired`, `hasVerifiedEmail`). `null` means unverified; missing is treated as verified (GuestGuard / legacy).
172
- - `createRequireVerifiedMiddleware()` is the previous PHP framework `verified` (HTML 302 `/email/verify`, JSON 403).
191
+ - `createRequireVerifiedMiddleware()` requires a verified email (HTML 302 `/email/verify`, JSON 403).
173
192
 
174
193
  ## 0.5.73
175
194
 
@@ -194,40 +213,40 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
194
213
 
195
214
  ## 0.5.68
196
215
 
197
- - Published core no longer imports the previous in-repo app `src/config`. Frontend mode, queue retries, CORS, and upload limits read env (`FRONTEND_MODE`, `QUEUE_*`, `CORS_ALLOWED_ORIGINS`, `MAX_UPLOAD_BYTES`).
216
+ - Published core no longer imports HiroApp `src/config`. Frontend mode, queue retries, CORS, and upload limits read env (`FRONTEND_MODE`, `QUEUE_*`, `CORS_ALLOWED_ORIGINS`, `MAX_UPLOAD_BYTES`).
198
217
  - `@getstrata/core/runtime/frontendMode` exports `readFrontendMode` / `isViewsEnabled` / `isSpaEnabled`.
199
218
 
200
219
  ## 0.5.67
201
220
 
202
- - OpenAPI title, server URLs, and generated SDK class name come from `APP_NAME` / `APP_URL` / `API_PREFIX` / `APP_SDK_CLASS` instead of importing the previous in-repo app `src/config/app`.
203
- - `appEnv()`, `appUrl()`, `apiPrefix()`, and `sdkClientClassName()` live on `@getstrata/core/runtime/appKeyPrefix`. SIEM export, HSTS, and `safeFetch` DNS resolve use `appEnv()` instead of the previous in-repo app `appConfig`.
221
+ - OpenAPI title, server URLs, and generated SDK class name come from `APP_NAME` / `APP_URL` / `API_PREFIX` / `APP_SDK_CLASS` instead of importing HiroApp `src/config/app`.
222
+ - `appEnv()`, `appUrl()`, `apiPrefix()`, and `sdkClientClassName()` live on `@getstrata/core/runtime/appKeyPrefix`. SIEM export, HSTS, and `safeFetch` DNS resolve use `appEnv()` instead of HiroApp `appConfig`.
204
223
 
205
224
  ## 0.5.66
206
225
 
207
- - `createValidateSignatureMiddleware()` on `@getstrata/core/http/signedUrl` is the previous PHP framework's `signed` / `ValidateSignature` middleware. Invalid or expired links throw `ForbiddenError`.
226
+ - `createValidateSignatureMiddleware()` on `@getstrata/core/http/signedUrl` rejects invalid or expired signed links with `ForbiddenError`.
208
227
 
209
228
  ## 0.5.65
210
229
 
211
- - Identity helpers on `@getstrata/core/runtime/appKeyPrefix` (`smtpEhloHost`, `siemEventType`, `appUserAgent`, `otelServiceName`, `appDisplayName`, `webhookSignatureHeader`) so sibling apps are not stuck with the previous in-repo app SMTP/SIEM/OTEL/OAuth names.
212
- - SIEM `event_type` and CEF vendor follow `SIEM_EVENT_TYPE` / `APP_NAME` (defaults stay `strata.audit` / `the previous in-repo app`).
230
+ - Identity helpers on `@getstrata/core/runtime/appKeyPrefix` (`smtpEhloHost`, `siemEventType`, `appUserAgent`, `otelServiceName`, `appDisplayName`, `webhookSignatureHeader`) so sibling apps are not stuck with HiroApp SMTP/SIEM/OTEL/OAuth names.
231
+ - SIEM `event_type` and CEF vendor follow `SIEM_EVENT_TYPE` / `APP_NAME` (defaults stay `strata.audit` / `HiroApp`).
213
232
 
214
233
  ## 0.5.64
215
234
 
216
- - `APP_KEY_PREFIX` (default `strata`) namespaces Redis cache, queue, and throttle keys so sibling apps do not share the previous in-repo app's keyspace.
217
- - `DispatchWebhookJob` implementation lives in the the previous in-repo app webhook module. `@getstrata/core/jobs/dispatchWebhookJob` remains a compatibility re-export.
235
+ - `APP_KEY_PREFIX` (default `strata`) namespaces Redis cache, queue, and throttle keys so sibling apps do not share HiroApp's keyspace.
236
+ - `DispatchWebhookJob` implementation lives in the HiroApp webhook module. `@getstrata/core/jobs/dispatchWebhookJob` remains a compatibility re-export.
218
237
 
219
238
  ## 0.5.63
220
239
 
221
- - Flash cookies honor `FLASH_COOKIE_NAME` (default `strata_flash`) so sibling apps do not inherit the previous in-repo app's cookie name.
240
+ - Flash cookies honor `FLASH_COOKIE_NAME` (default `strata_flash`) so sibling apps do not inherit HiroApp's cookie name.
222
241
 
223
242
  ## 0.5.62
224
243
 
225
- the previous PHP framework URL signing, Bun-native markdown mail, and session-auth cleanup.
244
+ URL signing, Bun-native markdown mail, and session-auth cleanup.
226
245
 
227
246
  - `temporarySignedUrl` / `signedUrl` / `hasValidSignature` / `assertValidSignature` on `@getstrata/core/http/signedUrl`. HMAC uses `SIGNED_URL_SECRET` or `SESSION_SECRET`. Paths must be same-origin (`/` only; reject `//` and `://`).
228
247
  - `markdownToHtml()` uses `Bun.markdown.html()` plus `sanitizeMailHtml` (allowlist). Scripts, `javascript:` links, and unknown tags are stripped.
229
248
  - `ExportAuditLogsJob` (`@getstrata/core/jobs/exportAuditLogsJob`) wraps SIEM export so the scheduler can dispatch a real job.
230
- - `DispatchWebhookJob` reads `APP_ENV` and `WEBHOOK_SIGNATURE_HEADER` instead of the previous in-repo app `appConfig`.
249
+ - `DispatchWebhookJob` reads `APP_ENV` and `WEBHOOK_SIGNATURE_HEADER` instead of HiroApp `appConfig`.
231
250
  - `createRequireWebAuthMiddleware` runs the handler inside `runWithAuthUser` so `currentAuthUser()` works on HTMX session routes.
232
251
  - `generateTotpSecret()` / `buildOtpauthUrl()` on `@getstrata/core/security/totp`.
233
252
  - `createRequireAbilityMiddleware` rethrows `ForbiddenError` for HTML views so HTMX routes render a styled 403 instead of JSON.
@@ -251,13 +270,13 @@ HTMX HTML kernel gaps that sibling apps could not work around without weakening
251
270
 
252
271
  ## 0.5.59
253
272
 
254
- Sibling HTMX apps can consume published packages without the previous in-repo app-only glue.
273
+ Sibling HTMX apps can consume published packages without HiroApp-only glue.
255
274
 
256
275
  - Emit `@getstrata/core/facades` types at `dist/core/facades/index.d.ts` (CI checks every `exports.*.types` path after build).
257
- - Session and CSRF cookie names are configurable (`SESSION_COOKIE_NAME`, `CSRF_COOKIE_NAME`). Defaults remain `strata_session` and `strata_csrf` for the previous in-repo app.
276
+ - Session and CSRF cookie names are configurable (`SESSION_COOKIE_NAME`, `CSRF_COOKIE_NAME`). Defaults remain `strata_session` and `strata_csrf` for HiroApp.
258
277
  - Split `CORE_ABILITY_CHECKER_TOKEN` and `CORE_AUTH_USER_DIRECTORY_TOKEN` from `CORE_TOKEN_SERVICE_TOKEN`. HttpKernel prefers the ability-checker token; HMAC `SessionGuard` / layout data prefer the user directory. A compatibility shim uses `CORE_TOKEN_SERVICE_TOKEN` only when the bound value matches the requested type.
259
- - `configureWebLayoutData` lets apps choose `currentUser` vs `authUser`, a custom user loader, and extra template fields. the previous in-repo app still gets `{ authUser, csrfToken, flash }`.
278
+ - `configureWebLayoutData` lets apps choose `currentUser` vs `authUser`, a custom user loader, and extra template fields. HiroApp still gets `{ authUser, csrfToken, flash }`.
260
279
  - `redirectResponse`, `notFoundHtmlResponse`, `textResponse`, and `xmlResponse` join `htmlResponse` / `isHtmxRequest` on `@getstrata/core/view`.
261
280
  - `LOGIN_RATE_LIMIT_WINDOW_MS` is a deprecated alias for `LOGIN_RATE_LIMIT_WINDOW_SECONDS`.
262
281
 
263
- HMAC `SessionGuard` is unchanged for the previous in-repo app API/token apps. Sibling HTMX apps should use `@getstrata/bootstrap` `CookieSessionStore` + `createCookieSessionAuthManager`.
282
+ HMAC `SessionGuard` is unchanged for HiroApp API/token apps. Sibling HTMX apps should use `@getstrata/bootstrap` `CookieSessionStore` + `createCookieSessionAuthManager`.
package/README.md CHANGED
@@ -1,71 +1,64 @@
1
1
  # @getstrata/core
2
2
 
3
- Stable **Strata** framework surface for application modules.
3
+ Runtime library for [Strata](https://github.com/EyK-26/strata) apps: HTTP, auth, database, queue, mail, and security. Requires Bun (tested on 1.4.x).
4
4
 
5
- **Source:** `src/framework/public-api.ts` (monorepo)
6
- **Repository:** [EyK-26/strata](https://github.com/EyK-26/strata), directory `packages/strata-core`
5
+ Generate an app rather than wiring this by hand:
7
6
 
8
- HiroApp is the example product. Import this package from your own modules. HTML cookie apps: [docs/BUILDING-APPS.md](../../docs/BUILDING-APPS.md). Auth choices: [docs/AUTH.md](../../docs/AUTH.md). SQL engines: [docs/DATABASE.md](../../docs/DATABASE.md).
7
+ ```bash
8
+ bunx create-strata my-app
9
+ ```
9
10
 
10
- ## Usage
11
+ ## Import subpaths, not the root
11
12
 
12
- Set `DATABASE_URL` before importing (the connection is created lazily on first query):
13
+ Apps, and anything they import, should use subpaths:
13
14
 
14
15
  ```typescript
15
- process.env.DATABASE_URL ??= "postgresql://postgres:postgres@localhost:5432/myapp";
16
-
17
- import { Policy } from "@getstrata/core/auth/policy";
16
+ import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
17
+ import { bindDatabaseConnection } from "@getstrata/core/database/bindConnection";
18
18
  import { BaseRepository } from "@getstrata/core/database/baseRepository";
19
19
  import { FormRequest } from "@getstrata/core/http/formRequest";
20
+ import { Policy } from "@getstrata/core/auth/policy";
20
21
  import { withErrorHandling } from "@getstrata/core/http/response";
21
- import { EtaViewEngine } from "@getstrata/core/view";
22
+ import { ValidationError } from "@getstrata/core/errors/http";
23
+ import type { Migration } from "@getstrata/core/database/migrations/types";
22
24
  ```
23
25
 
24
- `.eta` files are **HTML + Eta tags** (`<% %>`, `<%= %>`, `<%~ include() %>`), not another template language. Class/attribute shorthand such as `section.section` or `a href=` fails at render time with the template name.
25
-
26
- **Dependency:** `eta` is a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database **engine** is your choice. Generated HiroApp uses **Bun's built-in `Bun.sql`** (Postgres): `createBunSqlPool()`, then `registerDefaultDatabasePool()` and `bindDatabaseConnection()`. `bindBunSql()` is a helper that registers both. Extra engines register with `registerNamedConnection` (`database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`).
27
-
28
- `orderBy` accepts `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`. `{ ilike }` uses the value as-is. Pass `%term%` yourself.
26
+ The root `@getstrata/core` import resolves, but nothing stops you from ending up with two copies of process-wide state: the database pool, the dialect override, async-local auth and tenant context, and the `HttpError` base class used with `instanceof`. Some subpaths re-export the main bundle for exactly that reason. Prefer subpaths everywhere and the problem does not arise.
29
27
 
30
- ## Admin and queue helpers
28
+ ## Database
31
29
 
32
- - `AdminResourceRegistry`, `formatAdminValue`: read-only resource browsers
33
- - `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
34
- - `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
30
+ Set `DATABASE_URL` before the first query; the connection is created lazily.
35
31
 
36
- Core ships failed-job helpers and an optional admin resource registry. Generated HiroApp does not include an admin dashboard. You do not have to use the registry.
32
+ ```typescript
33
+ process.env.DATABASE_URL ??= "postgresql://postgres:postgres@localhost:5432/myapp";
34
+ ```
37
35
 
38
- ## Build and verify (monorepo root)
36
+ The engine is your choice, and an app should have one primary engine. Postgres apps use Bun's built-in `Bun.sql` through `createBunSqlPool()`, then `registerDefaultDatabasePool()` and `bindDatabaseConnection()`; `bindBunSql()` registers both. SQLite and MySQL register through `database/sqliteConnection` and `database/mysqlConnection`, or `registerNamedConnection` from `database/namedConnections`.
39
37
 
40
- ```bash
41
- bun run build:framework
42
- bun run verify:framework
43
- bun run verify:shared-subpaths
44
- ```
38
+ `orderBy` takes `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`. `{ ilike }` uses the value as-is, so pass `%term%` yourself.
45
39
 
46
- ## Subpath imports
40
+ Full-text `tsMatch` is PostgreSQL only.
47
41
 
48
- `@getstrata/core` publishes many subpaths (for example `@getstrata/core/http/authMiddleware`, `@getstrata/core/database/migrations`). Prefer subpaths over the root import in apps, bootstrap, and tests.
42
+ `mysql2` and `eta` are direct dependencies, so they install even for a SQLite JSON API app.
49
43
 
50
- Some subpaths **re-export the main bundle** so singleton state stays shared (database pool, dialect override, async-local auth/tenant, `HttpError` for `instanceof`). The list lives in `scripts/core-shared-subpaths.ts`.
44
+ ## Views
51
45
 
52
- When adding a subpath that owns process-wide state or base classes used with `instanceof`, append it to `CORE_SHARED_SUBPATHS`, run `bun scripts/sync-package-subpaths.ts`, and rebuild.
46
+ `.eta` files are HTML plus Eta tags (`<% %>`, `<%= %>`, `<%~ include() %>`). Pug-style class or id shorthand such as `section.section` throws at render time and names the offending template.
53
47
 
54
- `scripts/verify-no-root-imports.ts` blocks root `@getstrata/core` imports in application source.
48
+ ## Admin and queue helpers
55
49
 
56
- ```typescript
57
- import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
58
- import { bindDatabaseConnection } from "@getstrata/core/database/bindConnection";
59
- import { ValidationError } from "@getstrata/core/errors/http";
60
- import type { Migration } from "@getstrata/core/database/migrations/types";
61
- ```
50
+ - `AdminResourceRegistry`, `formatAdminValue`: read-only resource browsers
51
+ - `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
52
+ - `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
62
53
 
63
- ## Publish to npm
54
+ Generated apps do not include an admin dashboard, and the registry is optional.
64
55
 
65
- Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
56
+ ## Docs
66
57
 
67
- 1. Add `NPM_TOKEN` to GitHub repository secrets.
68
- 2. Tag a release: `git tag v0.7.5 && git push origin v0.7.5`
69
- 3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
58
+ - [Getting started](https://github.com/EyK-26/strata/blob/main/docs/GETTING-STARTED.md)
59
+ - [Building apps](https://github.com/EyK-26/strata/blob/main/docs/BUILDING-APPS.md)
60
+ - [Auth choices](https://github.com/EyK-26/strata/blob/main/docs/AUTH.md)
61
+ - [Databases](https://github.com/EyK-26/strata/blob/main/docs/DATABASE.md)
62
+ - [Production](https://github.com/EyK-26/strata/blob/main/docs/PRODUCTION.md)
70
63
 
71
- See [docs/PACKAGING.md](../../docs/PACKAGING.md).
64
+ Contributing to the framework itself: [CONTRIBUTING.md](https://github.com/EyK-26/strata/blob/main/CONTRIBUTING.md).
@@ -10,7 +10,8 @@ type ExtendedQueryOptions<TEntity extends object> = QueryOptions<TEntity> & {
10
10
  interface DatabaseConnection {
11
11
  unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
12
12
  begin?<T>(callback: (transaction: DatabaseConnection) => Promise<T>): Promise<T>;
13
- close?(): Promise<void>;
13
+ /** SQLite closes synchronously; pooled drivers return a promise. */
14
+ close?(): void | Promise<void>;
14
15
  }
15
16
  interface SqlDatabaseConnection extends DatabaseConnection {
16
17
  (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
@@ -4,6 +4,8 @@ interface SqlDialect {
4
4
  placeholder(index: number): string;
5
5
  quoteIdentifier(identifier: string): string;
6
6
  nowExpression(): string;
7
+ /** Render a Date as a literal this engine accepts for a timestamp column. */
8
+ timestampValue(value: Date): string;
7
9
  returningClause(columns: string): string;
8
10
  ilikeOperator(): "ILIKE" | "LIKE";
9
11
  nullsLastSuffix(): string;
@@ -12,7 +14,9 @@ interface SqlDialect {
12
14
  declare function dialectFor(driver: DatabaseDriver): SqlDialect;
13
15
  declare function currentSqlDialect(): SqlDialect;
14
16
  declare function useSqlDialect(driver: DatabaseDriver): SqlDialect;
17
+ /** Format a timestamp parameter for whichever engine is active. */
18
+ declare function sqlTimestamp(value?: Date): string;
15
19
  declare function resetSqlDialect(): void;
16
20
  declare function runWithSqlDialect<T>(driver: DatabaseDriver, callback: () => T | Promise<T>): T | Promise<T>;
17
21
  export type { SqlDialect };
18
- export { currentSqlDialect, dialectFor, resetSqlDialect, runWithSqlDialect, useSqlDialect };
22
+ export { currentSqlDialect, dialectFor, resetSqlDialect, runWithSqlDialect, sqlTimestamp, useSqlDialect, };
@@ -7,6 +7,10 @@ interface PostgresErrorLike {
7
7
  message?: string;
8
8
  }
9
9
  declare function isPostgresError(error: unknown): error is PostgresErrorLike;
10
+ /**
11
+ * Constraint violations become 4xx with a fixed message. Anything else is a
12
+ * 500 with a generic message; the raw driver text never reaches the client.
13
+ */
10
14
  declare function mapDatabaseError(error: unknown): HttpError;
11
15
  declare function withDatabaseErrorHandling<TValue>(operation: () => Promise<TValue>): Promise<TValue>;
12
16
  export { isPostgresError, mapDatabaseError, withDatabaseErrorHandling };
@@ -1,3 +1,4 @@
1
+ import mysql from "mysql2/promise";
1
2
  import type { ActiveDatabaseHandle } from "./connectionContext.ts";
2
3
  type MysqlExecutable = {
3
4
  execute: (sql: string, params?: unknown[]) => Promise<[unknown, unknown]>;
@@ -7,6 +8,7 @@ type MysqlConnection = ActiveDatabaseHandle & {
7
8
  close(): Promise<void>;
8
9
  };
9
10
  declare function createMysqlConnectionFromPool(pool: MysqlExecutable): MysqlConnection;
11
+ declare function createMysqlPool(url: string): mysql.Pool;
10
12
  declare function createMysqlConnection(url: string): MysqlConnection;
11
13
  export type { MysqlConnection, MysqlExecutable };
12
- export { createMysqlConnection, createMysqlConnectionFromPool };
14
+ export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool };
@@ -30,6 +30,9 @@ declare class PayloadTooLargeError extends HttpError {
30
30
  declare class PreconditionFailedError extends HttpError {
31
31
  constructor(message?: string, details?: unknown);
32
32
  }
33
+ declare class InternalServerError extends HttpError {
34
+ constructor(message?: string, details?: unknown);
35
+ }
33
36
  interface HttpErrorLike {
34
37
  status: number;
35
38
  message: string;
@@ -37,4 +40,4 @@ interface HttpErrorLike {
37
40
  }
38
41
  declare function isHttpErrorLike(error: unknown): error is HttpErrorLike;
39
42
  declare function toHttpError(error: unknown): HttpError | null;
40
- export { BadRequestError, ConflictError, ForbiddenError, HttpError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, };
43
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, };
@@ -1,3 +1,6 @@
1
- declare function trustForwardedFor(env?: Record<string, string | undefined>): boolean;
2
- declare function readClientIp(request: Request, env?: Record<string, string | undefined>): string | undefined;
3
- export { readClientIp, trustForwardedFor };
1
+ type EnvRecord = Record<string, string | undefined>;
2
+ declare function trustForwardedFor(env?: EnvRecord): boolean;
3
+ declare function isPrivateAddress(address: string): boolean;
4
+ /** Forwarded headers only when trusted; otherwise the socket address the server recorded. */
5
+ declare function readClientIp(request: Request, env?: EnvRecord): string | undefined;
6
+ export { isPrivateAddress, readClientIp, trustForwardedFor };
@@ -1,5 +1,7 @@
1
+ import { type HttpError } from "@getstrata/core/errors/http";
1
2
  type FieldErrors = Record<string, string[]>;
2
3
  declare function normalizeFieldErrors(details: unknown): FieldErrors;
4
+ declare function logServerError(error: unknown, mappedError: HttpError): void;
3
5
  declare function webErrorResponse(error: unknown, request?: Request): Promise<Response | null>;
4
6
  export type { FieldErrors };
5
- export { normalizeFieldErrors, webErrorResponse };
7
+ export { logServerError, normalizeFieldErrors, webErrorResponse };
@@ -0,0 +1,4 @@
1
+ type EnvRecord = Record<string, string | undefined>;
2
+ /** Production when either the framework or the Node convention says so. */
3
+ declare function isProductionEnv(env?: EnvRecord): boolean;
4
+ export { isProductionEnv };
@@ -1,4 +1,5 @@
1
1
  type TenancyDriver = "rls" | "column" | "none";
2
+ /** Unset means rls (the Postgres example). Anything else must be an exact driver name. */
2
3
  declare function readTenancyDriver(env?: Record<string, string | undefined>): TenancyDriver;
3
4
  declare function isTenancyEnabled(env?: Record<string, string | undefined>): boolean;
4
5
  /** Postgres `SET LOCAL` / `set_config` for row-level security. SQLite and MySQL cannot do this. */
@@ -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";
@@ -78,14 +78,16 @@ function resolveScimTenantFromToken(token) {
78
78
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
79
79
 
80
80
  // ../../src/core/tenant/tenancyConfig.ts
81
+ var TENANCY_DRIVERS = ["rls", "column", "none"];
81
82
  function readTenancyDriver(env = process.env) {
82
- if (env.TENANCY_DRIVER === "none") {
83
- return "none";
83
+ const raw = env.TENANCY_DRIVER?.trim();
84
+ if (raw === undefined || raw === "") {
85
+ return "rls";
84
86
  }
85
- if (env.TENANCY_DRIVER === "column") {
86
- return "column";
87
+ if (TENANCY_DRIVERS.includes(raw)) {
88
+ return raw;
87
89
  }
88
- return "rls";
90
+ throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
89
91
  }
90
92
  function isTenancyEnabled(env = process.env) {
91
93
  return readTenancyDriver(env) !== "none";
@@ -136,8 +138,8 @@ async function runWithTenantDatabase(tenant, callback) {
136
138
  return await runWithTenant(tenant, callback);
137
139
  }
138
140
  if (hasActiveDatabaseConnection()) {
139
- const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
140
- await applyTenantContextToTransaction(activeConnection2, tenant.id);
141
+ const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
142
+ await applyTenantContextToTransaction(activeConnection, tenant.id);
141
143
  return await runWithTenant(tenant, callback);
142
144
  }
143
145
  return await getDefaultDatabasePool().begin(async (transaction) => {
@@ -127,8 +127,8 @@ function readSession(request) {
127
127
  }
128
128
  const parts = cookieValue.split(".");
129
129
  if (parts.length === 4) {
130
- const [userIdRaw2, issuedAtRaw2, ttlRaw, cookieSignature2] = parts;
131
- return readSignedSession(String(userIdRaw2), String(issuedAtRaw2), cookieSignature2, Number.parseInt(String(ttlRaw), 10), true);
130
+ const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
131
+ return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
132
132
  }
133
133
  if (parts.length !== 3) {
134
134
  return null;
@@ -194,8 +194,8 @@ function readSession(request) {
194
194
  }
195
195
  const parts = cookieValue.split(".");
196
196
  if (parts.length === 4) {
197
- const [userIdRaw2, issuedAtRaw2, ttlRaw, cookieSignature2] = parts;
198
- return readSignedSession(String(userIdRaw2), String(issuedAtRaw2), cookieSignature2, Number.parseInt(String(ttlRaw), 10), true);
197
+ const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
198
+ return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
199
199
  }
200
200
  if (parts.length !== 3) {
201
201
  return null;
@@ -0,0 +1 @@
1
+ // @bun
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  BadRequestError,
5
5
  ConflictError,
6
+ InternalServerError,
6
7
  toHttpError,
7
8
  UnprocessableEntityError
8
9
  } from "@getstrata/core/errors/http";
@@ -21,14 +22,55 @@ function getPostgresSqlState(error) {
21
22
  }
22
23
  return;
23
24
  }
25
+ var MYSQL_ERRNO_MESSAGES = {
26
+ 1062: () => new ConflictError("A record with these values already exists."),
27
+ 1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
28
+ 1452: () => new UnprocessableEntityError("Referenced record does not exist."),
29
+ 1048: () => new BadRequestError("Required field is missing."),
30
+ 3819: () => new BadRequestError("Value violates a database constraint.")
31
+ };
32
+ function mapSqliteError(error) {
33
+ const code = typeof error.code === "string" ? error.code : "";
34
+ if (!code.startsWith("SQLITE_")) {
35
+ return null;
36
+ }
37
+ if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
38
+ return new ConflictError("A record with these values already exists.");
39
+ }
40
+ if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
41
+ return new UnprocessableEntityError("Referenced record does not exist.");
42
+ }
43
+ if (code === "SQLITE_CONSTRAINT_NOTNULL") {
44
+ return new BadRequestError("Required field is missing.");
45
+ }
46
+ if (code.startsWith("SQLITE_CONSTRAINT")) {
47
+ return new BadRequestError("Value violates a database constraint.");
48
+ }
49
+ return new InternalServerError("Database operation failed.");
50
+ }
51
+ function mapMysqlError(error) {
52
+ const code = typeof error.code === "string" ? error.code : "";
53
+ if (!code.startsWith("ER_")) {
54
+ return null;
55
+ }
56
+ const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
57
+ return factory ? factory() : new InternalServerError("Database operation failed.");
58
+ }
24
59
  function mapDatabaseError(error) {
25
60
  const httpError = toHttpError(error);
26
61
  if (httpError) {
27
62
  return httpError;
28
63
  }
29
64
  if (!isPostgresError(error)) {
30
- const message = error instanceof Error ? error.message : "Database operation failed.";
31
- return new BadRequestError(message);
65
+ return new InternalServerError;
66
+ }
67
+ const sqlite = mapSqliteError(error);
68
+ if (sqlite) {
69
+ return sqlite;
70
+ }
71
+ const mysql = mapMysqlError(error);
72
+ if (mysql) {
73
+ return mysql;
32
74
  }
33
75
  const sqlState = getPostgresSqlState(error);
34
76
  switch (sqlState) {
@@ -49,10 +91,7 @@ function mapDatabaseError(error) {
49
91
  constraint: error.constraint
50
92
  });
51
93
  default:
52
- return new BadRequestError(error.message ?? "Database operation failed.", {
53
- code: error.code,
54
- sqlState
55
- });
94
+ return new InternalServerError("Database operation failed.");
56
95
  }
57
96
  }
58
97
  async function withDatabaseErrorHandling(operation) {