@getstrata/bootstrap 1.0.9 → 1.1.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.
@@ -0,0 +1,54 @@
1
+ // @bun
2
+ // ../../src/bootstrap/discoverListeners.ts
3
+ import { existsSync, readdirSync } from "fs";
4
+ import { createRequire } from "module";
5
+ import { join } from "path";
6
+ var requireListener = createRequire(import.meta.url);
7
+ var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
8
+ function readDiscoverListenersState() {
9
+ const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
10
+ if (existing) {
11
+ return existing;
12
+ }
13
+ const state = {};
14
+ globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
15
+ return state;
16
+ }
17
+ function resolveListenersDirectory() {
18
+ const fromCwd = join(process.cwd(), "src", "listeners");
19
+ if (existsSync(fromCwd)) {
20
+ return fromCwd;
21
+ }
22
+ return join(import.meta.dir, "../listeners");
23
+ }
24
+ function loadDiscoveredListeners() {
25
+ const listenersDirectory = resolveListenersDirectory();
26
+ let entries;
27
+ try {
28
+ entries = readdirSync(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.(ts|js)$/.test(entry.name)).map((entry) => entry.name);
29
+ } catch (error) {
30
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
31
+ return [];
32
+ }
33
+ throw error;
34
+ }
35
+ const listeners = entries.map((fileName) => {
36
+ const filePath = join(listenersDirectory, fileName);
37
+ const loaded = requireListener(filePath);
38
+ return loaded.default;
39
+ });
40
+ return listeners.filter((listener) => typeof listener === "function");
41
+ }
42
+ function discoverListeners() {
43
+ const state = readDiscoverListenersState();
44
+ state.appListeners ??= loadDiscoveredListeners();
45
+ return state.appListeners;
46
+ }
47
+ function resetDiscoverListenersForTests() {
48
+ const state = readDiscoverListenersState();
49
+ state.appListeners = undefined;
50
+ }
51
+ export {
52
+ discoverListeners,
53
+ resetDiscoverListenersForTests
54
+ };
@@ -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) {
@@ -2,6 +2,7 @@
2
2
  // ../../src/bootstrap/metricsRoutes.ts
3
3
  import { timingSafeEqual } from "crypto";
4
4
  import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
5
+ import { isProductionEnv } from "@getstrata/core/runtime/appEnv";
5
6
  function tokensMatch(left, right) {
6
7
  const leftBuffer = Buffer.from(left);
7
8
  const rightBuffer = Buffer.from(right);
@@ -17,7 +18,7 @@ function authorizeMetrics(request) {
17
18
  if (expected) {
18
19
  return presented.length > 0 && tokensMatch(presented, expected);
19
20
  }
20
- if ((process.env.APP_ENV ?? "local") === "production") {
21
+ if (isProductionEnv()) {
21
22
  return false;
22
23
  }
23
24
  return true;
@@ -14,7 +14,7 @@ import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
14
14
  import { envFlagEnabled } from "@getstrata/core/runtime/appEnv";
15
15
  var authConfig = {
16
16
  allowDevHeaders: envFlagEnabled(process.env.AUTH_DEV_HEADERS),
17
- tokenDefaultAbilities: ["*"]
17
+ tokenDefaultAbilities: []
18
18
  };
19
19
 
20
20
  // ../../src/bootstrap/config.ts
@@ -50,7 +50,7 @@ var authProvider = {
50
50
  config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
51
51
  const apiGuard = new DatabaseTokenGuard(container);
52
52
  const sessionGuard = new SessionGuard(container);
53
- const jwtGuard = new JwtGuard;
53
+ const jwtGuard = new JwtGuard(container);
54
54
  const basicGuard = new BasicAuthGuard(container);
55
55
  const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
56
56
  if (authConfig.allowDevHeaders) {
@@ -95,7 +95,7 @@ import { validateEnv } from "@getstrata/core/config/envSchema";
95
95
  var appConfig = {
96
96
  name: process.env.APP_NAME?.trim() || "Strata",
97
97
  env: process.env.APP_ENV ?? "local",
98
- debug: (process.env.APP_DEBUG ?? "true") !== "false",
98
+ debug: (process.env.APP_DEBUG ?? "false") === "true",
99
99
  url: process.env.APP_URL ?? "http://localhost:3000",
100
100
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
101
101
  };
@@ -276,8 +276,19 @@ var events_default = eventsProvider;
276
276
 
277
277
  // ../../src/bootstrap/discoverListeners.ts
278
278
  import { existsSync, readdirSync } from "fs";
279
+ import { createRequire } from "module";
279
280
  import { join } from "path";
280
- import { pathToFileURL } from "url";
281
+ var requireListener = createRequire(import.meta.url);
282
+ var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
283
+ function readDiscoverListenersState() {
284
+ const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
285
+ if (existing) {
286
+ return existing;
287
+ }
288
+ const state = {};
289
+ globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
290
+ return state;
291
+ }
281
292
  function resolveListenersDirectory() {
282
293
  const fromCwd = join(process.cwd(), "src", "listeners");
283
294
  if (existsSync(fromCwd)) {
@@ -285,7 +296,7 @@ function resolveListenersDirectory() {
285
296
  }
286
297
  return join(import.meta.dir, "../listeners");
287
298
  }
288
- async function loadDiscoveredListeners() {
299
+ function loadDiscoveredListeners() {
289
300
  const listenersDirectory = resolveListenersDirectory();
290
301
  let entries;
291
302
  try {
@@ -296,16 +307,21 @@ async function loadDiscoveredListeners() {
296
307
  }
297
308
  throw error;
298
309
  }
299
- const listeners = await Promise.all(entries.map(async (fileName) => {
300
- const moduleUrl = pathToFileURL(join(listenersDirectory, fileName)).href;
301
- const loaded = await import(moduleUrl);
310
+ const listeners = entries.map((fileName) => {
311
+ const filePath = join(listenersDirectory, fileName);
312
+ const loaded = requireListener(filePath);
302
313
  return loaded.default;
303
- }));
314
+ });
304
315
  return listeners.filter((listener) => typeof listener === "function");
305
316
  }
306
- var appListeners = await loadDiscoveredListeners();
307
317
  function discoverListeners() {
308
- return appListeners;
318
+ const state = readDiscoverListenersState();
319
+ state.appListeners ??= loadDiscoveredListeners();
320
+ return state.appListeners;
321
+ }
322
+ function resetDiscoverListenersForTests() {
323
+ const state = readDiscoverListenersState();
324
+ state.appListeners = undefined;
309
325
  }
310
326
 
311
327
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
@@ -329,7 +345,7 @@ import {
329
345
  // ../../src/bootstrap/discoverModules.ts
330
346
  import { readdirSync as readdirSync2 } from "fs";
331
347
  import { join as join2 } from "path";
332
- import { pathToFileURL as pathToFileURL2 } from "url";
348
+ import { pathToFileURL } from "url";
333
349
  var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
334
350
  function readDiscoverModulesState() {
335
351
  const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
@@ -370,7 +386,7 @@ async function loadDiscoveredModules(options) {
370
386
  throw error;
371
387
  }
372
388
  const modules = await Promise.all(moduleNames.map(async (moduleName) => {
373
- const moduleUrl = pathToFileURL2(join2(modulesDirectory, moduleName, "index.ts")).href;
389
+ const moduleUrl = pathToFileURL(join2(modulesDirectory, moduleName, "index.ts")).href;
374
390
  const loaded = await import(moduleUrl);
375
391
  return loaded.default;
376
392
  }));
@@ -14,8 +14,8 @@ function readFeatureFlags() {
14
14
  samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
15
15
  scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
16
16
  billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
17
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
18
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
17
+ siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "false") === "true",
18
+ publicReads: (process.env.FEATURE_PUBLIC_READS ?? "false") === "true",
19
19
  emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
20
20
  mfa: (process.env.FEATURE_MFA ?? "false") === "true",
21
21
  registration: (process.env.FEATURE_REGISTRATION ?? "true") !== "false"
@@ -1,7 +1,15 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/secretsGuard.ts
3
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
3
4
  import { envFlagEnabled, isProductionEnv } from "@getstrata/core/runtime/appEnv";
4
5
  import { isViewsMode, parseFrontendMode } from "@getstrata/core/runtime/frontendMode";
6
+ import {
7
+ assertPostgresRoleCannotBypassRls,
8
+ inspectCurrentPostgresRole,
9
+ isPostgresUrl,
10
+ postgresUrlUsername
11
+ } from "@getstrata/core/tenant/enableTenantRls";
12
+ import { isRlsTenancy } from "@getstrata/core/tenant/tenancyConfig";
5
13
  var PUBLISHED_TEST_ADMIN_API_TOKEN = "strata-admin-test-token";
6
14
  var PUBLISHED_TEST_MEMBER_API_TOKEN = "strata-member-test-token";
7
15
  var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "strata-scim-test-token";
@@ -22,8 +30,11 @@ var SECRETS_TO_ROTATE = [
22
30
  "KMS_ENCRYPTION_KEY",
23
31
  "STRIPE_WEBHOOK_SECRET",
24
32
  "ADMIN_API_TOKEN",
25
- "MEMBER_API_TOKEN"
33
+ "MEMBER_API_TOKEN",
34
+ "DATABASE_URL",
35
+ "APP_DATABASE_URL"
26
36
  ];
37
+ var RLS_BYPASS_DATABASE_USERS = new Set(["postgres", "root"]);
27
38
  function assertNoPlaceholderSecrets(env) {
28
39
  const unrotated = SECRETS_TO_ROTATE.filter((name) => PLACEHOLDER_SECRET_PATTERN.test(env[name]?.trim() ?? ""));
29
40
  if (unrotated.length > 0) {
@@ -34,7 +45,11 @@ function isTokenAuthEnabled(env) {
34
45
  return Boolean(env.ADMIN_API_TOKEN?.trim()) || Boolean(env.MEMBER_API_TOKEN?.trim()) || envFlagEnabled(env.FEATURE_API_TOKENS);
35
46
  }
36
47
  function isOAuthEnabled(env) {
37
- return envFlagEnabled(env.FEATURE_OAUTH) || envFlagEnabled(env.FEATURE_SAML) || Boolean(env.GITHUB_CLIENT_ID?.trim()) || Boolean(env.OIDC_ISSUER?.trim()) || Boolean(env.SAML_LOGIN_URL?.trim());
48
+ return envFlagEnabled(env.FEATURE_OAUTH) || envFlagEnabled(env.FEATURE_SAML) || Boolean(env.GITHUB_CLIENT_ID?.trim()) || Boolean(env.OIDC_ISSUER?.trim()) || Boolean(env.SAML_IDP_SSO_URL?.trim()) || Boolean(env.SAML_LOGIN_URL?.trim());
49
+ }
50
+ function isHeaderOnlyAuth(env) {
51
+ const frontend = env.FRONTEND_MODE?.trim() ?? "";
52
+ return env.AUTH_MODE === "headers" || frontend === "api" && env.AUTH_DEV_HEADERS === "true";
38
53
  }
39
54
  function isCorsConfigured(env) {
40
55
  return env.CORS_ALLOWED_ORIGINS !== undefined;
@@ -77,8 +92,27 @@ function assertFeatureProductionSecrets(env) {
77
92
  throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from published test defaults.");
78
93
  }
79
94
  }
80
- if (envFlagEnabled(env.FEATURE_FIELD_ENCRYPTION) && !env.KMS_ENCRYPTION_KEY?.trim()) {
81
- throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
95
+ if ((envFlagEnabled(env.FEATURE_FIELD_ENCRYPTION) || envFlagEnabled(env.FEATURE_MFA)) && !env.KMS_ENCRYPTION_KEY?.trim()) {
96
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption or MFA is enabled.");
97
+ }
98
+ if (envFlagEnabled(env.FEATURE_SAML)) {
99
+ const required = [
100
+ "SAML_IDP_CERT",
101
+ "SAML_IDP_SSO_URL",
102
+ "SAML_SP_ENTITY_ID",
103
+ "SAML_ACS_URL",
104
+ "SAML_IDP_ISSUER"
105
+ ];
106
+ const missing = required.filter((name) => !env[name]?.trim());
107
+ if (missing.length > 0) {
108
+ throw new Error(`Production startup blocked: set ${missing.join(", ")} when FEATURE_SAML=true.`);
109
+ }
110
+ if (env.SAML_WANT_RESPONSE_SIGNED === "false") {
111
+ throw new Error("Production startup blocked: signed SAML responses are required (do not set SAML_WANT_RESPONSE_SIGNED=false).");
112
+ }
113
+ }
114
+ if (envFlagEnabled(env.UPLOAD_ALLOW_UNKNOWN_MIME)) {
115
+ throw new Error("Production startup blocked: set UPLOAD_ALLOW_UNKNOWN_MIME=false (unknown MIME types are not allowed).");
82
116
  }
83
117
  if (envFlagEnabled(env.FEATURE_BILLING) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
84
118
  throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
@@ -100,6 +134,61 @@ function assertFeatureProductionSecrets(env) {
100
134
  }
101
135
  }
102
136
  var LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
137
+ var livePostgresRoleInspector = null;
138
+ function setLivePostgresRoleInspectorForTests(inspect) {
139
+ livePostgresRoleInspector = inspect;
140
+ }
141
+ function rlsRuntimeDatabaseUrl(env) {
142
+ const app = env.APP_DATABASE_URL?.trim();
143
+ if (app) {
144
+ return { name: "APP_DATABASE_URL", raw: app };
145
+ }
146
+ const database = env.DATABASE_URL?.trim();
147
+ if (database) {
148
+ return { name: "DATABASE_URL", raw: database };
149
+ }
150
+ return null;
151
+ }
152
+ function assertRlsUsesAppDatabaseRole(env) {
153
+ if (!isRlsTenancy(env)) {
154
+ return;
155
+ }
156
+ const runtime = rlsRuntimeDatabaseUrl(env);
157
+ if (!runtime) {
158
+ return;
159
+ }
160
+ const user = postgresUrlUsername(runtime.raw);
161
+ if (user === null) {
162
+ return;
163
+ }
164
+ if (user === "") {
165
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: ${runtime.name} must include a NOBYPASSRLS role username.`);
166
+ }
167
+ if (RLS_BYPASS_DATABASE_USERS.has(user.toLowerCase())) {
168
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: ${runtime.name} for TENANCY_DRIVER=rls must use a NOBYPASSRLS role, not ${user}. FORCE RLS does not apply to PostgreSQL superusers.`);
169
+ }
170
+ }
171
+ async function defaultInspectLivePostgresRole() {
172
+ return await inspectCurrentPostgresRole(getDefaultDatabasePool());
173
+ }
174
+ async function assertRlsLiveDatabaseRole(env = process.env, inspect) {
175
+ if (!isRlsTenancy(env)) {
176
+ return;
177
+ }
178
+ const runtime = rlsRuntimeDatabaseUrl(env);
179
+ if (!runtime || !isPostgresUrl(runtime.raw)) {
180
+ return;
181
+ }
182
+ const source = runtime.name;
183
+ assertRlsUsesAppDatabaseRole(env);
184
+ let role;
185
+ try {
186
+ role = await (inspect ?? livePostgresRoleInspector ?? defaultInspectLivePostgresRole)();
187
+ } catch {
188
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: could not inspect the live Postgres role for ${source}.`);
189
+ }
190
+ assertPostgresRoleCannotBypassRls(role, source);
191
+ }
103
192
  function assertPublicAppUrl(env) {
104
193
  const raw = env.APP_URL?.trim() ?? "";
105
194
  let url = null;
@@ -122,6 +211,9 @@ function assertProductionSecrets(env = process.env) {
122
211
  assertNoPlaceholderSecrets(env);
123
212
  assertAuthDevHeadersDisabled(env);
124
213
  assertPublicAppUrl(env);
214
+ if (isHeaderOnlyAuth(env)) {
215
+ throw new Error("Production startup blocked: header-only authentication is not allowed. Configure cookie, token, or JWT auth.");
216
+ }
125
217
  if (isTokenAuthEnabled(env)) {
126
218
  assertTokenAuthProductionSecrets(env);
127
219
  }
@@ -129,7 +221,10 @@ function assertProductionSecrets(env = process.env) {
129
221
  if (isViewsMode(parseFrontendMode(env.FRONTEND_MODE))) {
130
222
  assertSessionSecret(env);
131
223
  }
224
+ assertRlsUsesAppDatabaseRole(env);
132
225
  }
133
226
  export {
134
- assertProductionSecrets
227
+ assertProductionSecrets,
228
+ assertRlsLiveDatabaseRole,
229
+ setLivePostgresRoleInspectorForTests
135
230
  };
@@ -144,8 +144,9 @@ class HttpKernel {
144
144
  case "web":
145
145
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
146
146
  case "api": {
147
+ const csrf = createCsrfMiddleware();
147
148
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
148
- return [];
149
+ return [csrf];
149
150
  }
150
151
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
151
152
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -155,7 +156,8 @@ class HttpKernel {
155
156
  createMemoryThrottleMiddleware({
156
157
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
157
158
  decaySeconds: 60
158
- })
159
+ }),
160
+ csrf
159
161
  ];
160
162
  }
161
163
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -164,7 +166,8 @@ class HttpKernel {
164
166
  redisUrl,
165
167
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
166
168
  decaySeconds: 60
167
- })
169
+ }),
170
+ csrf
168
171
  ];
169
172
  }
170
173
  default:
@@ -174,13 +177,14 @@ class HttpKernel {
174
177
  wrap(groups, handler) {
175
178
  const names = Array.isArray(groups) ? groups : [groups];
176
179
  const middleware = names.flatMap((name) => this.group(name));
177
- if (middleware.length === 0) {
178
- return handler;
180
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
181
+ if (names.includes("api")) {
182
+ return withJsonErrorHandling(wrapped);
179
183
  }
180
- return withMiddleware(...middleware)(handler);
184
+ return wrapped;
181
185
  }
182
186
  wrapApi(handler) {
183
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
187
+ return this.wrap(["api", "authenticated"], handler);
184
188
  }
185
189
  wrapWeb(handler) {
186
190
  return withErrorHandling(this.wrap("web", handler));
@@ -289,41 +293,28 @@ class HttpKernel {
289
293
  return [createRequireVerifiedMiddleware(auth)];
290
294
  }
291
295
  wrapThrottle(scope, rateLimit, handler) {
292
- const middleware = [];
293
296
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
294
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
295
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
296
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
297
- if (redisUrl) {
298
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
299
- redisUrl,
300
- maxAttempts: rateLimit.maxAttempts,
301
- decaySeconds: rateLimit.decaySeconds
302
- }) : createThrottleMiddleware({
303
- redisUrl,
304
- maxAttempts: rateLimit.maxAttempts,
305
- decaySeconds: rateLimit.decaySeconds,
306
- keyPrefix: memoryKeyPrefix
307
- });
308
- middleware.push(throttle);
309
- } else {
310
- middleware.push(createMemoryThrottleMiddleware({
311
- maxAttempts: rateLimit.maxAttempts,
312
- decaySeconds: rateLimit.decaySeconds,
313
- keyPrefix: memoryKeyPrefix
314
- }));
315
- }
316
- } else {
317
- middleware.push(createMemoryThrottleMiddleware({
297
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
298
+ if (scope === "login") {
299
+ return withMiddleware(createLoginThrottleMiddleware({
300
+ ...redisUrl ? { redisUrl } : {},
301
+ maxAttempts: rateLimit.maxAttempts,
302
+ decaySeconds: rateLimit.decaySeconds
303
+ }))(handler);
304
+ }
305
+ if (redisUrl) {
306
+ return withMiddleware(createThrottleMiddleware({
307
+ redisUrl,
318
308
  maxAttempts: rateLimit.maxAttempts,
319
309
  decaySeconds: rateLimit.decaySeconds,
320
310
  keyPrefix: memoryKeyPrefix
321
- }));
322
- }
323
- if (middleware.length === 0) {
324
- return handler;
311
+ }))(handler);
325
312
  }
326
- return withMiddleware(...middleware)(handler);
313
+ return withMiddleware(createMemoryThrottleMiddleware({
314
+ maxAttempts: rateLimit.maxAttempts,
315
+ decaySeconds: rateLimit.decaySeconds,
316
+ keyPrefix: memoryKeyPrefix
317
+ }))(handler);
327
318
  }
328
319
  }
329
320
  function createHttpKernel(dependencies) {
@@ -1,6 +1,7 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/web/server.ts
3
3
  import { currentRequestMeta, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
4
+ import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
4
5
  import { notFoundHtmlResponse } from "@getstrata/core/view";
5
6
  function socketAddress(server, request) {
6
7
  const address = server?.requestIP(request)?.address;
@@ -60,9 +61,14 @@ function createWebServer(options) {
60
61
  await options.onRequest?.(request);
61
62
  const url = new URL(request.url);
62
63
  if (url.pathname.startsWith("/assets/")) {
63
- const file = Bun.file(`${publicDir}${url.pathname}`);
64
- if (await file.exists()) {
65
- return new Response(file);
64
+ try {
65
+ const filePath = assertUrlPathUnderRoot(publicDir, url.pathname);
66
+ const file = Bun.file(filePath);
67
+ if (await file.exists()) {
68
+ return new Response(file);
69
+ }
70
+ } catch {
71
+ return await missingHtmlResponse();
66
72
  }
67
73
  }
68
74
  if (options.handle) {