@getstrata/bootstrap 1.0.9 → 1.1.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/bootstrap changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ Lockstep with `@getstrata/core` 1.1.0. Cookie sessions select `s.created_at AS session_created_at` so `users.created_at` from `u.*` cannot clobber the session issued-at. API group always includes session-mutating CSRF. `buildModuleRoutes` maps thrown API middleware errors (including CSRF `ForbiddenError`) to JSON. `HttpKernel.wrap("api")` does the same for routes used outside that builder. Path confinement on the web server and safer production defaults. The rls role denylist checks the runtime URL (`APP_DATABASE_URL` when set, otherwise `DATABASE_URL`) so fixture admin `DATABASE_URL` can stay `postgres`.
6
+
3
7
  ## 1.0.9
4
8
 
5
9
  Label HiroApp as internal e2e dogfood and seed notes via Model
@@ -1,2 +1,14 @@
1
+ type LivePostgresRoleInspector = () => Promise<{
2
+ rolname: string;
3
+ rolsuper: boolean;
4
+ rolbypassrls: boolean;
5
+ } | null>;
6
+ declare function setLivePostgresRoleInspectorForTests(inspect: LivePostgresRoleInspector | null): void;
7
+ /**
8
+ * After the runtime pool is open, refuse rolsuper / rolbypassrls whenever
9
+ * TENANCY_DRIVER=rls, including local. Fail closed if the live query cannot run.
10
+ * MIGRATION_DATABASE_URL may stay a superuser.
11
+ */
12
+ declare function assertRlsLiveDatabaseRole(env?: Record<string, string | undefined>, inspect?: LivePostgresRoleInspector): Promise<void>;
1
13
  declare function assertProductionSecrets(env?: Record<string, string | undefined>): void;
2
- export { assertProductionSecrets };
14
+ export { assertProductionSecrets, assertRlsLiveDatabaseRole, setLivePostgresRoleInspectorForTests };
@@ -42,6 +42,7 @@ export declare class CookieSessionStore {
42
42
  private sql;
43
43
  create(user: SessionUser, meta?: SessionCreateMeta): Promise<string>;
44
44
  destroy(sessionId: string): Promise<void>;
45
+ destroyAllSessions(userId: number): Promise<void>;
45
46
  destroyOtherSessions(userId: number, keepSessionId: string): Promise<void>;
46
47
  listForUser(userId: number): Promise<BrowserSessionRecord[]>;
47
48
  touch(sessionId: string): Promise<void>;
@@ -2,6 +2,7 @@
2
2
  // ../../src/bootstrap/buildModuleRoutes.ts
3
3
  import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
4
4
  import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
5
+ import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
5
6
 
6
7
  // ../../src/bootstrap/httpKernel.ts
7
8
  import {
@@ -138,8 +139,9 @@ class HttpKernel {
138
139
  case "web":
139
140
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
140
141
  case "api": {
142
+ const csrf = createCsrfMiddleware();
141
143
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
142
- return [];
144
+ return [csrf];
143
145
  }
144
146
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
145
147
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -149,7 +151,8 @@ class HttpKernel {
149
151
  createMemoryThrottleMiddleware({
150
152
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
151
153
  decaySeconds: 60
152
- })
154
+ }),
155
+ csrf
153
156
  ];
154
157
  }
155
158
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -158,7 +161,8 @@ class HttpKernel {
158
161
  redisUrl,
159
162
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
160
163
  decaySeconds: 60
161
- })
164
+ }),
165
+ csrf
162
166
  ];
163
167
  }
164
168
  default:
