@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/bootstrap changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
Fix generated app boot
|
|
6
|
+
|
|
7
|
+
## 1.1.0
|
|
8
|
+
|
|
9
|
+
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`.
|
|
10
|
+
|
|
3
11
|
## 1.0.9
|
|
4
12
|
|
|
5
13
|
Label HiroApp as internal e2e dogfood and seed notes via Model
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type ListenerRegistrar = () => void;
|
|
2
|
-
declare const appListeners: ListenerRegistrar[];
|
|
3
2
|
declare function discoverListeners(): ListenerRegistrar[];
|
|
3
|
+
declare function resetDiscoverListenersForTests(): void;
|
|
4
4
|
export type { ListenerRegistrar };
|
|
5
|
-
export {
|
|
5
|
+
export { discoverListeners, resetDiscoverListenersForTests };
|
|
@@ -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
|
-
|
|
172
|
-
|
|
175
|
+
const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
|
|
176
|
+
if (names.includes("api")) {
|
|
177
|
+
return withJsonErrorHandling(wrapped);
|
|
173
178
|
}
|
|
174
|
-
return
|
|
179
|
+
return wrapped;
|
|
175
180
|
}
|
|
176
181
|
wrapApi(handler) {
|
|
177
|
-
return
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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(
|
|
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 = [
|
|
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
|
-
|
|
175
|
-
|
|
178
|
+
const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
|
|
179
|
+
if (names.includes("api")) {
|
|
180
|
+
return withJsonErrorHandling(wrapped);
|
|
176
181
|
}
|
|
177
|
-
return
|
|
182
|
+
return wrapped;
|
|
178
183
|
}
|
|
179
184
|
wrapApi(handler) {
|
|
180
|
-
return
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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(
|
|
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 = [
|
|
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) {
|
package/dist/entries/context.js
CHANGED
|
@@ -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 ?? "
|
|
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
|
};
|
|
@@ -370,8 +370,19 @@ var events_default = eventsProvider;
|
|
|
370
370
|
|
|
371
371
|
// ../../src/bootstrap/discoverListeners.ts
|
|
372
372
|
import { existsSync, readdirSync as readdirSync2 } from "fs";
|
|
373
|
+
import { createRequire } from "module";
|
|
373
374
|
import { join as join2 } from "path";
|
|
374
|
-
|
|
375
|
+
var requireListener = createRequire(import.meta.url);
|
|
376
|
+
var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
|
|
377
|
+
function readDiscoverListenersState() {
|
|
378
|
+
const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
|
|
379
|
+
if (existing) {
|
|
380
|
+
return existing;
|
|
381
|
+
}
|
|
382
|
+
const state = {};
|
|
383
|
+
globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
|
|
384
|
+
return state;
|
|
385
|
+
}
|
|
375
386
|
function resolveListenersDirectory() {
|
|
376
387
|
const fromCwd = join2(process.cwd(), "src", "listeners");
|
|
377
388
|
if (existsSync(fromCwd)) {
|
|
@@ -379,7 +390,7 @@ function resolveListenersDirectory() {
|
|
|
379
390
|
}
|
|
380
391
|
return join2(import.meta.dir, "../listeners");
|
|
381
392
|
}
|
|
382
|
-
|
|
393
|
+
function loadDiscoveredListeners() {
|
|
383
394
|
const listenersDirectory = resolveListenersDirectory();
|
|
384
395
|
let entries;
|
|
385
396
|
try {
|
|
@@ -390,16 +401,21 @@ async function loadDiscoveredListeners() {
|
|
|
390
401
|
}
|
|
391
402
|
throw error;
|
|
392
403
|
}
|
|
393
|
-
const listeners =
|
|
394
|
-
const
|
|
395
|
-
const loaded =
|
|
404
|
+
const listeners = entries.map((fileName) => {
|
|
405
|
+
const filePath = join2(listenersDirectory, fileName);
|
|
406
|
+
const loaded = requireListener(filePath);
|
|
396
407
|
return loaded.default;
|
|
397
|
-
})
|
|
408
|
+
});
|
|
398
409
|
return listeners.filter((listener) => typeof listener === "function");
|
|
399
410
|
}
|
|
400
|
-
var appListeners = await loadDiscoveredListeners();
|
|
401
411
|
function discoverListeners() {
|
|
402
|
-
|
|
412
|
+
const state = readDiscoverListenersState();
|
|
413
|
+
state.appListeners ??= loadDiscoveredListeners();
|
|
414
|
+
return state.appListeners;
|
|
415
|
+
}
|
|
416
|
+
function resetDiscoverListenersForTests() {
|
|
417
|
+
const state = readDiscoverListenersState();
|
|
418
|
+
state.appListeners = undefined;
|
|
403
419
|
}
|
|
404
420
|
|
|
405
421
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
180
|
-
|
|
184
|
+
const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
|
|
185
|
+
if (names.includes("api")) {
|
|
186
|
+
return withJsonErrorHandling(wrapped);
|
|
181
187
|
}
|
|
182
|
-
return
|
|
188
|
+
return wrapped;
|
|
183
189
|
}
|
|
184
190
|
wrapApi(handler) {
|
|
185
|
-
return
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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 = [
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 ?? "
|
|
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
|
};
|
|
@@ -370,8 +370,19 @@ var events_default = eventsProvider;
|
|
|
370
370
|
|
|
371
371
|
// ../../src/bootstrap/discoverListeners.ts
|
|
372
372
|
import { existsSync, readdirSync as readdirSync2 } from "fs";
|
|
373
|
+
import { createRequire } from "module";
|
|
373
374
|
import { join as join2 } from "path";
|
|
374
|
-
|
|
375
|
+
var requireListener = createRequire(import.meta.url);
|
|
376
|
+
var DISCOVER_LISTENERS_STATE_KEY = Symbol.for("@getstrata/discoverListenersState");
|
|
377
|
+
function readDiscoverListenersState() {
|
|
378
|
+
const existing = globalThis[DISCOVER_LISTENERS_STATE_KEY];
|
|
379
|
+
if (existing) {
|
|
380
|
+
return existing;
|
|
381
|
+
}
|
|
382
|
+
const state = {};
|
|
383
|
+
globalThis[DISCOVER_LISTENERS_STATE_KEY] = state;
|
|
384
|
+
return state;
|
|
385
|
+
}
|
|
375
386
|
function resolveListenersDirectory() {
|
|
376
387
|
const fromCwd = join2(process.cwd(), "src", "listeners");
|
|
377
388
|
if (existsSync(fromCwd)) {
|
|
@@ -379,7 +390,7 @@ function resolveListenersDirectory() {
|
|
|
379
390
|
}
|
|
380
391
|
return join2(import.meta.dir, "../listeners");
|
|
381
392
|
}
|
|
382
|
-
|
|
393
|
+
function loadDiscoveredListeners() {
|
|
383
394
|
const listenersDirectory = resolveListenersDirectory();
|
|
384
395
|
let entries;
|
|
385
396
|
try {
|
|
@@ -390,16 +401,21 @@ async function loadDiscoveredListeners() {
|
|
|
390
401
|
}
|
|
391
402
|
throw error;
|
|
392
403
|
}
|
|
393
|
-
const listeners =
|
|
394
|
-
const
|
|
395
|
-
const loaded =
|
|
404
|
+
const listeners = entries.map((fileName) => {
|
|
405
|
+
const filePath = join2(listenersDirectory, fileName);
|
|
406
|
+
const loaded = requireListener(filePath);
|
|
396
407
|
return loaded.default;
|
|
397
|
-
})
|
|
408
|
+
});
|
|
398
409
|
return listeners.filter((listener) => typeof listener === "function");
|
|
399
410
|
}
|
|
400
|
-
var appListeners = await loadDiscoveredListeners();
|
|
401
411
|
function discoverListeners() {
|
|
402
|
-
|
|
412
|
+
const state = readDiscoverListenersState();
|
|
413
|
+
state.appListeners ??= loadDiscoveredListeners();
|
|
414
|
+
return state.appListeners;
|
|
415
|
+
}
|
|
416
|
+
function resetDiscoverListenersForTests() {
|
|
417
|
+
const state = readDiscoverListenersState();
|
|
418
|
+
state.appListeners = undefined;
|
|
403
419
|
}
|
|
404
420
|
|
|
405
421
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|