@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.
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  // ../../src/bootstrap/buildModuleRoutes.ts
18
18
  import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
19
19
  import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
20
+ import { createJsonErrorMiddleware } from "@getstrata/core/http/response";
20
21
 
21
22
  // ../../src/bootstrap/httpKernel.ts
22
23
  import {
@@ -152,8 +153,9 @@ class HttpKernel {
152
153
  case "web":
153
154
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
154
155
  case "api": {
156
+ const csrf = createCsrfMiddleware();
155
157
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
156
- return [];
158
+ return [csrf];
157
159
  }
158
160
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
159
161
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -163,7 +165,8 @@ class HttpKernel {
163
165
  createMemoryThrottleMiddleware({
164
166
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
165
167
  decaySeconds: 60
166
- })
168
+ }),
169
+ csrf
167
170
  ];
168
171
  }
169
172
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -172,7 +175,8 @@ class HttpKernel {
172
175
  redisUrl,
173
176
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
174
177
  decaySeconds: 60
175
- })
178
+ }),
179
+ csrf
176
180
  ];
177
181
  }
178
182
  default:
@@ -182,13 +186,14 @@ class HttpKernel {
182
186
  wrap(groups, handler) {
183
187
  const names = Array.isArray(groups) ? groups : [groups];
184
188
  const middleware = names.flatMap((name) => this.group(name));
185
- if (middleware.length === 0) {
186
- return handler;
189
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
190
+ if (names.includes("api")) {
191
+ return withJsonErrorHandling(wrapped);
187
192
  }
188
- return withMiddleware(...middleware)(handler);
193
+ return wrapped;
189
194
  }
190
195
  wrapApi(handler) {
191
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
196
+ return this.wrap(["api", "authenticated"], handler);
192
197
  }
193
198
  wrapWeb(handler) {
194
199
  return withErrorHandling(this.wrap("web", handler));
@@ -297,41 +302,28 @@ class HttpKernel {
297
302
  return [createRequireVerifiedMiddleware(auth)];
298
303
  }
299
304
  wrapThrottle(scope, rateLimit, handler) {
300
- const middleware = [];
301
305
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
302
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
303
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
304
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
305
- if (redisUrl) {
306
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
307
- redisUrl,
308
- maxAttempts: rateLimit.maxAttempts,
309
- decaySeconds: rateLimit.decaySeconds
310
- }) : createThrottleMiddleware({
311
- redisUrl,
312
- maxAttempts: rateLimit.maxAttempts,
313
- decaySeconds: rateLimit.decaySeconds,
314
- keyPrefix: memoryKeyPrefix
315
- });
316
- middleware.push(throttle);
317
- } else {
318
- middleware.push(createMemoryThrottleMiddleware({
319
- maxAttempts: rateLimit.maxAttempts,
320
- decaySeconds: rateLimit.decaySeconds,
321
- keyPrefix: memoryKeyPrefix
322
- }));
323
- }
324
- } else {
325
- middleware.push(createMemoryThrottleMiddleware({
306
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
307
+ if (scope === "login") {
308
+ return withMiddleware(createLoginThrottleMiddleware({
309
+ ...redisUrl ? { redisUrl } : {},
310
+ maxAttempts: rateLimit.maxAttempts,
311
+ decaySeconds: rateLimit.decaySeconds
312
+ }))(handler);
313
+ }
314
+ if (redisUrl) {
315
+ return withMiddleware(createThrottleMiddleware({
316
+ redisUrl,
326
317
  maxAttempts: rateLimit.maxAttempts,
327
318
  decaySeconds: rateLimit.decaySeconds,
328
319
  keyPrefix: memoryKeyPrefix
329
- }));
330
- }
331
- if (middleware.length === 0) {
332
- return handler;
320
+ }))(handler);
333
321
  }
334
- return withMiddleware(...middleware)(handler);
322
+ return withMiddleware(createMemoryThrottleMiddleware({
323
+ maxAttempts: rateLimit.maxAttempts,
324
+ decaySeconds: rateLimit.decaySeconds,
325
+ keyPrefix: memoryKeyPrefix
326
+ }))(handler);
335
327
  }
336
328
  }