@@ -168,13 +172,14 @@ class HttpKernel {
168
172
  wrap(groups, handler) {
169
173
  const names = Array.isArray(groups) ? groups : [groups];
170
174
  const middleware = names.flatMap((name) => this.group(name));
171
- if (middleware.length === 0) {
172
- return handler;
175
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
176
+ if (names.includes("api")) {
177
+ return withJsonErrorHandling(wrapped);
173
178
  }
174
- return withMiddleware(...middleware)(handler);
179
+ return wrapped;
175
180
  }
176
181
  wrapApi(handler) {
177
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
182
+ return this.wrap(["api", "authenticated"], handler);
178
183
  }
179
184
  wrapWeb(handler) {
180
185
  return withErrorHandling(this.wrap("web", handler));
@@ -283,41 +288,28 @@ class HttpKernel {
283
288
  return [createRequireVerifiedMiddleware(auth)];
284
289
  }
285
290
  wrapThrottle(scope, rateLimit, handler) {
286
- const middleware = [];
287
291
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
288
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
289
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
290
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
291
- if (redisUrl) {
292
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
293
- redisUrl,
294
- maxAttempts: rateLimit.maxAttempts,
295
- decaySeconds: rateLimit.decaySeconds
296
- }) : createThrottleMiddleware({
297
- redisUrl,
298
- maxAttempts: rateLimit.maxAttempts,
299
- decaySeconds: rateLimit.decaySeconds,
300
- keyPrefix: memoryKeyPrefix
301
- });
302
- middleware.push(throttle);
303
- } else {
304
- middleware.push(createMemoryThrottleMiddleware({
305
- maxAttempts: rateLimit.maxAttempts,
306
- decaySeconds: rateLimit.decaySeconds,
307
- keyPrefix: memoryKeyPrefix
308
- }));
309
- }
310
- } else {
311
- middleware.push(createMemoryThrottleMiddleware({
292
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
293
+ if (scope === "login") {
294
+ return withMiddleware(createLoginThrottleMiddleware({
295
+ ...redisUrl ? { redisUrl } : {},
296
+ maxAttempts: rateLimit.maxAttempts,
297
+ decaySeconds: rateLimit.decaySeconds
298
+ }))(handler);
299
+ }
300
+ if (redisUrl) {
301
+ return withMiddleware(createThrottleMiddleware({
302
+ redisUrl,
312
303
  maxAttempts: rateLimit.maxAttempts,
313
304
  decaySeconds: rateLimit.decaySeconds,
314
305
  keyPrefix: memoryKeyPrefix
315
- }));
316
- }
317
- if (middleware.length === 0) {
318
- return handler;
306
+ }))(handler);
319
307
  }
320
- return withMiddleware(...middleware)(handler);
308
+ return withMiddleware(createMemoryThrottleMiddleware({
309
+ maxAttempts: rateLimit.maxAttempts,
310
+ decaySeconds: rateLimit.decaySeconds,
311
+ keyPrefix: memoryKeyPrefix
312
+ }))(handler);
321
313
  }
322
314
  }
323
315
  function createHttpKernel(dependencies) {
@@ -462,7 +454,11 @@ function buildModuleRoutes(dependencies, options = {}) {
462
454
  routeRegistry.clear();
463
455
  }
464
456
  const kernel = createHttpKernel(dependencies);
465
- const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
457
+ const middleware = [
458
+ ...kernel.globalMiddleware(),
459
+ createJsonErrorMiddleware(),
460
+ ...kernel.group("api")
461
+ ];
466
462
  const cachedJson = createCachedJson(dependencies);
467
463
  const moduleRoutes = {};
468
464
  for (const module of modules) {
@@ -5,6 +5,7 @@ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/
5
5
  // ../../src/bootstrap/buildModuleRoutes.ts
6
6
  import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
7
7
  import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
8
+ import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
8
9
 
9
10
  // ../../src/bootstrap/httpKernel.ts
10
11
  import {
@@ -141,8 +142,9 @@ class HttpKernel {
141
142
  case "web":
142
143
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
143
144
  case "api": {
145
+ const csrf = createCsrfMiddleware();
144
146
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
145
- return [];
147
+ return [csrf];
146
148
  }
147
149
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
148
150
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -152,7 +154,8 @@ class HttpKernel {
152
154
  createMemoryThrottleMiddleware({
153
155
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
154
156
  decaySeconds: 60
155
- })
157
+ }),
158
+ csrf
156
159
  ];
157
160
  }
158
161
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -161,7 +164,8 @@ class HttpKernel {
161
164
  redisUrl,
162
165
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
163
166
  decaySeconds: 60
164
- })
167
+ }),
168
+ csrf
165
169
  ];
