@getstrata/bootstrap 0.2.47 → 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 +6 -0
- package/dist/_.._/_.._/index.html +174 -0
- package/dist/bootstrap/discoverModules.d.ts +2 -1
- package/dist/bootstrap/inProcessCron.d.ts +4 -0
- package/dist/bootstrap/metricsRoutes.d.ts +1 -1
- package/dist/bootstrap/public-api.d.ts +0 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/entries/applicationRegistry.js +8 -8
- package/dist/entries/buildModuleRoutes.js +14 -7
- package/dist/entries/buildWebModuleRoutes.js +13 -6
- package/dist/entries/cache/modelCacheTags.js +9 -3
- package/dist/entries/config.js +19 -19
- package/dist/entries/context.js +10 -91
- package/dist/entries/contracts.js +4 -4
- package/dist/entries/createRoutes.js +56 -126
- package/dist/entries/createSpaRoutes.js +2 -2
- package/dist/entries/createWebRoutes.js +15 -8
- package/dist/entries/dependencies.js +9 -90
- package/dist/entries/discoverModules.js +10 -3
- package/dist/entries/health.js +2 -2
- package/dist/entries/http/securedRouteModelBinding.js +2 -2
- package/dist/entries/httpKernel.js +7 -6
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +7 -1
- package/dist/entries/metricsRoutes.js +31 -5
- package/dist/entries/providers/view.js +2 -2
- package/dist/entries/providers.js +7 -1
- package/dist/entries/routeRegistry.js +2 -2
- package/dist/entries/secretsGuard.js +23 -56
- package/dist/entries/web/forms.js +2 -2
- package/dist/entries/web/routing.js +14 -13
- package/dist/entries/web/server.js +2 -2
- package/dist/index.js +133 -220
- package/package.json +3 -3
|
@@ -7,7 +7,7 @@ import { jsonResponse as jsonResponse4 } from "@getstrata/core/http/response";
|
|
|
7
7
|
import { htmlResponse as htmlResponse2 } from "@getstrata/core/view";
|
|
8
8
|
|
|
9
9
|
// ../../index.html
|
|
10
|
-
var strata_default = __jsonParse("{\"index\":\"
|
|
10
|
+
var strata_default = __jsonParse("{\"index\":\"_.._/_.._/index.html\",\"files\":[{\"input\":\"../../index.html\",\"path\":\"./index-sfreg6q3.js\",\"loader\":\"js\",\"isEntry\":true,\"headers\":{\"etag\":\"Oc_YLpCmV6M\",\"content-type\":\"text/javascript;charset=utf-8\"}},{\"input\":\"../../index.html\",\"path\":\"_.._/_.._/index.html\",\"loader\":\"html\",\"isEntry\":true,\"headers\":{\"etag\":\"rWY90LWIcx8\",\"content-type\":\"text/html;charset=utf-8\"}}]}");
|
|
11
11
|
|
|
12
12
|
// ../../src/config/app.ts
|
|
13
13
|
var appConfig = {
|
|
@@ -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 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 withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
|
|
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 withMiddleware(...middleware)(handler);
|
|
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 withMiddleware(...middleware)(handler);
|
|
237
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
237
238
|
}
|
|
238
239
|
wrapAuthenticated(handler) {
|
|
239
240
|
return this.wrap("authenticated", handler);
|
|
@@ -332,7 +333,7 @@ function resolveModulesDirectory(options) {
|
|
|
332
333
|
if (state.configuredModulesDir) {
|
|
333
334
|
return state.configuredModulesDir;
|
|
334
335
|
}
|
|
335
|
-
|
|
336
|
+
throw new Error("configureModulesDirectory() must be called before discovering modules. WorkHub and the starter do this from preload.");
|
|
336
337
|
}
|
|
337
338
|
async function loadDiscoveredModules(options) {
|
|
338
339
|
const modulesDirectory = resolveModulesDirectory(options);
|
|
@@ -366,6 +367,12 @@ async function ensureModulesLoaded(options) {
|
|
|
366
367
|
function discoverModules() {
|
|
367
368
|
return readDiscoverModulesState().appModules;
|
|
368
369
|
}
|
|
370
|
+
function resetDiscoverModulesForTests() {
|
|
371
|
+
const state = readDiscoverModulesState();
|
|
372
|
+
state.configuredModulesDir = undefined;
|
|
373
|
+
state.appModules.length = 0;
|
|
374
|
+
state.modulesReady = undefined;
|
|
375
|
+
}
|
|
369
376
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
370
377
|
function prefixRouteMap(prefix, routes) {
|
|
371
378
|
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
@@ -496,7 +503,7 @@ function buildWebModuleRoutes(dependencies, options = {}) {
|
|
|
496
503
|
routeRegistry.clear();
|
|
497
504
|
}
|
|
498
505
|
const kernel = createHttpKernel(dependencies);
|
|
499
|
-
const middleware =
|
|
506
|
+
const middleware = kernel.globalMiddleware();
|
|
500
507
|
const moduleRoutes = { ...seedRoutes };
|
|
501
508
|
for (const module of modules) {
|
|
502
509
|
if (!module.webRoutes) {
|
|
@@ -731,15 +738,41 @@ function createHealthRoutes(dependencies) {
|
|
|
731
738
|
}
|
|
732
739
|
|
|
733
740
|
// ../../src/bootstrap/metricsRoutes.ts
|
|
741
|
+
import { timingSafeEqual } from "crypto";
|
|
734
742
|
import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
|
|
743
|
+
function tokensMatch(left, right) {
|
|
744
|
+
const leftBuffer = Buffer.from(left);
|
|
745
|
+
const rightBuffer = Buffer.from(right);
|
|
746
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
750
|
+
}
|
|
751
|
+
function authorizeMetrics(request) {
|
|
752
|
+
const expected = process.env.METRICS_TOKEN?.trim();
|
|
753
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
754
|
+
const presented = authorization.startsWith("Bearer ") ? authorization.slice("Bearer ".length) : "";
|
|
755
|
+
if (expected) {
|
|
756
|
+
return presented.length > 0 && tokensMatch(presented, expected);
|
|
757
|
+
}
|
|
758
|
+
if ((process.env.APP_ENV ?? "local") === "production") {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
735
763
|
function createMetricsRoutes() {
|
|
736
764
|
return {
|
|
737
|
-
"/metrics": async () =>
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
"content-type": "text/plain; version=0.0.4; charset=utf-8"
|
|
765
|
+
"/metrics": async (request) => {
|
|
766
|
+
if (!authorizeMetrics(request)) {
|
|
767
|
+
return new Response("Not Found", { status: 404 });
|
|
741
768
|
}
|
|
742
|
-
|
|
769
|
+
return new Response(prometheusRegistry.renderMetrics(), {
|
|
770
|
+
status: 200,
|
|
771
|
+
headers: {
|
|
772
|
+
"content-type": "text/plain; version=0.0.4; charset=utf-8"
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
743
776
|
};
|
|
744
777
|
}
|
|
745
778
|
|
|
@@ -749,7 +782,7 @@ import { withMiddleware as withMiddleware2 } from "@getstrata/core/http/routeMid
|
|
|
749
782
|
import { createScimThrottleMiddleware } from "@getstrata/core/http/scimThrottleMiddleware";
|
|
750
783
|
|
|
751
784
|
// ../../src/modules/scim/controller.ts
|
|
752
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
785
|
+
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
753
786
|
|
|
754
787
|
// ../../src/modules/scim/scimResponse.ts
|
|
755
788
|
import {
|
|
@@ -797,8 +830,6 @@ import { NotFoundError as NotFoundError4 } from "@getstrata/core/errors/http";
|
|
|
797
830
|
import { currentTenantId as currentTenantId4 } from "@getstrata/core/tenant/tenantContext";
|
|
798
831
|
|
|
799
832
|
// ../../src/domain/scim.ts
|
|
800
|
-
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
801
|
-
var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
|
|
802
833
|
var SCIM_SCHEMAS = {
|
|
803
834
|
user: "urn:ietf:params:scim:schemas:core:2.0:User",
|
|
804
835
|
group: "urn:ietf:params:scim:schemas:core:2.0:Group",
|
|
@@ -942,7 +973,7 @@ import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExp
|
|
|
942
973
|
import { verifyTotp } from "@getstrata/core/security/totp";
|
|
943
974
|
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
944
975
|
|
|
945
|
-
// ../../src/
|
|
976
|
+
// ../../src/core/auth/abilityCatalog.ts
|
|
946
977
|
var MEMBER_ABILITIES = [
|
|
947
978
|
"organizations:read",
|
|
948
979
|
"projects:read",
|
|
@@ -972,107 +1003,6 @@ var ADMIN_ABILITIES = [
|
|
|
972
1003
|
"webhooks:write",
|
|
973
1004
|
"audit:read"
|
|
974
1005
|
];
|
|
975
|
-
var PLATFORM_ADMIN_ABILITIES = ["*"];
|
|
976
|
-
function resolveAbilitiesForRole(role) {
|
|
977
|
-
if (role === "admin") {
|
|
978
|
-
return [...PLATFORM_ADMIN_ABILITIES];
|
|
979
|
-
}
|
|
980
|
-
return [...MEMBER_ABILITIES];
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// ../../src/modules/user/authService.ts
|
|
984
|
-
class AuthService {
|
|
985
|
-
users;
|
|
986
|
-
tokens;
|
|
987
|
-
oauthIdentities;
|
|
988
|
-
oauthProviders = new Map;
|
|
989
|
-
constructor(users, tokens, oauthIdentities) {
|
|
990
|
-
this.users = users;
|
|
991
|
-
this.tokens = tokens;
|
|
992
|
-
this.oauthIdentities = oauthIdentities;
|
|
993
|
-
}
|
|
994
|
-
registerOAuthProvider(provider) {
|
|
995
|
-
this.oauthProviders.set(provider.name, provider);
|
|
996
|
-
}
|
|
997
|
-
getOAuthProvider(name) {
|
|
998
|
-
return this.oauthProviders.get(name);
|
|
999
|
-
}
|
|
1000
|
-
async loginWithPassword(email, password, options = {}) {
|
|
1001
|
-
const user = await this.users.findByEmail(email);
|
|
1002
|
-
if (!user?.password_hash) {
|
|
1003
|
-
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
1004
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
1005
|
-
}
|
|
1006
|
-
const valid = await verifyPassword(password, user.password_hash);
|
|
1007
|
-
if (!valid) {
|
|
1008
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
1009
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
1010
|
-
}
|
|
1011
|
-
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
1012
|
-
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
1013
|
-
throw new UnauthorizedError("Email address is not verified.");
|
|
1014
|
-
}
|
|
1015
|
-
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
1016
|
-
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
1017
|
-
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
1018
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
1019
|
-
throw new UnauthorizedError("Invalid MFA code.");
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
1023
|
-
return await this.tokens.createToken(user.id, {
|
|
1024
|
-
name: "password-login",
|
|
1025
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
1026
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
async loginWithOAuth(providerName, code) {
|
|
1030
|
-
const provider = this.oauthProviders.get(providerName);
|
|
1031
|
-
if (!provider) {
|
|
1032
|
-
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1033
|
-
}
|
|
1034
|
-
const profile = await provider.exchangeCode(code);
|
|
1035
|
-
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
1036
|
-
logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
|
|
1037
|
-
return await this.tokens.createToken(user.id, {
|
|
1038
|
-
name: `${providerName}-oauth`,
|
|
1039
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
1040
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
buildOAuthAuthorizationUrl(providerName, state) {
|
|
1044
|
-
const provider = this.oauthProviders.get(providerName);
|
|
1045
|
-
if (!provider) {
|
|
1046
|
-
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1047
|
-
}
|
|
1048
|
-
return provider.getAuthorizationUrl(state);
|
|
1049
|
-
}
|
|
1050
|
-
async findOrCreateOAuthUser(providerName, profile) {
|
|
1051
|
-
const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
|
|
1052
|
-
if (existingIdentity) {
|
|
1053
|
-
return await this.users.findByIdOrThrow(existingIdentity.user_id);
|
|
1054
|
-
}
|
|
1055
|
-
const existingUser = await this.users.findByEmail(profile.email);
|
|
1056
|
-
const user = existingUser ?? await this.users.create({
|
|
1057
|
-
name: profile.name,
|
|
1058
|
-
email: profile.email,
|
|
1059
|
-
role: "member",
|
|
1060
|
-
tenant_id: currentTenantId2(),
|
|
1061
|
-
email_verified_at: new Date,
|
|
1062
|
-
created_at: new Date,
|
|
1063
|
-
updated_at: new Date
|
|
1064
|
-
});
|
|
1065
|
-
await this.oauthIdentities.create({
|
|
1066
|
-
user_id: user.id,
|
|
1067
|
-
provider: providerName,
|
|
1068
|
-
provider_user_id: profile.providerUserId,
|
|
1069
|
-
email: profile.email,
|
|
1070
|
-
created_at: new Date
|
|
1071
|
-
});
|
|
1072
|
-
return user;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
1006
|
// ../../src/modules/user/notificationRepository.ts
|
|
1077
1007
|
import { BaseRepository as BaseRepository3 } from "@getstrata/core/database/baseRepository";
|
|
1078
1008
|
|
|
@@ -1317,30 +1247,30 @@ class ScimController {
|
|
|
1317
1247
|
constructor(dependencies, service = createScimService(dependencies)) {
|
|
1318
1248
|
this.service = service;
|
|
1319
1249
|
}
|
|
1320
|
-
serviceProviderConfig =
|
|
1250
|
+
serviceProviderConfig = withErrorHandling2(async () => {
|
|
1321
1251
|
return scimResponse(this.service.serviceProviderConfig());
|
|
1322
1252
|
});
|
|
1323
|
-
listUsers =
|
|
1253
|
+
listUsers = withErrorHandling2(async (request) => {
|
|
1324
1254
|
const url = new URL(request.url);
|
|
1325
1255
|
const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
|
|
1326
1256
|
const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
|
|
1327
1257
|
return scimResponse(await this.service.listUsers(startIndex, count));
|
|
1328
1258
|
});
|
|
1329
|
-
createUser =
|
|
1259
|
+
createUser = withErrorHandling2(async (request) => {
|
|
1330
1260
|
const payload = await request.json();
|
|
1331
1261
|
const user = await this.service.createUser(payload);
|
|
1332
1262
|
const id = Number.parseInt(user.id, 10);
|
|
1333
1263
|
const record = await this.service.findUserRecord(id);
|
|
1334
1264
|
return scimResponse(user, { status: 201, etagSource: record });
|
|
1335
1265
|
});
|
|
1336
|
-
showUser =
|
|
1266
|
+
showUser = withErrorHandling2(async (request) => {
|
|
1337
1267
|
const params = request.params;
|
|
1338
1268
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1339
1269
|
const record = await this.service.findUserRecord(id);
|
|
1340
1270
|
const user = await this.service.getUser(id);
|
|
1341
1271
|
return scimResponse(user, { request, etagSource: record });
|
|
1342
1272
|
});
|
|
1343
|
-
patchUser =
|
|
1273
|
+
patchUser = withErrorHandling2(async (request) => {
|
|
1344
1274
|
const params = request.params;
|
|
1345
1275
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1346
1276
|
const record = await this.service.findUserRecord(id);
|
|
@@ -1350,7 +1280,7 @@ class ScimController {
|
|
|
1350
1280
|
const updated = await this.service.findUserRecord(id);
|
|
1351
1281
|
return scimResponse(user, { etagSource: updated });
|
|
1352
1282
|
});
|
|
1353
|
-
deleteUser =
|
|
1283
|
+
deleteUser = withErrorHandling2(async (request) => {
|
|
1354
1284
|
const params = request.params;
|
|
1355
1285
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1356
1286
|
const record = await this.service.findUserRecord(id);
|
|
@@ -1358,20 +1288,20 @@ class ScimController {
|
|
|
1358
1288
|
await this.service.deleteUser(id);
|
|
1359
1289
|
return new Response(null, { status: 204 });
|
|
1360
1290
|
});
|
|
1361
|
-
listGroups =
|
|
1291
|
+
listGroups = withErrorHandling2(async (request) => {
|
|
1362
1292
|
const url = new URL(request.url);
|
|
1363
1293
|
const startIndex = Number.parseInt(url.searchParams.get("startIndex") ?? "1", 10);
|
|
1364
1294
|
const count = Number.parseInt(url.searchParams.get("count") ?? "100", 10);
|
|
1365
1295
|
return scimResponse(await this.service.listGroups(startIndex, count));
|
|
1366
1296
|
});
|
|
1367
|
-
showGroup =
|
|
1297
|
+
showGroup = withErrorHandling2(async (request) => {
|
|
1368
1298
|
const params = request.params;
|
|
1369
1299
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1370
1300
|
const record = await this.service.findOrganizationRecord(id);
|
|
1371
1301
|
const group = await this.service.getGroup(id);
|
|
1372
1302
|
return scimResponse(group, { request, etagSource: record });
|
|
1373
1303
|
});
|
|
1374
|
-
patchGroup =
|
|
1304
|
+
patchGroup = withErrorHandling2(async (request) => {
|
|
1375
1305
|
const params = request.params;
|
|
1376
1306
|
const id = Number.parseInt(params?.id ?? "", 10);
|
|
1377
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 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 withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
|
|
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 withMiddleware(...middleware)(handler);
|
|
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 withMiddleware(...middleware)(handler);
|
|
206
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
206
207
|
}
|
|
207
208
|
wrapAuthenticated(handler) {
|
|
208
209
|
return this.wrap("authenticated", handler);
|
|
@@ -301,7 +302,7 @@ function resolveModulesDirectory(options) {
|
|
|
301
302
|
if (state.configuredModulesDir) {
|
|
302
303
|
return state.configuredModulesDir;
|
|
303
304
|
}
|
|
304
|
-
|
|
305
|
+
throw new Error("configureModulesDirectory() must be called before discovering modules. WorkHub and the starter do this from preload.");
|
|
305
306
|
}
|
|
306
307
|
async function loadDiscoveredModules(options) {
|
|
307
308
|
const modulesDirectory = resolveModulesDirectory(options);
|
|
@@ -335,6 +336,12 @@ async function ensureModulesLoaded(options) {
|
|
|
335
336
|
function discoverModules() {
|
|
336
337
|
return readDiscoverModulesState().appModules;
|
|
337
338
|
}
|
|
339
|
+
function resetDiscoverModulesForTests() {
|
|
340
|
+
const state = readDiscoverModulesState();
|
|
341
|
+
state.configuredModulesDir = undefined;
|
|
342
|
+
state.appModules.length = 0;
|
|
343
|
+
state.modulesReady = undefined;
|
|
344
|
+
}
|
|
338
345
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
339
346
|
function prefixRouteMap(prefix, routes) {
|
|
340
347
|
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
@@ -423,7 +430,7 @@ function buildWebModuleRoutes(dependencies, options = {}) {
|
|
|
423
430
|
routeRegistry.clear();
|
|
424
431
|
}
|
|
425
432
|
const kernel = createHttpKernel(dependencies);
|
|
426
|
-
const middleware =
|
|
433
|
+
const middleware = kernel.globalMiddleware();
|
|
427
434
|
const moduleRoutes = { ...seedRoutes };
|
|
428
435
|
for (const module of modules) {
|
|
429
436
|
if (!module.webRoutes) {
|
|
@@ -473,6 +480,6 @@ function mergeWebRoutes(dependencies, routes) {
|
|
|
473
480
|
};
|
|
474
481
|
}
|
|
475
482
|
export {
|
|
476
|
-
|
|
477
|
-
|
|
483
|
+
createWebRoutes,
|
|
484
|
+
mergeWebRoutes
|
|
478
485
|
};
|
|
@@ -50,7 +50,7 @@ function resolveModulesDirectory(options) {
|
|
|
50
50
|
if (state.configuredModulesDir) {
|
|
51
51
|
return state.configuredModulesDir;
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
throw new Error("configureModulesDirectory() must be called before discovering modules. WorkHub and the starter do this from preload.");
|
|
54
54
|
}
|
|
55
55
|
async function loadDiscoveredModules(options) {
|
|
56
56
|
const modulesDirectory = resolveModulesDirectory(options);
|
|
@@ -84,6 +84,12 @@ async function ensureModulesLoaded(options) {
|
|
|
84
84
|
function discoverModules() {
|
|
85
85
|
return readDiscoverModulesState().appModules;
|
|
86
86
|
}
|
|
87
|
+
function resetDiscoverModulesForTests() {
|
|
88
|
+
const state = readDiscoverModulesState();
|
|
89
|
+
state.configuredModulesDir = undefined;
|
|
90
|
+
state.appModules.length = 0;
|
|
91
|
+
state.modulesReady = undefined;
|
|
92
|
+
}
|
|
87
93
|
// ../../src/bootstrap/providers/auth.ts
|
|
88
94
|
import {
|
|
89
95
|
AuthManager,
|
|
@@ -500,92 +506,6 @@ var coreProviders = [
|
|
|
500
506
|
viewProvider
|
|
501
507
|
];
|
|
502
508
|
|
|
503
|
-
// ../../src/config/features.ts
|
|
504
|
-
function readFeatureFlags() {
|
|
505
|
-
return {
|
|
506
|
-
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
507
|
-
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
508
|
-
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
509
|
-
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
510
|
-
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
511
|
-
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
512
|
-
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
513
|
-
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
514
|
-
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
515
|
-
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
516
|
-
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
var featureFlags = readFeatureFlags();
|
|
520
|
-
function isFeatureEnabled(feature) {
|
|
521
|
-
return readFeatureFlags()[feature];
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// ../../src/domain/auth.ts
|
|
525
|
-
var TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
526
|
-
var TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
527
|
-
|
|
528
|
-
// ../../src/domain/scim.ts
|
|
529
|
-
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
530
|
-
var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
|
|
531
|
-
var SCIM_SCHEMAS = {
|
|
532
|
-
user: "urn:ietf:params:scim:schemas:core:2.0:User",
|
|
533
|
-
group: "urn:ietf:params:scim:schemas:core:2.0:Group",
|
|
534
|
-
listResponse: "urn:ietf:params:scim:api:messages:2.0:ListResponse",
|
|
535
|
-
patchOp: "urn:ietf:params:scim:api:messages:2.0:PatchOp",
|
|
536
|
-
serviceProviderConfig: "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
// ../../src/bootstrap/secretsGuard.ts
|
|
540
|
-
var DEFAULT_TOKENS = new Set([TEST_ADMIN_API_TOKEN, TEST_MEMBER_API_TOKEN]);
|
|
541
|
-
var DEFAULT_SCIM_TOKENS = new Set([TEST_SCIM_BEARER_TOKEN, DEFAULT_SCIM_BEARER_TOKEN]);
|
|
542
|
-
function assertProductionSecrets(env = process.env) {
|
|
543
|
-
const appEnv = env.APP_ENV ?? appConfig.env;
|
|
544
|
-
if (appEnv !== "production") {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const adminToken = env.ADMIN_API_TOKEN ?? TEST_ADMIN_API_TOKEN;
|
|
548
|
-
const memberToken = env.MEMBER_API_TOKEN ?? TEST_MEMBER_API_TOKEN;
|
|
549
|
-
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
550
|
-
const encryptionEnabled = env.FEATURE_FIELD_ENCRYPTION !== "false";
|
|
551
|
-
const devHeadersEnabled = (env.AUTH_DEV_HEADERS ?? "true") !== "false";
|
|
552
|
-
if (devHeadersEnabled) {
|
|
553
|
-
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
554
|
-
}
|
|
555
|
-
if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
|
|
556
|
-
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
557
|
-
}
|
|
558
|
-
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
559
|
-
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
560
|
-
}
|
|
561
|
-
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
562
|
-
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
563
|
-
}
|
|
564
|
-
if (!env.SIEM_EXPORT_URL?.trim() && isFeatureEnabled("siemExport")) {
|
|
565
|
-
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
566
|
-
}
|
|
567
|
-
const billingEnabled = (env.FEATURE_BILLING ?? "true") !== "false";
|
|
568
|
-
if (billingEnabled && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
569
|
-
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
570
|
-
}
|
|
571
|
-
const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
|
|
572
|
-
if (corsOrigins.includes("*")) {
|
|
573
|
-
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
574
|
-
}
|
|
575
|
-
if ((env.FEATURE_PUBLIC_READS ?? "true") !== "false") {
|
|
576
|
-
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
577
|
-
}
|
|
578
|
-
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
579
|
-
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
580
|
-
}
|
|
581
|
-
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
582
|
-
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
583
|
-
}
|
|
584
|
-
if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
|
|
585
|
-
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
509
|
// ../../src/bootstrap/context.ts
|
|
590
510
|
function collectProviders(modules = discoverModules()) {
|
|
591
511
|
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
@@ -596,7 +516,6 @@ function runProviderPhase(providers, phase, context) {
|
|
|
596
516
|
}
|
|
597
517
|
}
|
|
598
518
|
function createAppContext() {
|
|
599
|
-
assertProductionSecrets();
|
|
600
519
|
const container = new ServiceContainer;
|
|
601
520
|
const config = new ConfigStore;
|
|
602
521
|
const dependencies = {
|
|
@@ -641,6 +560,6 @@ function createAppDependencies() {
|
|
|
641
560
|
return createAppContext().dependencies;
|
|
642
561
|
}
|
|
643
562
|
export {
|
|
644
|
-
|
|
645
|
-
|
|
563
|
+
createAppContext,
|
|
564
|
+
createAppDependencies
|
|
646
565
|
};
|
|
@@ -26,7 +26,7 @@ function resolveModulesDirectory(options) {
|
|
|
26
26
|
if (state.configuredModulesDir) {
|
|
27
27
|
return state.configuredModulesDir;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
throw new Error("configureModulesDirectory() must be called before discovering modules. WorkHub and the starter do this from preload.");
|
|
30
30
|
}
|
|
31
31
|
async function loadDiscoveredModules(options) {
|
|
32
32
|
const modulesDirectory = resolveModulesDirectory(options);
|
|
@@ -60,8 +60,15 @@ async function ensureModulesLoaded(options) {
|
|
|
60
60
|
function discoverModules() {
|
|
61
61
|
return readDiscoverModulesState().appModules;
|
|
62
62
|
}
|
|
63
|
+
function resetDiscoverModulesForTests() {
|
|
64
|
+
const state = readDiscoverModulesState();
|
|
65
|
+
state.configuredModulesDir = undefined;
|
|
66
|
+
state.appModules.length = 0;
|
|
67
|
+
state.modulesReady = undefined;
|
|
68
|
+
}
|
|
63
69
|
export {
|
|
64
|
-
|
|
70
|
+
configureModulesDirectory,
|
|
65
71
|
discoverModules,
|
|
66
|
-
|
|
72
|
+
ensureModulesLoaded,
|
|
73
|
+
resetDiscoverModulesForTests
|
|
67
74
|
};
|
package/dist/entries/health.js
CHANGED
|
@@ -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 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 withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
|
|
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 withMiddleware(...middleware)(handler);
|
|
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 withMiddleware(...middleware)(handler);
|
|
195
|
+
return this.wrapWeb(withMiddleware(...middleware)(handler));
|
|
195
196
|
}
|
|
196
197
|
wrapAuthenticated(handler) {
|
|
197
198
|
return this.wrap("authenticated", handler);
|
|
@@ -265,6 +266,6 @@ function createHttpKernel(dependencies) {
|
|
|
265
266
|
return new HttpKernel(dependencies);
|
|
266
267
|
}
|
|
267
268
|
export {
|
|
268
|
-
|
|
269
|
-
|
|
269
|
+
HttpKernel,
|
|
270
|
+
createHttpKernel
|
|
270
271
|
};
|
|
@@ -44,7 +44,7 @@ function resolveModulesDirectory(options) {
|
|
|
44
44
|
if (state.configuredModulesDir) {
|
|
45
45
|
return state.configuredModulesDir;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
throw new Error("configureModulesDirectory() must be called before discovering modules. WorkHub and the starter do this from preload.");
|
|
48
48
|
}
|
|
49
49
|
async function loadDiscoveredModules(options) {
|
|
50
50
|
const modulesDirectory = resolveModulesDirectory(options);
|
|
@@ -78,6 +78,12 @@ async function ensureModulesLoaded(options) {
|
|
|
78
78
|
function discoverModules() {
|
|
79
79
|
return readDiscoverModulesState().appModules;
|
|
80
80
|
}
|
|
81
|
+
function resetDiscoverModulesForTests() {
|
|
82
|
+
const state = readDiscoverModulesState();
|
|
83
|
+
state.configuredModulesDir = undefined;
|
|
84
|
+
state.appModules.length = 0;
|
|
85
|
+
state.modulesReady = undefined;
|
|
86
|
+
}
|
|
81
87
|
|
|
82
88
|
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
83
89
|
function cacheTagsForModelWrite(tableName, action) {
|