337
329
  function createHttpKernel(dependencies) {
@@ -470,7 +462,11 @@ function buildModuleRoutes(dependencies, options = {}) {
470
462
  routeRegistry.clear();
471
463
  }
472
464
  const kernel = createHttpKernel(dependencies);
473
- const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
465
+ const middleware = [
466
+ ...kernel.globalMiddleware(),
467
+ createJsonErrorMiddleware(),
468
+ ...kernel.group("api")
469
+ ];
474
470
  const cachedJson = createCachedJson(dependencies);
475
471
  const moduleRoutes = {};
476
472
  for (const module of modules) {
@@ -541,7 +537,7 @@ import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
541
537
  import { envFlagEnabled } from "@getstrata/core/runtime/appEnv";
542
538
  var authConfig = {
543
539
  allowDevHeaders: envFlagEnabled(process.env.AUTH_DEV_HEADERS),
544
- tokenDefaultAbilities: ["*"]
540
+ tokenDefaultAbilities: []
545
541
  };
546
542
 
547
543
  // ../../src/bootstrap/providers/auth.ts
@@ -551,7 +547,7 @@ var authProvider = {
551
547
  config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
552
548
  const apiGuard = new DatabaseTokenGuard(container);
553
549
  const sessionGuard = new SessionGuard(container);
554
- const jwtGuard = new JwtGuard;
550
+ const jwtGuard = new JwtGuard(container);
555
551
  const basicGuard = new BasicAuthGuard(container);
556
552
  const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
557
553
  if (authConfig.allowDevHeaders) {
@@ -596,7 +592,7 @@ import { validateEnv } from "@getstrata/core/config/envSchema";
596
592
  var appConfig = {
597
593
  name: process.env.APP_NAME?.trim() || "Strata",
598
594
  env: process.env.APP_ENV ?? "local",
599
- debug: (process.env.APP_DEBUG ?? "true") !== "false",
595
+ debug: (process.env.APP_DEBUG ?? "false") === "true",
600
596
  url: process.env.APP_URL ?? "http://localhost:3000",
601
597
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
602
598
  };
@@ -777,8 +773,19 @@ var events_default = eventsProvider;
777
773
 
778
774
  // ../../src/bootstrap/discoverListeners.ts
779
775
  import { existsSync, readdirSync as readdirSync2 } from "fs";
776
+ import { createRequire } from "module";
780
777
  import { join as join2 } from "path";
781
- import { pathToFileURL as pathToFileURL2 } from "url";
778
+ var requireListener = createRequire(import.meta.url);
779
+ var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
780
+ function readDiscoverListenersState() {
781
+ const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
782
+ if (existing) {
783
+ return existing;
784
+ }
785
+ const state = {};
786
+ globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
787
+ return state;
788
+ }
782
789
  function resolveListenersDirectory() {
783
790
  const fromCwd = join2(process.cwd(), "src", "listeners");
784
791
  if (existsSync(fromCwd)) {
@@ -786,7 +793,7 @@ function resolveListenersDirectory() {
786
793
  }
787
794
  return join2(import.meta.dir, "../listeners");
788
795
  }
789
- async function loadDiscoveredListeners() {
796
+ function loadDiscoveredListeners() {
790
797
  const listenersDirectory = resolveListenersDirectory();
791
798
  let entries;
792
799
  try {
@@ -797,16 +804,17 @@ async function loadDiscoveredListeners() {
797
804
  }
798
805
  throw error;
799
806
  }
800
- const listeners = await Promise.all(entries.map(async (fileName) => {
801
- const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
802
- const loaded = await import(moduleUrl);
807
+ const listeners = entries.map((fileName) => {
808
+ const filePath = join2(listenersDirectory, fileName);
809
+ const loaded = requireListener(filePath);
803
810
  return loaded.default;
804
- }));
811
+ });
805
812
  return listeners.filter((listener) => typeof listener === "function");
806
813
  }
807
- var appListeners = await loadDiscoveredListeners();
808
814
  function discoverListeners() {
809
- return appListeners;
815
+ const state = readDiscoverListenersState();
816
+ state.appListeners ??= loadDiscoveredListeners();
817
+ return state.appListeners;
810
818
  }
811
819
 
812
820
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
@@ -1001,6 +1009,7 @@ function createAppContext() {
1001
1009
  // ../../src/bootstrap/createWebRoutes.ts
1002
1010
  import { join as join3 } from "path";
1003
1011
  import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
1012
+ import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
1004
1013
  import { notFoundHtmlResponse } from "@getstrata/core/view";
1005
1014
  function registerRoute(method, path, middleware) {
1006
1015
  routeRegistry.register({ method, path, middleware });
@@ -1014,12 +1023,16 @@ function createWebRoutes(dependencies, options = {}) {
1014
1023
  wrappedRoutes["/assets/*"] = async (request) => {
1015
1024
  registerRoute("GET", "/assets/*", ["global", "web"]);
1016
1025
  const pathname = new URL(request.url).pathname;
1017
- const relativePath = pathname.replace(/^\//, "");
1018
- const file = Bun.file(join3(process.cwd(), "public", relativePath));
1019
- if (!await file.exists()) {
1026
+ try {
1027
+ const filePath = assertUrlPathUnderRoot(join3(process.cwd(), "public"), pathname);
1028
+ const file = Bun.file(filePath);
1029
+ if (!await file.exists()) {
1030
+ return notFoundHtmlResponse();
1031
+ }
1032
+ return new Response(file);
1033
+ } catch {
1020
1034
  return notFoundHtmlResponse();
1021
1035
  }
1022
- return new Response(file);
1023
1036
  };
1024
1037
  registerRoute("GET", "/assets/*", ["global", "web"]);
1025
1038
  return wrappedRoutes;
@@ -1041,6 +1054,7 @@ function createAppDependencies() {
1041
1054
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
1042
1055
  import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
1043
1056
  import { jsonResponse } from "@getstrata/core/http/response";
1057
+ import { envFlagEnabled as envFlagEnabled2 } from "@getstrata/core/runtime/appEnv";
1044
1058
  var {RedisClient } = globalThis.Bun;
1045
1059
  function resolveRedisUrl(dependencies) {
1046
1060
  if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
@@ -1124,10 +1138,10 @@ function createHealthRoutes(dependencies, options = {}) {
1124
1138
  "/ready": async () => {
1125
1139
  const extra = await resolveExtraFields(options.extra);
1126
1140
  const { checks, ready } = await collectDependencyChecks(dependencies);
1141
+ const debug = envFlagEnabled2(process.env.APP_DEBUG);
1127
1142
  return jsonResponse({
1128
1143
  status: ready ? "ready" : "not_ready",
1129
- checks,
1130
- ...extra
1144
+ ...debug ? { checks, ...extra } : {}
1131
1145
  }, { status: ready ? 200 : 503 });
1132
1146
  }
1133
1147
  };
@@ -1140,8 +1154,16 @@ import {
1140
1154
  // ../../src/bootstrap/membershipService.ts
1141
1155
  import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
1142
1156
  // ../../src/bootstrap/secretsGuard.ts
1143
- import { envFlagEnabled as envFlagEnabled2, isProductionEnv } from "@getstrata/core/runtime/appEnv";
1157
+ import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
1158
+ import { envFlagEnabled as envFlagEnabled3, isProductionEnv } from "@getstrata/core/runtime/appEnv";
1144
1159
  import { isViewsMode, parseFrontendMode } from "@getstrata/core/runtime/frontendMode";
1160
+ import {
1161
+ assertPostgresRoleCannotBypassRls,
1162
+ inspectCurrentPostgresRole,
1163
+ isPostgresUrl,
1164
+ postgresUrlUsername
1165
+ } from "@getstrata/core/tenant/enableTenantRls";
1166
+ import { isRlsTenancy } from "@getstrata/core/tenant/tenancyConfig";
1145
1167
  var PUBLISHED_TEST_ADMIN_API_TOKEN = "strata-admin-test-token";
1146
1168
  var PUBLISHED_TEST_MEMBER_API_TOKEN = "strata-member-test-token";
1147
1169
  var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "strata-scim-test-token";
@@ -1162,8 +1184,11 @@ var SECRETS_TO_ROTATE = [
1162
1184
  "KMS_ENCRYPTION_KEY",
1163
1185
  "STRIPE_WEBHOOK_SECRET",
1164
1186
  "ADMIN_API_TOKEN",
1165
- "MEMBER_API_TOKEN"
1187
+ "MEMBER_API_TOKEN",
1188
+ "DATABASE_URL",
1189
+ "APP_DATABASE_URL"
1166
1190
  ];
1191
+ var RLS_BYPASS_DATABASE_USERS = new Set(["postgres", "root"]);
1167
1192
  function assertNoPlaceholderSecrets(env) {
1168
1193
  const unrotated = SECRETS_TO_ROTATE.filter((name) => PLACEHOLDER_SECRET_PATTERN.test(env[name]?.trim() ?? ""));
1169
1194
  if (unrotated.length > 0) {
@@ -1171,10 +1196,14 @@ function assertNoPlaceholderSecrets(env) {
1171
1196
  }
1172
1197
  }
1173
1198
  function isTokenAuthEnabled(env) {
1174
- return Boolean(env.ADMIN_API_TOKEN?.trim()) || Boolean(env.MEMBER_API_TOKEN?.trim()) || envFlagEnabled2(env.FEATURE_API_TOKENS);
1199
+ return Boolean(env.ADMIN_API_TOKEN?.trim()) || Boolean(env.MEMBER_API_TOKEN?.trim()) || envFlagEnabled3(env.FEATURE_API_TOKENS);
1175
1200
  }
1176
1201
  function isOAuthEnabled(env) {
1177
- return envFlagEnabled2(env.FEATURE_OAUTH) || envFlagEnabled2(env.FEATURE_SAML) || Boolean(env.GITHUB_CLIENT_ID?.trim()) || Boolean(env.OIDC_ISSUER?.trim()) || Boolean(env.SAML_LOGIN_URL?.trim());
1202
+ return envFlagEnabled3(env.FEATURE_OAUTH) || envFlagEnabled3(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());
1203
+ }
1204
+ function isHeaderOnlyAuth(env) {
1205
+ const frontend = env.FRONTEND_MODE?.trim() ?? "";
1206
+ return env.AUTH_MODE === "headers" || frontend === "api" && env.AUTH_DEV_HEADERS === "true";
1178
1207
  }
1179
1208
  function isCorsConfigured(env) {
1180
1209
  return env.CORS_ALLOWED_ORIGINS !== undefined;
@@ -1211,16 +1240,35 @@ function assertTokenAuthProductionSecrets(env) {
1211
1240
  }
1212
1241
  }
1213
1242
  function assertFeatureProductionSecrets(env) {
1214
- if (envFlagEnabled2(env.FEATURE_SCIM)) {
1243
+ if (envFlagEnabled3(env.FEATURE_SCIM)) {
1215
1244
  const scimToken = env.SCIM_BEARER_TOKEN ?? PUBLISHED_TEST_SCIM_BEARER_TOKEN;
1216
1245
  if (PUBLISHED_TEST_SCIM_TOKENS.has(scimToken) || !env.SCIM_BEARER_TOKEN?.trim()) {
1217
1246
  throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from published test defaults.");
1218
1247
  }
1219
1248
  }
1220
- if (envFlagEnabled2(env.FEATURE_FIELD_ENCRYPTION) && !env.KMS_ENCRYPTION_KEY?.trim()) {
1221
- throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
1249
+ if ((envFlagEnabled3(env.FEATURE_FIELD_ENCRYPTION) || envFlagEnabled3(env.FEATURE_MFA)) && !env.KMS_ENCRYPTION_KEY?.trim()) {
1250
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption or MFA is enabled.");
1222
1251
  }
1223
- if (envFlagEnabled2(env.FEATURE_BILLING) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
1252
+ if (envFlagEnabled3(env.FEATURE_SAML)) {
1253
+ const required = [
1254
+ "SAML_IDP_CERT",
1255
+ "SAML_IDP_SSO_URL",
1256
+ "SAML_SP_ENTITY_ID",
1257
+ "SAML_ACS_URL",
1258
+ "SAML_IDP_ISSUER"
1259
+ ];
1260
+ const missing = required.filter((name) => !env[name]?.trim());
1261
+ if (missing.length > 0) {
1262
+ throw new Error(`Production startup blocked: set ${missing.join(", ")} when FEATURE_SAML=true.`);
1263
+ }
1264
+ if (env.SAML_WANT_RESPONSE_SIGNED === "false") {
1265
+ throw new Error("Production startup blocked: signed SAML responses are required (do not set SAML_WANT_RESPONSE_SIGNED=false).");
1266
+ }
1267
+ }
1268
+ if (envFlagEnabled3(env.UPLOAD_ALLOW_UNKNOWN_MIME)) {
1269
+ throw new Error("Production startup blocked: set UPLOAD_ALLOW_UNKNOWN_MIME=false (unknown MIME types are not allowed).");
1270
+ }
1271
+ if (envFlagEnabled3(env.FEATURE_BILLING) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
1224
1272
  throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
1225
1273
  }
1226
1274
  if (isOAuthEnabled(env) && !env.OAUTH_STATE_SECRET?.trim()) {
@@ -1232,14 +1280,44 @@ function assertFeatureProductionSecrets(env) {
1232
1280
  throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
1233
1281
  }
1234
1282
  }
1235
- if (envFlagEnabled2(env.FEATURE_PUBLIC_READS)) {
1283
+ if (envFlagEnabled3(env.FEATURE_PUBLIC_READS)) {
1236
1284
  throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
1237
1285
  }
1238
- if (!env.SIEM_EXPORT_URL?.trim() && envFlagEnabled2(env.FEATURE_SIEM_EXPORT)) {
1286
+ if (!env.SIEM_EXPORT_URL?.trim() && envFlagEnabled3(env.FEATURE_SIEM_EXPORT)) {
1239
1287
  console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
1240
1288
  }
1241
1289
  }
1242
1290
  var LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
1291
+ function rlsRuntimeDatabaseUrl(env) {
1292
+ const app = env.APP_DATABASE_URL?.trim();
1293
+ if (app) {
1294
+ return { name: "APP_DATABASE_URL", raw: app };
1295
+ }
1296
+ const database = env.DATABASE_URL?.trim();
1297
+ if (database) {
1298
+ return { name: "DATABASE_URL", raw: database };
1299
+ }
1300
+ return null;
1301
+ }
1302
+ function assertRlsUsesAppDatabaseRole(env) {
1303
+ if (!isRlsTenancy(env)) {
1304
+ return;
1305
+ }
1306
+ const runtime = rlsRuntimeDatabaseUrl(env);
1307
+ if (!runtime) {
1308
+ return;
1309
+ }
1310
+ const user = postgresUrlUsername(runtime.raw);
1311
+ if (user === null) {
1312
+ return;
1313
+ }
1314
+ if (user === "") {
1315
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: ${runtime.name} must include a NOBYPASSRLS role username.`);
1316
+ }
1317
+ if (RLS_BYPASS_DATABASE_USERS.has(user.toLowerCase())) {
1318
+ 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.`);
1319
+ }
1320
+ }
1243
1321
  function assertPublicAppUrl(env) {
1244
1322
  const raw = env.APP_URL?.trim() ?? "";
1245
1323
  let url = null;
@@ -1262,6 +1340,9 @@ function assertProductionSecrets(env = process.env) {
1262
1340
  assertNoPlaceholderSecrets(env);
1263
1341
  assertAuthDevHeadersDisabled(env);
1264
1342
  assertPublicAppUrl(env);
1343
+ if (isHeaderOnlyAuth(env)) {
1344
+ throw new Error("Production startup blocked: header-only authentication is not allowed. Configure cookie, token, or JWT auth.");
1345
+ }
1265
1346
  if (isTokenAuthEnabled(env)) {
1266
1347
  assertTokenAuthProductionSecrets(env);
1267
1348
  }
@@ -1269,6 +1350,7 @@ function assertProductionSecrets(env = process.env) {
1269
1350
  if (isViewsMode(parseFrontendMode(env.FRONTEND_MODE))) {
1270
1351
  assertSessionSecret(env);
1271
1352
  }
1353
+ assertRlsUsesAppDatabaseRole(env);
1272
1354
  }
1273
1355
  // ../../src/bootstrap/web/forms.ts
1274
1356
  import {
@@ -1350,6 +1432,7 @@ function createRouteKernel(dependencies) {
1350
1432
  }
1351
1433
  // ../../src/bootstrap/web/server.ts
1352
1434
  import { currentRequestMeta as currentRequestMeta2, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
1435
+ import { assertUrlPathUnderRoot as assertUrlPathUnderRoot2 } from "@getstrata/core/security/safePath";
1353
1436
  import { notFoundHtmlResponse as notFoundHtmlResponse2 } from "@getstrata/core/view";
1354
1437
  function socketAddress(server, request) {
1355
1438
  const address = server?.requestIP(request)?.address;
@@ -1409,9 +1492,14 @@ function createWebServer(options) {
1409
1492
  await options.onRequest?.(request);
1410
1493
  const url = new URL(request.url);
1411
1494
  if (url.pathname.startsWith("/assets/")) {
1412
- const file = Bun.file(`${publicDir}${url.pathname}`);
1413
- if (await file.exists()) {
1414
- return new Response(file);
1495
+ try {
1496
+ const filePath = assertUrlPathUnderRoot2(publicDir, url.pathname);
1497
+ const file = Bun.file(filePath);
1498
+ if (await file.exists()) {
1499
+ return new Response(file);
1500
+ }
1501
+ } catch {
1502
+ return await missingHtmlResponse();
1415
1503
  }
1416
1504
  }
1417
1505
  if (options.handle) {
@@ -1426,13 +1514,22 @@ function createWebServer(options) {
1426
1514
  // ../../src/bootstrap/web/session.ts
1427
1515
  import { createHmac, randomBytes } from "crypto";
1428
1516
  import { AuthManager as AuthManager2 } from "@getstrata/core/auth/guard";
1517
+ import { isSessionInvalidated } from "@getstrata/core/auth/sessionCookie";
1429
1518
  import { getBoundDatabaseConnection as getBoundDatabaseConnection2 } from "@getstrata/core/database/boundConnection";
1430
- import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
1519
+ import {
1520
+ getActiveDatabaseConnection,
1521
+ hasActiveDatabaseConnection
1522
+ } from "@getstrata/core/database/connectionContext";
1523
+ import {
1524
+ getDefaultDatabasePool as getDefaultDatabasePool3,
1525
+ getDefaultDatabaseQuery
1526
+ } from "@getstrata/core/database/defaultConnection";
1431
1527
  import { currentSqlDialect, sqlTimestamp } from "@getstrata/core/database/dialect";
1432
1528
  import { readRequestCookie } from "@getstrata/core/http/cookies";
1433
1529
  import { currentRequestMeta as currentRequestMeta3 } from "@getstrata/core/http/requestMetaContext";
1434
1530
  import { isProductionEnv as isProductionEnv2 } from "@getstrata/core/runtime/appEnv";
1435
1531
  import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
1532
+ import { runWithMigrationBypassForIdentifier } from "@getstrata/core/tenant/databaseTenantContext";
1436
1533
  function sqlPlaceholder(index) {
1437
1534
  return currentSqlDialect().placeholder(index);
1438
1535
  }
@@ -1443,17 +1540,25 @@ function isSqlClient(value) {
1443
1540
  return typeof value.unsafe === "function";
1444
1541
  }
1445
1542
  function resolveSql(source) {
1446
- if (isSqlClient(source)) {
1447
- return source;
1543
+ const client = isSqlClient(source) ? source : source();
1544
+ if (!hasActiveDatabaseConnection()) {
1545
+ return client;
1546
+ }
1547
+ try {
1548
+ if (client === getDefaultDatabasePool3() || client === getDefaultDatabaseQuery()) {
1549
+ return getActiveDatabaseConnection(client);
1550
+ }
1551
+ } catch {
1552
+ return client;
1448
1553
  }
1449
- return source();
1554
+ return client;
1450
1555
  }
1451
1556
  function defaultSessionSql() {
1452
1557
  const bound = getBoundDatabaseConnection2();
1453
1558
  if (bound) {
1454
1559
  return bound;
1455
1560
  }
1456
- return getDefaultDatabasePool2();
1561
+ return getDefaultDatabasePool3();
1457
1562
  }
1458
1563
  function defaultMapSessionUser(user) {
1459
1564
  return {
@@ -1483,14 +1588,32 @@ function mapSessionUserRow(row) {
1483
1588
  };
1484
1589
  }
1485
1590
  async function defaultLoadSessionUser(sql, sessionId) {
1486
- const rows = await sql.unsafe(`SELECT s.user_id, s.expires_at, u.*
1487
- FROM sessions s
1488
- INNER JOIN users u ON u.id = s.user_id
1489
- WHERE s.id = ${sqlPlaceholder(1)} AND s.expires_at > ${sqlNow()}`, [sessionId]);
1490
- const row = rows[0];
1491
- if (!row)
1591
+ const sessionRows = await runWithMigrationBypassForIdentifier(sessionId, async () => {
1592
+ return await sql.unsafe(`SELECT user_id, expires_at, created_at AS session_created_at
1593
+ FROM sessions
1594
+ WHERE id = ${sqlPlaceholder(1)} AND expires_at > ${sqlNow()}`, [sessionId]);
1595
+ });
1596
+ const session = sessionRows[0];
1597
+ if (!session)
1598
+ return null;
1599
+ const userId = Number(session.user_id);
1600
+ if (!Number.isInteger(userId) || userId <= 0) {
1492
1601
  return null;
1493
- return mapSessionUserRow(row);
1602
+ }
1603
+ return await runWithMigrationBypassForIdentifier(userId, async () => {
1604
+ const rows = await sql.unsafe(`SELECT * FROM users WHERE id = ${sqlPlaceholder(1)}`, [
1605
+ userId
1606
+ ]);
1607
+ const row = rows[0];
1608
+ if (!row)
1609
+ return null;
1610
+ const createdSource = session.session_created_at;
1611
+ const createdAt = createdSource instanceof Date ? createdSource.getTime() : createdSource ? Date.parse(String(createdSource)) : Number.NaN;
1612
+ if (!Number.isFinite(createdAt) || isSessionInvalidated(createdAt, row.session_valid_after)) {
1613
+ return null;
1614
+ }
1615
+ return mapSessionUserRow({ ...row, user_id: userId, session_created_at: createdSource });
1616
+ });
1494
1617
  }
1495
1618
  function redirectWithCookie(location, setCookie, status) {
1496
1619
  return new Response(null, {
@@ -1545,25 +1668,40 @@ class CookieSessionStore {
1545
1668
  async create(user, meta = {}) {
1546
1669
  const id = randomBytes(32).toString("hex");
1547
1670
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
1548
- await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
1549
- VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, sqlTimestamp(expires), meta.userAgent ?? null, meta.ipAddress ?? null]);
1671
+ await runWithMigrationBypassForIdentifier(user.id, async () => {
1672
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at, created_at)
1673
+ VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()}, ${sqlNow()})`, [id, user.id, sqlTimestamp(expires), meta.userAgent ?? null, meta.ipAddress ?? null]);
1674
+ });
1550
1675
  return id;
1551
1676
  }
1552
1677
  async destroy(sessionId) {
1553
- await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
1678
+ await runWithMigrationBypassForIdentifier(sessionId, async () => {
1679
+ await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
1680
+ });
1681
+ }
1682
+ async destroyAllSessions(userId) {
1683
+ await runWithMigrationBypassForIdentifier(userId, async () => {
1684
+ await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)}`, [
1685
+ userId
1686
+ ]);
1687
+ });
1554
1688
  }
1555
1689
  async destroyOtherSessions(userId, keepSessionId) {
1556
- await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
1690
+ await runWithMigrationBypassForIdentifier(userId, async () => {
1691
+ await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
1692
+ });
1557
1693
  }
1558
1694
  async listForUser(userId) {
1559
1695
  const dialect = currentSqlDialect();
1560
- return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
1696
+ return await runWithMigrationBypassForIdentifier(userId, () => this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
1561
1697
  FROM sessions
1562
1698
  WHERE user_id = ${dialect.placeholder(1)} AND expires_at > ${dialect.nowExpression()}
1563
- ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]);
1699
+ ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]));
1564
1700
  }
1565
1701
  async touch(sessionId) {
1566
- await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
1702
+ await runWithMigrationBypassForIdentifier(sessionId, async () => {
1703
+ await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
1704
+ });
1567
1705
  }
1568
1706
  async read(request) {
1569
1707
  const sessionId = this.sessionIdFromRequest(request);