166
170
  }
167
171
  default:
@@ -171,13 +175,14 @@ class HttpKernel {
171
175
  wrap(groups, handler) {
172
176
  const names = Array.isArray(groups) ? groups : [groups];
173
177
  const middleware = names.flatMap((name) => this.group(name));
174
- if (middleware.length === 0) {
175
- return handler;
178
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
179
+ if (names.includes("api")) {
180
+ return withJsonErrorHandling(wrapped);
176
181
  }
177
- return withMiddleware(...middleware)(handler);
182
+ return wrapped;
178
183
  }
179
184
  wrapApi(handler) {
180
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
185
+ return this.wrap(["api", "authenticated"], handler);
181
186
  }
182
187
  wrapWeb(handler) {
183
188
  return withErrorHandling(this.wrap("web", handler));
@@ -286,41 +291,28 @@ class HttpKernel {
286
291
  return [createRequireVerifiedMiddleware(auth)];
287
292
  }
288
293
  wrapThrottle(scope, rateLimit, handler) {
289
- const middleware = [];
290
294
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
291
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
292
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
293
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
294
- if (redisUrl) {
295
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
296
- redisUrl,
297
- maxAttempts: rateLimit.maxAttempts,
298
- decaySeconds: rateLimit.decaySeconds
299
- }) : createThrottleMiddleware({
300
- redisUrl,
301
- maxAttempts: rateLimit.maxAttempts,
302
- decaySeconds: rateLimit.decaySeconds,
303
- keyPrefix: memoryKeyPrefix
304
- });
305
- middleware.push(throttle);
306
- } else {
307
- middleware.push(createMemoryThrottleMiddleware({
308
- maxAttempts: rateLimit.maxAttempts,
309
- decaySeconds: rateLimit.decaySeconds,
310
- keyPrefix: memoryKeyPrefix
311
- }));
312
- }
313
- } else {
314
- middleware.push(createMemoryThrottleMiddleware({
295
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
296
+ if (scope === "login") {
297
+ return withMiddleware(createLoginThrottleMiddleware({
298
+ ...redisUrl ? { redisUrl } : {},
299
+ maxAttempts: rateLimit.maxAttempts,
300
+ decaySeconds: rateLimit.decaySeconds
301
+ }))(handler);
302
+ }
303
+ if (redisUrl) {
304
+ return withMiddleware(createThrottleMiddleware({
305
+ redisUrl,
315
306
  maxAttempts: rateLimit.maxAttempts,
316
307
  decaySeconds: rateLimit.decaySeconds,
317
308
  keyPrefix: memoryKeyPrefix
318
- }));
319
- }
320
- if (middleware.length === 0) {
321
- return handler;
309
+ }))(handler);
322
310
  }
323
- return withMiddleware(...middleware)(handler);
311
+ return withMiddleware(createMemoryThrottleMiddleware({
312
+ maxAttempts: rateLimit.maxAttempts,
313
+ decaySeconds: rateLimit.decaySeconds,
314
+ keyPrefix: memoryKeyPrefix
315
+ }))(handler);
324
316
  }
325
317
  }
