@getstrata/bootstrap 0.2.48 → 0.2.50
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/README.md +5 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/health.d.ts +8 -2
- package/dist/bootstrap/public-api.d.ts +4 -2
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/bootstrap/web/session.d.ts +25 -3
- package/dist/entries/buildModuleRoutes.js +24 -8
- package/dist/entries/buildWebModuleRoutes.js +24 -8
- package/dist/entries/config.js +4 -0
- package/dist/entries/context.js +2 -63
- package/dist/entries/createRoutes.js +218 -169
- package/dist/entries/createSpaRoutes.js +1 -1
- package/dist/entries/createWebRoutes.js +24 -8
- package/dist/entries/dependencies.js +2 -63
- package/dist/entries/health.js +69 -152
- package/dist/entries/httpKernel.js +24 -8
- package/dist/entries/providers.js +2 -0
- package/dist/entries/secretsGuard.js +64 -10
- package/dist/entries/web/routing.js +28 -12
- package/dist/entries/web/session.js +59 -7
- package/dist/index.js +300 -81
- package/package.json +5 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @getstrata/bootstrap changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.50
|
|
4
|
+
|
|
5
|
+
- `createCookieSessionAuthManager` / `CookieSessionGuard` turn `CookieSessionStore` into an `AuthGuard` / `AuthManager` for `CORE_AUTH_TOKEN`. Supply a `mapUser` mapper; WorkHub abilities are not baked in. Omit `sql` to read the client from `bindDatabaseConnection()` on every call.
|
|
6
|
+
- `checkDatabase()` / `createHealthRoutes()` ping the bound database client, not WorkHub’s private connection holder. `/health` stays `{ status: "ok" }` unless `{ pingOnHealth: true }`. Extra JSON fields are optional.
|
|
7
|
+
- `assertProductionSecrets()` is feature-gated. A production HTMX app with `SESSION_SECRET` (32+), `AUTH_DEV_HEADERS=false`, and feature flags off does not need WorkHub API tokens. WorkHub’s current production env (rotated tokens, CORS, pepper, expiry) still passes.
|
|
8
|
+
- Re-exports `CORE_ABILITY_CHECKER_TOKEN` and `CORE_AUTH_USER_DIRECTORY_TOKEN`.
|
package/README.md
CHANGED
|
@@ -28,7 +28,11 @@ import {
|
|
|
28
28
|
import { createHttpKernel, createAppContext, coreProviders } from "@getstrata/bootstrap";
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`assertProductionSecrets`
|
|
31
|
+
`createAppContext()` does not call `assertProductionSecrets`. WorkHub calls that from `App.serve()` / `queue:work`. Sibling HTMX apps can call it in production without WorkHub API tokens when feature flags are off — see [SIBLING-HTMX.md](../../docs/SIBLING-HTMX.md).
|
|
32
|
+
|
|
33
|
+
Sibling HTMX apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstrap/web/session` instead of HMAC `SessionGuard`. `CookieSessionStore` reads optional `is_admin` from the user row for `wrapWebGlobalAdmin`. Pass `mapUser` to map `is_admin` / `learn_subscriber` onto `AuthUser.role`.
|
|
34
|
+
|
|
35
|
+
`wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 in `FRONTEND_MODE=server-htmx`. `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle — do not wrap them with `wrapWeb` again.
|
|
32
36
|
|
|
33
37
|
These subpaths remain WorkHub-oriented and are not a generic starter API: `@getstrata/bootstrap/createRoutes` (includes SCIM), `@getstrata/bootstrap/schedule`, and `@getstrata/bootstrap/createWebRoutes` (redirects `/` to `/organizations`).
|
|
34
38
|
|
|
@@ -4,7 +4,7 @@ declare const CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
|
4
4
|
declare const CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
5
5
|
declare const REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
6
6
|
declare const DATABASE_URL_CONFIG_KEY = "database.url";
|
|
7
|
-
export { CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "@getstrata/core/contracts/serviceTokens";
|
|
7
|
+
export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "@getstrata/core/contracts/serviceTokens";
|
|
8
8
|
declare const AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
|
|
9
9
|
declare const DEFAULT_APP_PORT = 3000;
|
|
10
10
|
declare const DEFAULT_CACHE_TTL_MS = 3600000;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { AppDependencies } from "./contracts";
|
|
2
|
+
interface CreateHealthRoutesOptions {
|
|
3
|
+
pingOnHealth?: boolean;
|
|
4
|
+
extra?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>);
|
|
5
|
+
}
|
|
2
6
|
declare function checkDatabase(): Promise<boolean>;
|
|
7
|
+
declare function pingDatabase(): Promise<boolean>;
|
|
3
8
|
declare function checkRedis(redisUrl: string): Promise<boolean>;
|
|
4
|
-
declare function createHealthRoutes(dependencies: AppDependencies): {
|
|
9
|
+
declare function createHealthRoutes(dependencies: AppDependencies, options?: CreateHealthRoutesOptions): {
|
|
5
10
|
"/health": () => Promise<Response>;
|
|
6
11
|
"/ready": () => Promise<Response>;
|
|
7
12
|
};
|
|
8
|
-
export {
|
|
13
|
+
export type { CreateHealthRoutesOptions };
|
|
14
|
+
export { checkDatabase, checkRedis, createHealthRoutes, pingDatabase };
|
|
@@ -9,7 +9,7 @@ export { buildModuleRoutes } from "./buildModuleRoutes.ts";
|
|
|
9
9
|
export type { BuildWebModuleRoutesOptions } from "./buildWebModuleRoutes.ts";
|
|
10
10
|
export { buildWebModuleRoutes } from "./buildWebModuleRoutes.ts";
|
|
11
11
|
export { cacheTagsForModelWrite, discoverModelTableNames } from "./cache/modelCacheTags.ts";
|
|
12
|
-
export { APP_PORT_CONFIG_KEY, CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, DATABASE_URL_CONFIG_KEY, DEFAULT_APP_PORT, REDIS_URL_CONFIG_KEY, } from "./config.ts";
|
|
12
|
+
export { APP_PORT_CONFIG_KEY, CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, DATABASE_URL_CONFIG_KEY, DEFAULT_APP_PORT, REDIS_URL_CONFIG_KEY, } from "./config.ts";
|
|
13
13
|
export { collectProviders, createAppContext, runProviderPhase } from "./context.ts";
|
|
14
14
|
export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, ConfigStore, ModuleRouteContext, MutableAppDependencies, ProviderContext, ServiceFactory, ServiceProvider, } from "./contracts.ts";
|
|
15
15
|
export { assertAppDependenciesComplete, getRequiredDependency, resolveService, ServiceContainer, } from "./contracts.ts";
|
|
@@ -17,6 +17,8 @@ export { createWebRoutes, mergeWebRoutes } from "./createWebRoutes.ts";
|
|
|
17
17
|
export { createAppDependencies } from "./dependencies.ts";
|
|
18
18
|
export type { DiscoverModulesOptions } from "./discoverModules.ts";
|
|
19
19
|
export { configureModulesDirectory, discoverModules, ensureModulesLoaded, } from "./discoverModules.ts";
|
|
20
|
+
export type { CreateHealthRoutesOptions } from "./health.ts";
|
|
21
|
+
export { checkDatabase, checkRedis, createHealthRoutes, pingDatabase } from "./health.ts";
|
|
20
22
|
export { type RouteModelAuthorization, securedBindRouteModel, securedBindRouteModelByKey, } from "./http/securedRouteModelBinding.ts";
|
|
21
23
|
export { createHttpKernel, type HttpKernel, type MiddlewareGroupName } from "./httpKernel.ts";
|
|
22
24
|
export { resolveMembershipService } from "./membershipService.ts";
|
|
@@ -25,4 +27,4 @@ export { coreProviders } from "./providers/index.ts";
|
|
|
25
27
|
export { registerDefaultJobs } from "./queue/defaultJobs.ts";
|
|
26
28
|
export { RouteRegistry, routeRegistry } from "./routeRegistry.ts";
|
|
27
29
|
export { assertProductionSecrets } from "./secretsGuard.ts";
|
|
28
|
-
export { CookieSessionStore, createCsrfProtection, createRouteKernel, createWebServer, type ParsedForm, parseFormBody, routeParams, type SessionUser, slugify, toRouteRequest, type WebServerOptions, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./web/index.ts";
|
|
30
|
+
export { CookieSessionGuard, CookieSessionStore, createCookieSessionAuthManager, createCsrfProtection, createRouteKernel, createWebServer, type MapSessionUser, type ParsedForm, parseFormBody, routeParams, type SessionUser, slugify, toRouteRequest, type WebServerOptions, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./web/index.ts";
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
export { createCsrfProtection, type ParsedForm, parseFormBody } from "./forms.ts";
|
|
5
5
|
export { createRouteKernel, routeParams, toRouteRequest, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./routing.ts";
|
|
6
6
|
export { convertAppRoutesToBunRoutes, createWebServer, type WebServerOptions } from "./server.ts";
|
|
7
|
-
export { CookieSessionStore, type SessionUser } from "./session.ts";
|
|
7
|
+
export { CookieSessionGuard, CookieSessionStore, createCookieSessionAuthManager, type MapSessionUser, type SessionUser, } from "./session.ts";
|
|
8
8
|
export { slugify } from "./slug.ts";
|
|
@@ -14,9 +14,9 @@ export declare function wrapSecuredRouteModelByKey<TParams extends Record<string
|
|
|
14
14
|
resource: string;
|
|
15
15
|
action: "view" | "create" | "update" | "delete";
|
|
16
16
|
}, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): RouteHandler;
|
|
17
|
-
/**
|
|
17
|
+
/** Web group (CSRF + flash + HTML errors) plus login throttle. Do not wrap with wrapWeb again. */
|
|
18
18
|
export declare function wrapWebLogin(kernel: HttpKernel, handler: RouteHandler, onThrottled: (request: Request) => Response | Promise<Response>): RouteHandler;
|
|
19
|
-
/**
|
|
19
|
+
/** Web group (CSRF + flash + HTML errors) plus register throttle. Do not wrap with wrapWeb again. */
|
|
20
20
|
export declare function wrapWebRegister(kernel: HttpKernel, handler: RouteHandler, onThrottled: (request: Request) => Response | Promise<Response>): RouteHandler;
|
|
21
21
|
/** Convenience alias: HttpKernel is the Laravel-style router middleware wrapper. */
|
|
22
22
|
export declare function createRouteKernel(dependencies: AppDependencies): HttpKernel;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AuthUser } from "@getstrata/core/auth/authContext";
|
|
2
|
+
import { type AuthGuard, AuthManager } from "@getstrata/core/auth/guard";
|
|
1
3
|
export interface SessionUser {
|
|
2
4
|
id: number;
|
|
3
5
|
name: string;
|
|
@@ -8,18 +10,38 @@ export interface SessionUser {
|
|
|
8
10
|
type SqlClient = {
|
|
9
11
|
unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
|
|
10
12
|
};
|
|
13
|
+
type SqlSource = SqlClient | (() => SqlClient);
|
|
14
|
+
declare function defaultSessionSql(): SqlClient;
|
|
15
|
+
export type MapSessionUser = (user: SessionUser) => AuthUser;
|
|
16
|
+
declare function defaultMapSessionUser(user: SessionUser): AuthUser;
|
|
11
17
|
export declare class CookieSessionStore {
|
|
12
|
-
private readonly
|
|
18
|
+
private readonly sqlSource;
|
|
13
19
|
private readonly secret;
|
|
14
20
|
private readonly cookieName;
|
|
15
21
|
private readonly maxAgeSeconds;
|
|
16
|
-
constructor(
|
|
22
|
+
constructor(sqlSource: SqlSource, secret: string, cookieName?: string, maxAgeSeconds?: number);
|
|
17
23
|
cookieHeader(_user: SessionUser, sessionId: string): string;
|
|
18
24
|
clearCookieHeader(): string;
|
|
19
25
|
private withSecureFlag;
|
|
26
|
+
private sql;
|
|
20
27
|
create(user: SessionUser): Promise<string>;
|
|
21
28
|
destroy(sessionId: string): Promise<void>;
|
|
22
29
|
read(request: Request): Promise<SessionUser | null>;
|
|
23
30
|
private sign;
|
|
24
31
|
}
|
|
25
|
-
export {
|
|
32
|
+
export declare class CookieSessionGuard implements AuthGuard {
|
|
33
|
+
private readonly store;
|
|
34
|
+
private readonly mapUser;
|
|
35
|
+
constructor(store: CookieSessionStore, mapUser?: MapSessionUser);
|
|
36
|
+
resolve(request: Request): Promise<AuthUser | null>;
|
|
37
|
+
}
|
|
38
|
+
export interface CreateCookieSessionAuthManagerOptions {
|
|
39
|
+
store?: CookieSessionStore;
|
|
40
|
+
sql?: SqlSource;
|
|
41
|
+
secret?: string;
|
|
42
|
+
cookieName?: string;
|
|
43
|
+
maxAgeSeconds?: number;
|
|
44
|
+
mapUser?: MapSessionUser;
|
|
45
|
+
}
|
|
46
|
+
declare function createCookieSessionAuthManager(options?: CreateCookieSessionAuthManagerOptions): AuthManager;
|
|
47
|
+
export { createCookieSessionAuthManager, defaultMapSessionUser, defaultSessionSql };
|
|
@@ -7,6 +7,7 @@ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
|
7
7
|
|
|
8
8
|
// ../../src/bootstrap/httpKernel.ts
|
|
9
9
|
import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
|
|
10
|
+
import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
|
|
10
11
|
import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
|
|
11
12
|
import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
|
|
12
13
|
import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
|
|
@@ -21,6 +22,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
21
22
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
22
23
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
23
24
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
25
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
24
26
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
25
27
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
26
28
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -66,24 +68,38 @@ function parsePositiveInt(value, fallback) {
|
|
|
66
68
|
}
|
|
67
69
|
return Math.trunc(parsed);
|
|
68
70
|
}
|
|
71
|
+
function parseWindowSeconds(secondsValue, msValue, fallback) {
|
|
72
|
+
if (secondsValue !== undefined && secondsValue.trim() !== "") {
|
|
73
|
+
return parsePositiveInt(secondsValue, fallback);
|
|
74
|
+
}
|
|
75
|
+
if (msValue !== undefined && msValue.trim() !== "") {
|
|
76
|
+
const parsedMs = Number(msValue);
|
|
77
|
+
if (Number.isFinite(parsedMs) && parsedMs > 0) {
|
|
78
|
+
return Math.max(1, Math.trunc(parsedMs / 1000));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
69
83
|
function resolveLoginRateLimit() {
|
|
70
84
|
const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
|
|
71
85
|
return {
|
|
72
86
|
maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
73
|
-
decaySeconds:
|
|
87
|
+
decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
|
|
74
88
|
};
|
|
75
89
|
}
|
|
76
90
|
function resolveRegisterRateLimit() {
|
|
77
91
|
const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
|
|
78
92
|
return {
|
|
79
93
|
maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
80
|
-
decaySeconds:
|
|
94
|
+
decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
|
|
81
95
|
};
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
// ../../src/bootstrap/config.ts
|
|
85
99
|
import {
|
|
100
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
86
101
|
CORE_AUTH_TOKEN,
|
|
102
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
87
103
|
CORE_CACHE_TOKEN,
|
|
88
104
|
CORE_CONFIG_TOKEN,
|
|
89
105
|
CORE_EVENT_BUS_TOKEN,
|
|
@@ -173,7 +189,7 @@ class HttpKernel {
|
|
|
173
189
|
return this.wrap(["api", "authenticated"], handler);
|
|
174
190
|
}
|
|
175
191
|
wrapWeb(handler) {
|
|
176
|
-
return this.wrap("web", handler);
|
|
192
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
177
193
|
}
|
|
178
194
|
wrapWebPublicRead(handler) {
|
|
179
195
|
if (isPublicReadsEnabled()) {
|
|
@@ -183,19 +199,19 @@ class HttpKernel {
|
|
|
183
199
|
}
|
|
184
200
|
wrapWebAuthenticated(handler) {
|
|
185
201
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
186
|
-
return this.
|
|
202
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
187
203
|
}
|
|
188
204
|
wrapWebAbility(ability, handler) {
|
|
189
205
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
190
|
-
const abilityChecker = this.dependencies.container
|
|
206
|
+
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
191
207
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
192
208
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
193
|
-
return this.
|
|
209
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
194
210
|
}
|
|
195
211
|
wrapWebGlobalAdmin(handler) {
|
|
196
212
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
197
213
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
198
|
-
return this.
|
|
214
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
199
215
|
}
|
|
200
216
|
wrapAuthenticated(handler) {
|
|
201
217
|
return this.wrap("authenticated", handler);
|
|
@@ -211,7 +227,7 @@ class HttpKernel {
|
|
|
211
227
|
return withMiddleware(...middleware)(handler);
|
|
212
228
|
}
|
|
213
229
|
wrapAbility(ability, handler) {
|
|
214
|
-
const abilityChecker = this.dependencies.container
|
|
230
|
+
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
215
231
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
216
232
|
const middleware = [...this.group("authenticated"), requireAbility(ability)];
|
|
217
233
|
return withMiddleware(...middleware)(handler);
|
|
@@ -10,6 +10,7 @@ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
|
|
|
10
10
|
|
|
11
11
|
// ../../src/bootstrap/httpKernel.ts
|
|
12
12
|
import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
|
|
13
|
+
import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
|
|
13
14
|
import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
|
|
14
15
|
import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
|
|
15
16
|
import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
|
|
@@ -24,6 +25,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
24
25
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
25
26
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
26
27
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
28
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
27
29
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
28
30
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
29
31
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -69,24 +71,38 @@ function parsePositiveInt(value, fallback) {
|
|
|
69
71
|
}
|
|
70
72
|
return Math.trunc(parsed);
|
|
71
73
|
}
|
|
74
|
+
function parseWindowSeconds(secondsValue, msValue, fallback) {
|
|
75
|
+
if (secondsValue !== undefined && secondsValue.trim() !== "") {
|
|
76
|
+
return parsePositiveInt(secondsValue, fallback);
|
|
77
|
+
}
|
|
78
|
+
if (msValue !== undefined && msValue.trim() !== "") {
|
|
79
|
+
const parsedMs = Number(msValue);
|
|
80
|
+
if (Number.isFinite(parsedMs) && parsedMs > 0) {
|
|
81
|
+
return Math.max(1, Math.trunc(parsedMs / 1000));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
72
86
|
function resolveLoginRateLimit() {
|
|
73
87
|
const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
|
|
74
88
|
return {
|
|
75
89
|
maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
76
|
-
decaySeconds:
|
|
90
|
+
decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
|
|
77
91
|
};
|
|
78
92
|
}
|
|
79
93
|
function resolveRegisterRateLimit() {
|
|
80
94
|
const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
|
|
81
95
|
return {
|
|
82
96
|
maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
83
|
-
decaySeconds:
|
|
97
|
+
decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
|
|
84
98
|
};
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
// ../../src/bootstrap/config.ts
|
|
88
102
|
import {
|
|
103
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
89
104
|
CORE_AUTH_TOKEN,
|
|
105
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
90
106
|
CORE_CACHE_TOKEN,
|
|
91
107
|
CORE_CONFIG_TOKEN,
|
|
92
108
|
CORE_EVENT_BUS_TOKEN,
|
|
@@ -176,7 +192,7 @@ class HttpKernel {
|
|
|
176
192
|
return this.wrap(["api", "authenticated"], handler);
|
|
177
193
|
}
|
|
178
194
|
wrapWeb(handler) {
|
|
179
|
-
return this.wrap("web", handler);
|
|
195
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
180
196
|
}
|
|
181
197
|
wrapWebPublicRead(handler) {
|
|
182
198
|
if (isPublicReadsEnabled()) {
|
|
@@ -186,19 +202,19 @@ class HttpKernel {
|
|
|
186
202
|
}
|
|
187
203
|
wrapWebAuthenticated(handler) {
|
|
188
204
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
189
|
-
return this.
|
|
205
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
190
206
|
}
|
|
191
207
|
wrapWebAbility(ability, handler) {
|
|
192
208
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
193
|
-
const abilityChecker = this.dependencies.container
|
|
209
|
+
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
194
210
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
195
211
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
196
|
-
return this.
|
|
212
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
197
213
|
}
|
|
198
214
|
wrapWebGlobalAdmin(handler) {
|
|
199
215
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
200
216
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
201
|
-
return this.
|
|
217
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
202
218
|
}
|
|
203
219
|
wrapAuthenticated(handler) {
|
|
204
220
|
return this.wrap("authenticated", handler);
|
|
@@ -214,7 +230,7 @@ class HttpKernel {
|
|
|
214
230
|
return withMiddleware(...middleware)(handler);
|
|
215
231
|
}
|
|
216
232
|
wrapAbility(ability, handler) {
|
|
217
|
-
const abilityChecker = this.dependencies.container
|
|
233
|
+
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
218
234
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
219
235
|
const middleware = [...this.group("authenticated"), requireAbility(ability)];
|
|
220
236
|
return withMiddleware(...middleware)(handler);
|
package/dist/entries/config.js
CHANGED
|
@@ -3,7 +3,9 @@ var __jsonParse = (a) => JSON.parse(a);
|
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/config.ts
|
|
5
5
|
import {
|
|
6
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
6
7
|
CORE_AUTH_TOKEN,
|
|
8
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
7
9
|
CORE_CACHE_TOKEN,
|
|
8
10
|
CORE_CONFIG_TOKEN,
|
|
9
11
|
CORE_EVENT_BUS_TOKEN,
|
|
@@ -30,7 +32,9 @@ export {
|
|
|
30
32
|
CACHE_DRIVER_CONFIG_KEY,
|
|
31
33
|
CACHE_MAX_ENTRIES_CONFIG_KEY,
|
|
32
34
|
CACHE_TTL_MS_CONFIG_KEY,
|
|
35
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
33
36
|
CORE_AUTH_TOKEN,
|
|
37
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
34
38
|
CORE_CACHE_TOKEN,
|
|
35
39
|
CORE_CONFIG_TOKEN,
|
|
36
40
|
CORE_EVENT_BUS_TOKEN,
|
package/dist/entries/context.js
CHANGED
|
@@ -107,7 +107,9 @@ var authConfig = {
|
|
|
107
107
|
|
|
108
108
|
// ../../src/bootstrap/config.ts
|
|
109
109
|
import {
|
|
110
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
110
111
|
CORE_AUTH_TOKEN,
|
|
112
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
111
113
|
CORE_CACHE_TOKEN,
|
|
112
114
|
CORE_CONFIG_TOKEN,
|
|
113
115
|
CORE_EVENT_BUS_TOKEN,
|
|
@@ -506,68 +508,6 @@ var coreProviders = [
|
|
|
506
508
|
viewProvider
|
|
507
509
|
];
|
|
508
510
|
|
|
509
|
-
// ../../src/bootstrap/secretsGuard.ts
|
|
510
|
-
var DEFAULT_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
511
|
-
var DEFAULT_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
512
|
-
var DEFAULT_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
513
|
-
var DEFAULT_TOKENS = new Set([DEFAULT_ADMIN_API_TOKEN, DEFAULT_MEMBER_API_TOKEN]);
|
|
514
|
-
var DEFAULT_SCIM_TOKENS = new Set([DEFAULT_SCIM_BEARER_TOKEN]);
|
|
515
|
-
function isEnabled(value, defaultEnabled) {
|
|
516
|
-
if (value === undefined) {
|
|
517
|
-
return defaultEnabled;
|
|
518
|
-
}
|
|
519
|
-
return defaultEnabled ? value !== "false" : value === "true";
|
|
520
|
-
}
|
|
521
|
-
function assertProductionSecrets(env = process.env) {
|
|
522
|
-
const appEnv = env.APP_ENV ?? "local";
|
|
523
|
-
if (appEnv !== "production") {
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
const adminToken = env.ADMIN_API_TOKEN ?? DEFAULT_ADMIN_API_TOKEN;
|
|
527
|
-
const memberToken = env.MEMBER_API_TOKEN ?? DEFAULT_MEMBER_API_TOKEN;
|
|
528
|
-
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
529
|
-
const encryptionEnabled = isEnabled(env.FEATURE_FIELD_ENCRYPTION, true);
|
|
530
|
-
const devHeadersEnabled = isEnabled(env.AUTH_DEV_HEADERS, true);
|
|
531
|
-
if (devHeadersEnabled) {
|
|
532
|
-
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
533
|
-
}
|
|
534
|
-
if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
|
|
535
|
-
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
536
|
-
}
|
|
537
|
-
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
538
|
-
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
539
|
-
}
|
|
540
|
-
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
541
|
-
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
542
|
-
}
|
|
543
|
-
if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, true)) {
|
|
544
|
-
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
545
|
-
}
|
|
546
|
-
if (isEnabled(env.FEATURE_BILLING, true) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
547
|
-
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
548
|
-
}
|
|
549
|
-
const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
|
|
550
|
-
if (corsOrigins.includes("*")) {
|
|
551
|
-
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
552
|
-
}
|
|
553
|
-
if (isEnabled(env.FEATURE_PUBLIC_READS, true)) {
|
|
554
|
-
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
555
|
-
}
|
|
556
|
-
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
557
|
-
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
558
|
-
}
|
|
559
|
-
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
560
|
-
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
561
|
-
}
|
|
562
|
-
if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
|
|
563
|
-
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
564
|
-
}
|
|
565
|
-
const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
|
|
566
|
-
if (frontendMode === "server-htmx" && !env.SESSION_SECRET?.trim()) {
|
|
567
|
-
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx.");
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
511
|
// ../../src/bootstrap/context.ts
|
|
572
512
|
function collectProviders(modules = discoverModules()) {
|
|
573
513
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
@@ -578,7 +518,6 @@ function runProviderPhase(providers, phase, context) {
|
|
|
578
518
|
}
|
|
579
519
|
}
|
|
580
520
|
function createAppContext() {
|
|
581
|
-
assertProductionSecrets();
|
|
582
521
|
const container = new ServiceContainer;
|
|
583
522
|
const config = new ConfigStore;
|
|
584
523
|
const dependencies = {
|