@getstrata/bootstrap 0.2.48 → 0.2.49
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/README.md +3 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/entries/buildModuleRoutes.js +5 -4
- package/dist/entries/buildWebModuleRoutes.js +5 -4
- package/dist/entries/context.js +0 -63
- package/dist/entries/createRoutes.js +15 -14
- package/dist/entries/createWebRoutes.js +5 -4
- package/dist/entries/dependencies.js +0 -63
- package/dist/entries/httpKernel.js +5 -4
- package/dist/entries/web/routing.js +9 -8
- package/dist/index.js +70 -71
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,7 +28,9 @@ 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 apps should call it only if they want those WorkHub production gates (API tokens, Stripe, SCIM, CORS).
|
|
32
|
+
|
|
33
|
+
`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
34
|
|
|
33
35
|
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
36
|
|
|
@@ -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;
|
|
@@ -21,6 +21,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
21
21
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
22
22
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
23
23
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
24
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
24
25
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
25
26
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
26
27
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -173,7 +174,7 @@ class HttpKernel {
|
|
|
173
174
|
return this.wrap(["api", "authenticated"], handler);
|
|
174
175
|
}
|
|
175
176
|
wrapWeb(handler) {
|
|
176
|
-
return this.wrap("web", handler);
|
|
177
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
177
178
|
}
|
|
178
179
|
wrapWebPublicRead(handler) {
|
|
179
180
|
if (isPublicReadsEnabled()) {
|
|
@@ -183,19 +184,19 @@ class HttpKernel {
|
|
|
183
184
|
}
|
|
184
185
|
wrapWebAuthenticated(handler) {
|
|
185
186
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
186
|
-
return this.
|
|
187
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
187
188
|
}
|
|
188
189
|
wrapWebAbility(ability, handler) {
|
|
189
190
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
190
191
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
191
192
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
192
193
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
193
|
-
return this.
|
|
194
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
194
195
|
}
|
|
195
196
|
wrapWebGlobalAdmin(handler) {
|
|
196
197
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
197
198
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
198
|
-
return this.
|
|
199
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
199
200
|
}
|
|
200
201
|
wrapAuthenticated(handler) {
|
|
201
202
|
return this.wrap("authenticated", handler);
|
|
@@ -24,6 +24,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
24
24
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
25
25
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
26
26
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
27
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
27
28
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
28
29
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
29
30
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -176,7 +177,7 @@ class HttpKernel {
|
|
|
176
177
|
return this.wrap(["api", "authenticated"], handler);
|
|
177
178
|
}
|
|
178
179
|
wrapWeb(handler) {
|
|
179
|
-
return this.wrap("web", handler);
|
|
180
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
180
181
|
}
|
|
181
182
|
wrapWebPublicRead(handler) {
|
|
182
183
|
if (isPublicReadsEnabled()) {
|
|
@@ -186,19 +187,19 @@ class HttpKernel {
|
|
|
186
187
|
}
|
|
187
188
|
wrapWebAuthenticated(handler) {
|
|
188
189
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
189
|
-
return this.
|
|
190
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
190
191
|
}
|
|
191
192
|
wrapWebAbility(ability, handler) {
|
|
192
193
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
193
194
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
194
195
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
195
196
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
196
|
-
return this.
|
|
197
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
197
198
|
}
|
|
198
199
|
wrapWebGlobalAdmin(handler) {
|
|
199
200
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
200
201
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
201
|
-
return this.
|
|
202
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
202
203
|
}
|
|
203
204
|
wrapAuthenticated(handler) {
|
|
204
205
|
return this.wrap("authenticated", handler);
|
package/dist/entries/context.js
CHANGED
|
@@ -506,68 +506,6 @@ var coreProviders = [
|
|
|
506
506
|
viewProvider
|
|
507
507
|
];
|
|
508
508
|
|
|
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
509
|
// ../../src/bootstrap/context.ts
|
|
572
510
|
function collectProviders(modules = discoverModules()) {
|
|
573
511
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
@@ -578,7 +516,6 @@ function runProviderPhase(providers, phase, context) {
|
|
|
578
516
|
}
|
|
579
517
|
}
|
|
580
518
|
function createAppContext() {
|
|
581
|
-
assertProductionSecrets();
|
|
582
519
|
const container = new ServiceContainer;
|
|
583
520
|
const config = new ConfigStore;
|
|
584
521
|
const dependencies = {
|
|
@@ -77,6 +77,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
77
77
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
78
78
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
79
79
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
80
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
80
81
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
81
82
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
82
83
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -211,7 +212,7 @@ class HttpKernel {
|
|
|
211
212
|
return this.wrap(["api", "authenticated"], handler);
|
|
212
213
|
}
|
|
213
214
|
wrapWeb(handler) {
|
|
214
|
-
return this.wrap("web", handler);
|
|
215
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
215
216
|
}
|
|
216
217
|
wrapWebPublicRead(handler) {
|
|
217
218
|
if (isPublicReadsEnabled()) {
|
|
@@ -221,19 +222,19 @@ class HttpKernel {
|
|
|
221
222
|
}
|
|
222
223
|
wrapWebAuthenticated(handler) {
|
|
223
224
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
224
|
-
return this.
|
|
225
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
225
226
|
}
|
|
226
227
|
wrapWebAbility(ability, handler) {
|
|
227
228
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
228
229
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
229
230
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
230
231
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
231
|
-
return this.
|
|
232
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
232
233
|
}
|
|
233
234
|
wrapWebGlobalAdmin(handler) {
|
|
234
235
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
235
236
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
236
|
-
return this.
|
|
237
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
237
238
|
}
|
|
238
239
|
wrapAuthenticated(handler) {
|
|
239
240
|
return this.wrap("authenticated", handler);
|
|
@@ -781,7 +782,7 @@ import { withMiddleware as withMiddleware2 } from "@getstrata/core/http/routeMid
|
|
|
781
782
|
import { createScimThrottleMiddleware } from "@getstrata/core/http/scimThrottleMiddleware";
|
|
782
783
|
|
|
783
784
|
// ../../src/modules/scim/controller.ts
|
|
784
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
785
|
+
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
785
786
|
|
|
786
787
|
// ../../src/modules/scim/scimResponse.ts
|
|
787
788
|
import {
|
|
@@ -1246,30 +1247,30 @@ class ScimController {
|
|
|
1246
1247
|
constructor(dependencies, service = createScimService(dependencies)) {
|
|
1247
1248
|
this.service = service;
|
|
1248
1249
|
}
|
|
1249
|
-
serviceProviderConfig =
|
|
1250
|
+
serviceProviderConfig = withErrorHandling2(async () => {
|
|
1250
1251
|
return scimResponse(this.service.serviceProviderConfig());
|
|
1251
1252
|
});
|
|
1252
|
-
listUsers =
|
|
1253
|
+
listUsers = withErrorHandling2(async (request) => {
|
|
1253
1254
|
const url = new URL(request.url);
|
|
1254
1255
|
const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
|
|
1255
1256
|
const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
|
|
1256
1257
|
return scimResponse(await this.service.listUsers(startIndex, count));
|
|
1257
1258
|
});
|
|
1258
|
-
createUser =
|
|
1259
|
+
createUser = withErrorHandling2(async (request) => {
|
|
1259
1260
|
const payload = await request.json();
|
|
1260
1261
|
const user = await this.service.createUser(payload);
|
|
1261
1262
|
const id = Number.parseInt(user.id, 10);
|
|
1262
1263
|
const record = await this.service.findUserRecord(id);
|
|
1263
1264
|
return scimResponse(user, { status: 201, etagSource: record });
|
|
1264
1265
|
});
|
|
1265
|
-
showUser =
|
|
1266
|
+
showUser = withErrorHandling2(async (request) => {
|
|
1266
1267
|
const params = request.params;
|
|
1267
1268
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1268
1269
|
const record = await this.service.findUserRecord(id);
|
|
1269
1270
|
const user = await this.service.getUser(id);
|
|
1270
1271
|
return scimResponse(user, { request, etagSource: record });
|
|
1271
1272
|
});
|
|
1272
|
-
patchUser =
|
|
1273
|
+
patchUser = withErrorHandling2(async (request) => {
|
|
1273
1274
|
const params = request.params;
|
|
1274
1275
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1275
1276
|
const record = await this.service.findUserRecord(id);
|
|
@@ -1279,7 +1280,7 @@ class ScimController {
|
|
|
1279
1280
|
const updated = await this.service.findUserRecord(id);
|
|
1280
1281
|
return scimResponse(user, { etagSource: updated });
|
|
1281
1282
|
});
|
|
1282
|
-
deleteUser =
|
|
1283
|
+
deleteUser = withErrorHandling2(async (request) => {
|
|
1283
1284
|
const params = request.params;
|
|
1284
1285
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1285
1286
|
const record = await this.service.findUserRecord(id);
|
|
@@ -1287,20 +1288,20 @@ class ScimController {
|
|
|
1287
1288
|
await this.service.deleteUser(id);
|
|
1288
1289
|
return new Response(null, { status: 204 });
|
|
1289
1290
|
});
|
|
1290
|
-
listGroups =
|
|
1291
|
+
listGroups = withErrorHandling2(async (request) => {
|
|
1291
1292
|
const url = new URL(request.url);
|
|
1292
1293
|
const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
|
|
1293
1294
|
const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
|
|
1294
1295
|
return scimResponse(await this.service.listGroups(startIndex, count));
|
|
1295
1296
|
});
|
|
1296
|
-
showGroup =
|
|
1297
|
+
showGroup = withErrorHandling2(async (request) => {
|
|
1297
1298
|
const params = request.params;
|
|
1298
1299
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1299
1300
|
const record = await this.service.findOrganizationRecord(id);
|
|
1300
1301
|
const group = await this.service.getGroup(id);
|
|
1301
1302
|
return scimResponse(group, { request, etagSource: record });
|
|
1302
1303
|
});
|
|
1303
|
-
patchGroup =
|
|
1304
|
+
patchGroup = withErrorHandling2(async (request) => {
|
|
1304
1305
|
const params = request.params;
|
|
1305
1306
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1306
1307
|
const record = await this.service.findOrganizationRecord(id);
|
|
@@ -46,6 +46,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
46
46
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
47
47
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
48
48
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
49
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
49
50
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
50
51
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
51
52
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -180,7 +181,7 @@ class HttpKernel {
|
|
|
180
181
|
return this.wrap(["api", "authenticated"], handler);
|
|
181
182
|
}
|
|
182
183
|
wrapWeb(handler) {
|
|
183
|
-
return this.wrap("web", handler);
|
|
184
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
184
185
|
}
|
|
185
186
|
wrapWebPublicRead(handler) {
|
|
186
187
|
if (isPublicReadsEnabled()) {
|
|
@@ -190,19 +191,19 @@ class HttpKernel {
|
|
|
190
191
|
}
|
|
191
192
|
wrapWebAuthenticated(handler) {
|
|
192
193
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
193
|
-
return this.
|
|
194
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
194
195
|
}
|
|
195
196
|
wrapWebAbility(ability, handler) {
|
|
196
197
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
197
198
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
198
199
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
199
200
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
200
|
-
return this.
|
|
201
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
201
202
|
}
|
|
202
203
|
wrapWebGlobalAdmin(handler) {
|
|
203
204
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
204
205
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
205
|
-
return this.
|
|
206
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
206
207
|
}
|
|
207
208
|
wrapAuthenticated(handler) {
|
|
208
209
|
return this.wrap("authenticated", handler);
|
|
@@ -506,68 +506,6 @@ var coreProviders = [
|
|
|
506
506
|
viewProvider
|
|
507
507
|
];
|
|
508
508
|
|
|
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
509
|
// ../../src/bootstrap/context.ts
|
|
572
510
|
function collectProviders(modules = discoverModules()) {
|
|
573
511
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
@@ -578,7 +516,6 @@ function runProviderPhase(providers, phase, context) {
|
|
|
578
516
|
}
|
|
579
517
|
}
|
|
580
518
|
function createAppContext() {
|
|
581
|
-
assertProductionSecrets();
|
|
582
519
|
const container = new ServiceContainer;
|
|
583
520
|
const config = new ConfigStore;
|
|
584
521
|
const dependencies = {
|
|
@@ -17,6 +17,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
17
17
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
18
18
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
19
19
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
20
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
20
21
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
21
22
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
22
23
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -169,7 +170,7 @@ class HttpKernel {
|
|
|
169
170
|
return this.wrap(["api", "authenticated"], handler);
|
|
170
171
|
}
|
|
171
172
|
wrapWeb(handler) {
|
|
172
|
-
return this.wrap("web", handler);
|
|
173
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
173
174
|
}
|
|
174
175
|
wrapWebPublicRead(handler) {
|
|
175
176
|
if (isPublicReadsEnabled()) {
|
|
@@ -179,19 +180,19 @@ class HttpKernel {
|
|
|
179
180
|
}
|
|
180
181
|
wrapWebAuthenticated(handler) {
|
|
181
182
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
182
|
-
return this.
|
|
183
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
183
184
|
}
|
|
184
185
|
wrapWebAbility(ability, handler) {
|
|
185
186
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
186
187
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
187
188
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
188
189
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
189
|
-
return this.
|
|
190
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
190
191
|
}
|
|
191
192
|
wrapWebGlobalAdmin(handler) {
|
|
192
193
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
193
194
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
194
|
-
return this.
|
|
195
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
195
196
|
}
|
|
196
197
|
wrapAuthenticated(handler) {
|
|
197
198
|
return this.wrap("authenticated", handler);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/web/routing.ts
|
|
5
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
5
|
+
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
6
6
|
|
|
7
7
|
// ../../src/bootstrap/http/securedRouteModelBinding.ts
|
|
8
8
|
import {
|
|
@@ -26,6 +26,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
26
26
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
27
27
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
28
28
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
29
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
29
30
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
30
31
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
31
32
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -178,7 +179,7 @@ class HttpKernel {
|
|
|
178
179
|
return this.wrap(["api", "authenticated"], handler);
|
|
179
180
|
}
|
|
180
181
|
wrapWeb(handler) {
|
|
181
|
-
return this.wrap("web", handler);
|
|
182
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
182
183
|
}
|
|
183
184
|
wrapWebPublicRead(handler) {
|
|
184
185
|
if (isPublicReadsEnabled()) {
|
|
@@ -188,19 +189,19 @@ class HttpKernel {
|
|
|
188
189
|
}
|
|
189
190
|
wrapWebAuthenticated(handler) {
|
|
190
191
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
191
|
-
return this.
|
|
192
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
192
193
|
}
|
|
193
194
|
wrapWebAbility(ability, handler) {
|
|
194
195
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
195
196
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
196
197
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
197
198
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
198
|
-
return this.
|
|
199
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
199
200
|
}
|
|
200
201
|
wrapWebGlobalAdmin(handler) {
|
|
201
202
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
202
203
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
203
|
-
return this.
|
|
204
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
204
205
|
}
|
|
205
206
|
wrapAuthenticated(handler) {
|
|
206
207
|
return this.wrap("authenticated", handler);
|
|
@@ -296,14 +297,14 @@ function toRouteRequest(request) {
|
|
|
296
297
|
return request;
|
|
297
298
|
}
|
|
298
299
|
function wrapSecuredRouteModelByKey(param, resolver, authorization, handler) {
|
|
299
|
-
const bound =
|
|
300
|
+
const bound = withErrorHandling2(securedBindRouteModelByKey(param, resolver, authorization, handler));
|
|
300
301
|
return async (request) => bound(toRouteRequest(request));
|
|
301
302
|
}
|
|
302
303
|
function wrapWebLogin(kernel, handler, onThrottled) {
|
|
303
|
-
return wrapWebThrottle(kernel, "login", handler, onThrottled);
|
|
304
|
+
return kernel.wrapWeb(wrapWebThrottle(kernel, "login", handler, onThrottled));
|
|
304
305
|
}
|
|
305
306
|
function wrapWebRegister(kernel, handler, onThrottled) {
|
|
306
|
-
return wrapWebThrottle(kernel, "register", handler, onThrottled);
|
|
307
|
+
return kernel.wrapWeb(wrapWebThrottle(kernel, "register", handler, onThrottled));
|
|
307
308
|
}
|
|
308
309
|
function wrapWebThrottle(kernel, scope, handler, onThrottled) {
|
|
309
310
|
const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
|
|
|
34
34
|
import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
|
|
35
35
|
import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
|
|
36
36
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
37
|
+
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
37
38
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
38
39
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
39
40
|
import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
|
|
@@ -182,7 +183,7 @@ class HttpKernel {
|
|
|
182
183
|
return this.wrap(["api", "authenticated"], handler);
|
|
183
184
|
}
|
|
184
185
|
wrapWeb(handler) {
|
|
185
|
-
return this.wrap("web", handler);
|
|
186
|
+
return withErrorHandling(this.wrap("web", handler));
|
|
186
187
|
}
|
|
187
188
|
wrapWebPublicRead(handler) {
|
|
188
189
|
if (isPublicReadsEnabled()) {
|
|
@@ -192,19 +193,19 @@ class HttpKernel {
|
|
|
192
193
|
}
|
|
193
194
|
wrapWebAuthenticated(handler) {
|
|
194
195
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
195
|
-
return this.
|
|
196
|
+
return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
|
|
196
197
|
}
|
|
197
198
|
wrapWebAbility(ability, handler) {
|
|
198
199
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
199
200
|
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
200
201
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
201
202
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
202
|
-
return this.
|
|
203
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
203
204
|
}
|
|
204
205
|
wrapWebGlobalAdmin(handler) {
|
|
205
206
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
206
207
|
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
207
|
-
return this.
|
|
208
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
208
209
|
}
|
|
209
210
|
wrapAuthenticated(handler) {
|
|
210
211
|
return this.wrap("authenticated", handler);
|
|
@@ -818,68 +819,6 @@ var coreProviders = [
|
|
|
818
819
|
viewProvider
|
|
819
820
|
];
|
|
820
821
|
|
|
821
|
-
// ../../src/bootstrap/secretsGuard.ts
|
|
822
|
-
var DEFAULT_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
823
|
-
var DEFAULT_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
824
|
-
var DEFAULT_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
825
|
-
var DEFAULT_TOKENS = new Set([DEFAULT_ADMIN_API_TOKEN, DEFAULT_MEMBER_API_TOKEN]);
|
|
826
|
-
var DEFAULT_SCIM_TOKENS = new Set([DEFAULT_SCIM_BEARER_TOKEN]);
|
|
827
|
-
function isEnabled(value, defaultEnabled) {
|
|
828
|
-
if (value === undefined) {
|
|
829
|
-
return defaultEnabled;
|
|
830
|
-
}
|
|
831
|
-
return defaultEnabled ? value !== "false" : value === "true";
|
|
832
|
-
}
|
|
833
|
-
function assertProductionSecrets(env = process.env) {
|
|
834
|
-
const appEnv = env.APP_ENV ?? "local";
|
|
835
|
-
if (appEnv !== "production") {
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const adminToken = env.ADMIN_API_TOKEN ?? DEFAULT_ADMIN_API_TOKEN;
|
|
839
|
-
const memberToken = env.MEMBER_API_TOKEN ?? DEFAULT_MEMBER_API_TOKEN;
|
|
840
|
-
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
841
|
-
const encryptionEnabled = isEnabled(env.FEATURE_FIELD_ENCRYPTION, true);
|
|
842
|
-
const devHeadersEnabled = isEnabled(env.AUTH_DEV_HEADERS, true);
|
|
843
|
-
if (devHeadersEnabled) {
|
|
844
|
-
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
845
|
-
}
|
|
846
|
-
if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
|
|
847
|
-
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
848
|
-
}
|
|
849
|
-
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
850
|
-
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
851
|
-
}
|
|
852
|
-
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
853
|
-
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
854
|
-
}
|
|
855
|
-
if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, true)) {
|
|
856
|
-
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
857
|
-
}
|
|
858
|
-
if (isEnabled(env.FEATURE_BILLING, true) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
859
|
-
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
860
|
-
}
|
|
861
|
-
const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
|
|
862
|
-
if (corsOrigins.includes("*")) {
|
|
863
|
-
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
864
|
-
}
|
|
865
|
-
if (isEnabled(env.FEATURE_PUBLIC_READS, true)) {
|
|
866
|
-
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
867
|
-
}
|
|
868
|
-
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
869
|
-
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
870
|
-
}
|
|
871
|
-
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
872
|
-
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
873
|
-
}
|
|
874
|
-
if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
|
|
875
|
-
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
876
|
-
}
|
|
877
|
-
const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
|
|
878
|
-
if (frontendMode === "server-htmx" && !env.SESSION_SECRET?.trim()) {
|
|
879
|
-
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx.");
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
822
|
// ../../src/bootstrap/context.ts
|
|
884
823
|
function collectProviders(modules = discoverModules()) {
|
|
885
824
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
@@ -890,7 +829,6 @@ function runProviderPhase(providers, phase, context) {
|
|
|
890
829
|
}
|
|
891
830
|
}
|
|
892
831
|
function createAppContext() {
|
|
893
|
-
assertProductionSecrets();
|
|
894
832
|
const container = new ServiceContainer;
|
|
895
833
|
const config = new ConfigStore;
|
|
896
834
|
const dependencies = {
|
|
@@ -960,6 +898,67 @@ import {
|
|
|
960
898
|
} from "@getstrata/core/http/securedRouteModelBinding";
|
|
961
899
|
// ../../src/bootstrap/membershipService.ts
|
|
962
900
|
import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
|
|
901
|
+
// ../../src/bootstrap/secretsGuard.ts
|
|
902
|
+
var DEFAULT_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
903
|
+
var DEFAULT_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
904
|
+
var DEFAULT_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
905
|
+
var DEFAULT_TOKENS = new Set([DEFAULT_ADMIN_API_TOKEN, DEFAULT_MEMBER_API_TOKEN]);
|
|
906
|
+
var DEFAULT_SCIM_TOKENS = new Set([DEFAULT_SCIM_BEARER_TOKEN]);
|
|
907
|
+
function isEnabled(value, defaultEnabled) {
|
|
908
|
+
if (value === undefined) {
|
|
909
|
+
return defaultEnabled;
|
|
910
|
+
}
|
|
911
|
+
return defaultEnabled ? value !== "false" : value === "true";
|
|
912
|
+
}
|
|
913
|
+
function assertProductionSecrets(env = process.env) {
|
|
914
|
+
const appEnv = env.APP_ENV ?? "local";
|
|
915
|
+
if (appEnv !== "production") {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const adminToken = env.ADMIN_API_TOKEN ?? DEFAULT_ADMIN_API_TOKEN;
|
|
919
|
+
const memberToken = env.MEMBER_API_TOKEN ?? DEFAULT_MEMBER_API_TOKEN;
|
|
920
|
+
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
921
|
+
const encryptionEnabled = isEnabled(env.FEATURE_FIELD_ENCRYPTION, true);
|
|
922
|
+
const devHeadersEnabled = isEnabled(env.AUTH_DEV_HEADERS, true);
|
|
923
|
+
if (devHeadersEnabled) {
|
|
924
|
+
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
925
|
+
}
|
|
926
|
+
if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
|
|
927
|
+
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
928
|
+
}
|
|
929
|
+
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
930
|
+
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
931
|
+
}
|
|
932
|
+
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
933
|
+
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
934
|
+
}
|
|
935
|
+
if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, true)) {
|
|
936
|
+
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
937
|
+
}
|
|
938
|
+
if (isEnabled(env.FEATURE_BILLING, true) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
939
|
+
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
940
|
+
}
|
|
941
|
+
const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
|
|
942
|
+
if (corsOrigins.includes("*")) {
|
|
943
|
+
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
944
|
+
}
|
|
945
|
+
if (isEnabled(env.FEATURE_PUBLIC_READS, true)) {
|
|
946
|
+
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
947
|
+
}
|
|
948
|
+
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
949
|
+
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
950
|
+
}
|
|
951
|
+
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
952
|
+
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
953
|
+
}
|
|
954
|
+
if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
|
|
955
|
+
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
956
|
+
}
|
|
957
|
+
const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
|
|
958
|
+
if (frontendMode === "server-htmx" && !env.SESSION_SECRET?.trim()) {
|
|
959
|
+
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx.");
|
|
960
|
+
}
|
|
961
|
+
}
|
|
963
962
|
// ../../src/bootstrap/web/forms.ts
|
|
964
963
|
import {
|
|
965
964
|
createCsrfProtection
|
|
@@ -993,7 +992,7 @@ async function parseFormBody(request) {
|
|
|
993
992
|
return { fields, files };
|
|
994
993
|
}
|
|
995
994
|
// ../../src/bootstrap/web/routing.ts
|
|
996
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
995
|
+
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
997
996
|
function routeParams(request) {
|
|
998
997
|
const normalized = {};
|
|
999
998
|
const raw = request.params;
|
|
@@ -1015,14 +1014,14 @@ function toRouteRequest(request) {
|
|
|
1015
1014
|
return request;
|
|
1016
1015
|
}
|
|
1017
1016
|
function wrapSecuredRouteModelByKey(param, resolver, authorization, handler) {
|
|
1018
|
-
const bound =
|
|
1017
|
+
const bound = withErrorHandling2(securedBindRouteModelByKey(param, resolver, authorization, handler));
|
|
1019
1018
|
return async (request) => bound(toRouteRequest(request));
|
|
1020
1019
|
}
|
|
1021
1020
|
function wrapWebLogin(kernel, handler, onThrottled) {
|
|
1022
|
-
return wrapWebThrottle(kernel, "login", handler, onThrottled);
|
|
1021
|
+
return kernel.wrapWeb(wrapWebThrottle(kernel, "login", handler, onThrottled));
|
|
1023
1022
|
}
|
|
1024
1023
|
function wrapWebRegister(kernel, handler, onThrottled) {
|
|
1025
|
-
return wrapWebThrottle(kernel, "register", handler, onThrottled);
|
|
1024
|
+
return kernel.wrapWeb(wrapWebThrottle(kernel, "register", handler, onThrottled));
|
|
1026
1025
|
}
|
|
1027
1026
|
function wrapWebThrottle(kernel, scope, handler, onThrottled) {
|
|
1028
1027
|
const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/bootstrap",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
4
4
|
"description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"access": "public"
|
|
180
180
|
},
|
|
181
181
|
"peerDependencies": {
|
|
182
|
-
"@getstrata/core": "^0.5.
|
|
182
|
+
"@getstrata/core": "^0.5.58",
|
|
183
183
|
"typescript": "^5.9.0"
|
|
184
184
|
}
|
|
185
185
|
}
|