326
318
  function createHttpKernel(dependencies) {
@@ -465,7 +457,11 @@ function buildModuleRoutes(dependencies, options = {}) {
465
457
  routeRegistry.clear();
466
458
  }
467
459
  const kernel = createHttpKernel(dependencies);
468
- const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
460
+ const middleware = [
461
+ ...kernel.globalMiddleware(),
462
+ createJsonErrorMiddleware(),
463
+ ...kernel.group("api")
464
+ ];
469
465
  const cachedJson = createCachedJson(dependencies);
470
466
  const moduleRoutes = {};
471
467
  for (const module of modules) {
@@ -108,7 +108,7 @@ import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
108
108
  import { envFlagEnabled } from "@getstrata/core/runtime/appEnv";
109
109
  var authConfig = {
110
110
  allowDevHeaders: envFlagEnabled(process.env.AUTH_DEV_HEADERS),
111
- tokenDefaultAbilities: ["*"]
111
+ tokenDefaultAbilities: []
112
112
  };
113
113
 
114
114
  // ../../src/bootstrap/config.ts
@@ -144,7 +144,7 @@ var authProvider = {
144
144
  config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
145
145
  const apiGuard = new DatabaseTokenGuard(container);
146
146
  const sessionGuard = new SessionGuard(container);
147
- const jwtGuard = new JwtGuard;
147
+ const jwtGuard = new JwtGuard(container);
148
148
  const basicGuard = new BasicAuthGuard(container);
149
149
  const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
150
150
  if (authConfig.allowDevHeaders) {
@@ -189,7 +189,7 @@ import { validateEnv } from "@getstrata/core/config/envSchema";
189
189
  var appConfig = {
190
190
  name: process.env.APP_NAME?.trim() || "Strata",
191
191
  env: process.env.APP_ENV ?? "local",
192
- debug: (process.env.APP_DEBUG ?? "true") !== "false",
192
+ debug: (process.env.APP_DEBUG ?? "false") === "true",
193
193
  url: process.env.APP_URL ?? "http://localhost:3000",
194
194
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
195
195
  };
@@ -3,6 +3,7 @@
3
3
  import { join } from "path";
4
4
  import { jsonResponse } from "@getstrata/core/http/response";
5
5
  import { isSpaEnabled, isViewsEnabled, readSpaPrefix } from "@getstrata/core/runtime/frontendMode";
6
+ import { assertPathUnderRoot } from "@getstrata/core/security/safePath";
6
7
  var SPA_DIST_DIRECTORY = join(process.cwd(), "frontend/dist");
7
8
  function relativeSpaPath(pathname, prefix) {
8
9
  if (pathname === prefix || pathname === `${prefix}/`) {
@@ -21,9 +22,15 @@ function createSpaDocumentHandler(prefix, distDirectory) {
21
22
  return new Response("Not found", { status: 404 });
22
23
  }
23
24
  const relativePath = relativeSpaPath(pathname, prefix);
24
- const assetFile = Bun.file(join(distDirectory, relativePath));
25
- if (relativePath.length > 0 && await assetFile.exists()) {
26
- return new Response(assetFile);
25
+ if (relativePath.length > 0) {
26
+ try {
27
+ const assetFile = Bun.file(assertPathUnderRoot(distDirectory, relativePath));
28
+ if (await assetFile.exists()) {
29
+ return new Response(assetFile);
30
+ }
31
+ } catch {
32
+ return new Response("Not found", { status: 404 });
33
+ }
27
34
  }
28
35
  const indexFile = Bun.file(indexFilePath);
29
36
  if (await indexFile.exists()) {
@@ -2,6 +2,7 @@
2
2
  // ../../src/bootstrap/createWebRoutes.ts
3
3
  import { join as join2 } from "path";
4
4
  import { isViewsEnabled as isViewsEnabled2 } from "@getstrata/core/runtime/frontendMode";
5
+ import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
5
6
  import { notFoundHtmlResponse } from "@getstrata/core/view";
6
7
 
7
8
  // ../../src/bootstrap/buildWebModuleRoutes.ts
@@ -10,6 +11,7 @@ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/
10
11
  // ../../src/bootstrap/buildModuleRoutes.ts
11
12
  import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
12
13
  import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
14
+ import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
13
15
 
14
16
  // ../../src/bootstrap/httpKernel.ts
15
17
  import {
@@ -146,8 +148,9 @@ class HttpKernel {
146
148
  case "web":
147
149
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
148
150
  case "api": {
151
+ const csrf = createCsrfMiddleware();
149
152
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
150
- return [];
153
+ return [csrf];
151
154
  }
152
155
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
153
156
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -157,7 +160,8 @@ class HttpKernel {
157
160
  createMemoryThrottleMiddleware({
158
161
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
159
162
  decaySeconds: 60
160
- })
163
+ }),
164
+ csrf
161
165
  ];
162
166
  }
163
167
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -166,7 +170,8 @@ class HttpKernel {
166
170
  redisUrl,
167
171
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
168
172
  decaySeconds: 60
169
- })
173
+ }),
174
+ csrf
170
175
  ];
171
176
  }
172
177
  default:
@@ -176,13 +181,14 @@ class HttpKernel {
176
181
  wrap(groups, handler) {
177
182
  const names = Array.isArray(groups) ? groups : [groups];
178
183
  const middleware = names.flatMap((name) => this.group(name));
179
- if (middleware.length === 0) {
180
- return handler;
184
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
185
+ if (names.includes("api")) {
186
+ return withJsonErrorHandling(wrapped);
181
187
  }
182
- return withMiddleware(...middleware)(handler);
188
+ return wrapped;
183
189
  }
184
190
  wrapApi(handler) {
185
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
191
+ return this.wrap(["api", "authenticated"], handler);
186
192
  }
187
193
  wrapWeb(handler) {
188
194
  return withErrorHandling(this.wrap("web", handler));
@@ -291,41 +297,28 @@ class HttpKernel {
291
297
  return [createRequireVerifiedMiddleware(auth)];
292
298
  }
293
299
  wrapThrottle(scope, rateLimit, handler) {
294
- const middleware = [];
295
300
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
296
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
297
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
298
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
299
- if (redisUrl) {
300
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
301
- redisUrl,
302
- maxAttempts: rateLimit.maxAttempts,
303
- decaySeconds: rateLimit.decaySeconds
304
- }) : createThrottleMiddleware({
305
- redisUrl,
306
- maxAttempts: rateLimit.maxAttempts,
307
- decaySeconds: rateLimit.decaySeconds,
308
- keyPrefix: memoryKeyPrefix
309
- });
310
- middleware.push(throttle);
311
- } else {
312
- middleware.push(createMemoryThrottleMiddleware({
313
- maxAttempts: rateLimit.maxAttempts,
314
- decaySeconds: rateLimit.decaySeconds,
315
- keyPrefix: memoryKeyPrefix
316
- }));
317
- }
318
- } else {
319
- middleware.push(createMemoryThrottleMiddleware({
301
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
302
+ if (scope === "login") {
303
+ return withMiddleware(createLoginThrottleMiddleware({
304
+ ...redisUrl ? { redisUrl } : {},
305
+ maxAttempts: rateLimit.maxAttempts,
306
+ decaySeconds: rateLimit.decaySeconds
307
+ }))(handler);
308
+ }
309
+ if (redisUrl) {
310
+ return withMiddleware(createThrottleMiddleware({
311
+ redisUrl,
320
312
  maxAttempts: rateLimit.maxAttempts,
321
313
  decaySeconds: rateLimit.decaySeconds,
322
314
  keyPrefix: memoryKeyPrefix
323
- }));
315
+ }))(handler);
324
316
  }
325
- if (middleware.length === 0) {
326
- return handler;
327
- }
328
- return withMiddleware(...middleware)(handler);
317
+ return withMiddleware(createMemoryThrottleMiddleware({
318
+ maxAttempts: rateLimit.maxAttempts,
319
+ decaySeconds: rateLimit.decaySeconds,
320
+ keyPrefix: memoryKeyPrefix
321
+ }))(handler);
329
322
  }
330
323
  }
331
324
  function createHttpKernel(dependencies) {
@@ -470,7 +463,11 @@ function buildModuleRoutes(dependencies, options = {}) {
470
463
  routeRegistry.clear();
471
464
  }
472
465
  const kernel = createHttpKernel(dependencies);
473
- const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
466
+ const middleware = [
467
+ ...kernel.globalMiddleware(),
468
+ createJsonErrorMiddleware(),
469
+ ...kernel.group("api")
470
+ ];
474
471
  const cachedJson = createCachedJson(dependencies);
475
472
  const moduleRoutes = {};
476
473
  for (const module of modules) {
@@ -518,12 +515,16 @@ function createWebRoutes(dependencies, options = {}) {
518
515
  wrappedRoutes["/assets/*"] = async (request) => {
519
516
  registerRoute("GET", "/assets/*", ["global", "web"]);
520
517
  const pathname = new URL(request.url).pathname;
521
- const relativePath = pathname.replace(/^\//, "");
522
- const file = Bun.file(join2(process.cwd(), "public", relativePath));
523
- if (!await file.exists()) {
518
+ try {
519
+ const filePath = assertUrlPathUnderRoot(join2(process.cwd(), "public"), pathname);
520
+ const file = Bun.file(filePath);
521
+ if (!await file.exists()) {
522
+ return notFoundHtmlResponse();
523
+ }
524
+ return new Response(file);
525
+ } catch {
524
526
  return notFoundHtmlResponse();
525
527
  }
526
- return new Response(file);
527
528
  };
528
529
  registerRoute("GET", "/assets/*", ["global", "web"]);
529
530
  return wrappedRoutes;
@@ -108,7 +108,7 @@ import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
108
108
  import { envFlagEnabled } from "@getstrata/core/runtime/appEnv";
109
109
  var authConfig = {
110
110
  allowDevHeaders: envFlagEnabled(process.env.AUTH_DEV_HEADERS),
111
- tokenDefaultAbilities: ["*"]
111
+ tokenDefaultAbilities: []
112
112
  };
113
113
 
114
114
  // ../../src/bootstrap/config.ts
@@ -144,7 +144,7 @@ var authProvider = {
144
144
  config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
145
145
  const apiGuard = new DatabaseTokenGuard(container);
146
146
  const sessionGuard = new SessionGuard(container);
147
- const jwtGuard = new JwtGuard;
147
+ const jwtGuard = new JwtGuard(container);
148
148
  const basicGuard = new BasicAuthGuard(container);
149
149
  const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
150
150
  if (authConfig.allowDevHeaders) {
@@ -189,7 +189,7 @@ import { validateEnv } from "@getstrata/core/config/envSchema";
189
189
  var appConfig = {
190
190
  name: process.env.APP_NAME?.trim() || "Strata",
191
191
  env: process.env.APP_ENV ?? "local",
192
- debug: (process.env.APP_DEBUG ?? "true") !== "false",
192
+ debug: (process.env.APP_DEBUG ?? "false") === "true",
193
193
  url: process.env.APP_URL ?? "http://localhost:3000",
194
194
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
195
195
  };
@@ -3,6 +3,7 @@
3
3
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
4
4
  import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
5
5
  import { jsonResponse } from "@getstrata/core/http/response";
6
+ import { envFlagEnabled } from "@getstrata/core/runtime/appEnv";
6
7
  var {RedisClient } = globalThis.Bun;
7
8
 
8
9
  // ../../src/bootstrap/config.ts
@@ -114,10 +115,10 @@ function createHealthRoutes(dependencies, options = {}) {
114
115
  "/ready": async () => {
115
116
  const extra = await resolveExtraFields(options.extra);
116
117
  const { checks, ready } = await collectDependencyChecks(dependencies);
118
+ const debug = envFlagEnabled(process.env.APP_DEBUG);
117
119
  return jsonResponse({
118
120
  status: ready ? "ready" : "not_ready",
119
- checks,
120
- ...extra
121
+ ...debug ? { checks, ...extra } : {}
121
122
  }, { status: ready ? 200 : 503 });
122
123
  }
123
124
  };
@@ -134,8 +134,9 @@ class HttpKernel {
134
134
  case "web":
135
135
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
136
136
  case "api": {
137
+ const csrf = createCsrfMiddleware();
137
138
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
138
- return [];
139
+ return [csrf];
139
140
  }
140
141
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
141
142
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -145,7 +146,8 @@ class HttpKernel {
145
146
  createMemoryThrottleMiddleware({
146
147
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
147
148
  decaySeconds: 60
148
- })
149
+ }),
150
+ csrf
149
151
  ];
150
152
  }
151
153
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -154,7 +156,8 @@ class HttpKernel {
154
156
  redisUrl,
155
157
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
156
158
  decaySeconds: 60
157
- })
159
+ }),
160
+ csrf
158
161
  ];
159
162
  }
160
163
  default:
@@ -164,13 +167,14 @@ class HttpKernel {
164
167
  wrap(groups, handler) {
165
168
  const names = Array.isArray(groups) ? groups : [groups];
166
169
  const middleware = names.flatMap((name) => this.group(name));
167
- if (middleware.length === 0) {
168
- return handler;
170
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
171
+ if (names.includes("api")) {
172
+ return withJsonErrorHandling(wrapped);
169
173
  }
170
- return withMiddleware(...middleware)(handler);
174
+ return wrapped;
171
175
  }
172
176
  wrapApi(handler) {
173
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
177
+ return this.wrap(["api", "authenticated"], handler);
174
178
  }
175
179
  wrapWeb(handler) {
176
180
  return withErrorHandling(this.wrap("web", handler));
@@ -279,41 +283,28 @@ class HttpKernel {
279
283
  return [createRequireVerifiedMiddleware(auth)];
280
284
  }
281
285
  wrapThrottle(scope, rateLimit, handler) {
282
- const middleware = [];
283
286
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
284
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
285
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
286
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
287
- if (redisUrl) {
288
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
289
- redisUrl,
290
- maxAttempts: rateLimit.maxAttempts,
291
- decaySeconds: rateLimit.decaySeconds
292
- }) : createThrottleMiddleware({
293
- redisUrl,
294
- maxAttempts: rateLimit.maxAttempts,
295
- decaySeconds: rateLimit.decaySeconds,
296
- keyPrefix: memoryKeyPrefix
297
- });
298
- middleware.push(throttle);
299
- } else {
300
- middleware.push(createMemoryThrottleMiddleware({
301
- maxAttempts: rateLimit.maxAttempts,
302
- decaySeconds: rateLimit.decaySeconds,
303
- keyPrefix: memoryKeyPrefix
304
- }));
305
- }
306
- } else {
307
- middleware.push(createMemoryThrottleMiddleware({
287
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
288
+ if (scope === "login") {
289
+ return withMiddleware(createLoginThrottleMiddleware({
290
+ ...redisUrl ? { redisUrl } : {},
291
+ maxAttempts: rateLimit.maxAttempts,
292
+ decaySeconds: rateLimit.decaySeconds
293
+ }))(handler);
294
+ }
295
+ if (redisUrl) {
296
+ return withMiddleware(createThrottleMiddleware({
297
+ redisUrl,
308
298
  maxAttempts: rateLimit.maxAttempts,
309
299
  decaySeconds: rateLimit.decaySeconds,
310
300
  keyPrefix: memoryKeyPrefix
311
- }));
312
- }
313
- if (middleware.length === 0) {
314
- return handler;
301
+ }))(handler);
315
302
  }
316
- return withMiddleware(...middleware)(handler);
303
+ return withMiddleware(createMemoryThrottleMiddleware({
304
+ maxAttempts: rateLimit.maxAttempts,
305
+ decaySeconds: rateLimit.decaySeconds,
306
+ keyPrefix: memoryKeyPrefix
307
+ }))(handler);
317
308
  }
318
309
  }
319
310
  function createHttpKernel(dependencies) {