@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/CHANGELOG.md +8 -0
- package/dist/bootstrap/discoverListeners.d.ts +2 -2
- package/dist/bootstrap/secretsGuard.d.ts +13 -1
- package/dist/bootstrap/web/session.d.ts +1 -0
- package/dist/entries/buildModuleRoutes.js +34 -38
- package/dist/entries/buildWebModuleRoutes.js +34 -38
- package/dist/entries/context.js +27 -11
- package/dist/entries/createSpaRoutes.js +10 -3
- package/dist/entries/createWebRoutes.js +43 -42
- package/dist/entries/dependencies.js +27 -11
- package/dist/entries/discoverListeners.js +54 -0
- package/dist/entries/health.js +3 -2
- package/dist/entries/httpKernel.js +28 -37
- package/dist/entries/metricsRoutes.js +2 -1
- package/dist/entries/providers.js +29 -13
- package/dist/entries/schedule.js +2 -2
- package/dist/entries/secretsGuard.js +100 -5
- package/dist/entries/web/routing.js +28 -37
- package/dist/entries/web/server.js +9 -3
- package/dist/entries/web/session.js +68 -18
- package/dist/index.js +225 -87
- package/package.json +8 -3
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
|
-
|
|
186
|
-
|
|
189
|
+
const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
|
|
190
|
+
if (names.includes("api")) {
|
|
191
|
+
return withJsonErrorHandling(wrapped);
|
|
187
192
|
}
|
|
188
|
-
return
|
|
193
|
+
return wrapped;
|
|
189
194
|
}
|
|
190
195
|
wrapApi(handler) {
|
|
191
|
-
return
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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(
|
|
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 = [
|
|
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 ?? "
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
801
|
-
const
|
|
802
|
-
const loaded =
|
|
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
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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 {
|
|
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()) ||
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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() &&
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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 {
|
|
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
|
-
|
|
1447
|
-
|
|
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
|
|
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
|
|
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
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
WHERE
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
-
|
|
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
|
|
1549
|
-
|
|
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
|
|
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
|
|
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
|
|
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);
|