@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.
@@ -3,15 +3,40 @@ var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/web/session.ts
5
5
  import { createHash, randomBytes } from "crypto";
6
+ import { AuthManager } from "@getstrata/core/auth/guard";
7
+ import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
8
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
6
9
  import { readRequestCookie } from "@getstrata/core/http/cookies";
10
+ function isSqlClient(value) {
11
+ return typeof value.unsafe === "function";
12
+ }
13
+ function resolveSql(source) {
14
+ if (isSqlClient(source)) {
15
+ return source;
16
+ }
17
+ return source();
18
+ }
19
+ function defaultSessionSql() {
20
+ const bound = getBoundDatabaseConnection();
21
+ if (bound) {
22
+ return bound;
23
+ }
24
+ return getDefaultDatabasePool();
25
+ }
26
+ function defaultMapSessionUser(user) {
27
+ return {
28
+ id: user.id,
29
+ role: user.is_admin ? "admin" : "member"
30
+ };
31
+ }
7
32
 
8
33
  class CookieSessionStore {
9
- sql;
34
+ sqlSource;
10
35
  secret;
11
36
  cookieName;
12
37
  maxAgeSeconds;
13
- constructor(sql, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
14
- this.sql = sql;
38
+ constructor(sqlSource, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
39
+ this.sqlSource = sqlSource;
15
40
  this.secret = secret;
16
41
  this.cookieName = cookieName;
17
42
  this.maxAgeSeconds = maxAgeSeconds;
@@ -29,10 +54,13 @@ class CookieSessionStore {
29
54
  }
30
55
  return header.includes("Secure") ? header : `${header}; Secure`;
31
56
  }
57
+ sql() {
58
+ return resolveSql(this.sqlSource);
59
+ }
32
60
  async create(user) {
33
61
  const id = randomBytes(32).toString("hex");
34
62
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
35
- await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
63
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
36
64
  id,
37
65
  user.id,
38
66
  expires
@@ -40,7 +68,7 @@ class CookieSessionStore {
40
68
  return id;
41
69
  }
42
70
  async destroy(sessionId) {
43
- await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
71
+ await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
44
72
  }
45
73
  async read(request) {
46
74
  const cookie = readRequestCookie(request, this.cookieName);
@@ -51,7 +79,7 @@ class CookieSessionStore {
51
79
  if (!sessionId || !signature || signature !== this.sign(sessionId)) {
52
80
  return null;
53
81
  }
54
- const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
82
+ const rows = await this.sql().unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
55
83
  COALESCE(u.is_admin, false) AS is_admin
56
84
  FROM sessions s
57
85
  INNER JOIN users u ON u.id = s.user_id
@@ -71,6 +99,30 @@ class CookieSessionStore {
71
99
  return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
72
100
  }
73
101
  }
102
+
103
+ class CookieSessionGuard {
104
+ store;
105
+ mapUser;
106
+ constructor(store, mapUser = defaultMapSessionUser) {
107
+ this.store = store;
108
+ this.mapUser = mapUser;
109
+ }
110
+ async resolve(request) {
111
+ const user = await this.store.read(request);
112
+ if (!user) {
113
+ return null;
114
+ }
115
+ return this.mapUser(user);
116
+ }
117
+ }
118
+ function createCookieSessionAuthManager(options = {}) {
119
+ const store = options.store ?? new CookieSessionStore(options.sql ?? defaultSessionSql, options.secret ?? process.env.SESSION_SECRET?.trim() ?? "", options.cookieName, options.maxAgeSeconds);
120
+ return new AuthManager(new CookieSessionGuard(store, options.mapUser ?? defaultMapSessionUser));
121
+ }
74
122
  export {
75
- CookieSessionStore
123
+ CookieSessionGuard,
124
+ CookieSessionStore,
125
+ createCookieSessionAuthManager,
126
+ defaultMapSessionUser,
127
+ defaultSessionSql
76
128
  };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
20
20
 
21
21
  // ../../src/bootstrap/httpKernel.ts
22
22
  import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
23
+ import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
23
24
  import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
24
25
  import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
25
26
  import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
@@ -34,6 +35,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
34
35
  import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
35
36
  import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
36
37
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
38
+ import { withErrorHandling } from "@getstrata/core/http/response";
37
39
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
38
40
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
39
41
  import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
@@ -76,24 +78,38 @@ function parsePositiveInt(value, fallback) {
76
78
  }
77
79
  return Math.trunc(parsed);
78
80
  }
81
+ function parseWindowSeconds(secondsValue, msValue, fallback) {
82
+ if (secondsValue !== undefined && secondsValue.trim() !== "") {
83
+ return parsePositiveInt(secondsValue, fallback);
84
+ }
85
+ if (msValue !== undefined && msValue.trim() !== "") {
86
+ const parsedMs = Number(msValue);
87
+ if (Number.isFinite(parsedMs) && parsedMs > 0) {
88
+ return Math.max(1, Math.trunc(parsedMs / 1000));
89
+ }
90
+ }
91
+ return fallback;
92
+ }
79
93
  function resolveLoginRateLimit() {
80
94
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
81
95
  return {
82
96
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
83
- decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
97
+ decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
84
98
  };
85
99
  }
86
100
  function resolveRegisterRateLimit() {
87
101
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
88
102
  return {
89
103
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
90
- decaySeconds: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS ?? process.env.REGISTER_RATE_LIMIT_WINDOW_MS ? String(Number(process.env.REGISTER_RATE_LIMIT_WINDOW_MS) / 1000) : undefined, defaults.decaySeconds)
104
+ decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
91
105
  };
92
106
  }
93
107
 
94
108
  // ../../src/bootstrap/config.ts
95
109
  import {
110
+ CORE_ABILITY_CHECKER_TOKEN,
96
111
  CORE_AUTH_TOKEN,
112
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
97
113
  CORE_CACHE_TOKEN,
98
114
  CORE_CONFIG_TOKEN,
99
115
  CORE_EVENT_BUS_TOKEN,
@@ -182,7 +198,7 @@ class HttpKernel {
182
198
  return this.wrap(["api", "authenticated"], handler);
183
199
  }
184
200
  wrapWeb(handler) {
185
- return this.wrap("web", handler);
201
+ return withErrorHandling(this.wrap("web", handler));
186
202
  }
187
203
  wrapWebPublicRead(handler) {
188
204
  if (isPublicReadsEnabled()) {
@@ -192,19 +208,19 @@ class HttpKernel {
192
208
  }
193
209
  wrapWebAuthenticated(handler) {
194
210
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
195
- return this.wrap("web", withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
211
+ return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
196
212
  }
197
213
  wrapWebAbility(ability, handler) {
198
214
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
199
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
215
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
200
216
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
201
217
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
202
- return this.wrap("web", withMiddleware(...middleware)(handler));
218
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
203
219
  }
204
220
  wrapWebGlobalAdmin(handler) {
205
221
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
206
222
  const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
207
- return this.wrap("web", withMiddleware(...middleware)(handler));
223
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
208
224
  }
209
225
  wrapAuthenticated(handler) {
210
226
  return this.wrap("authenticated", handler);
@@ -220,7 +236,7 @@ class HttpKernel {
220
236
  return withMiddleware(...middleware)(handler);
221
237
  }
222
238
  wrapAbility(ability, handler) {
223
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
239
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
224
240
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
225
241
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
226
242
  return withMiddleware(...middleware)(handler);
@@ -818,68 +834,6 @@ var coreProviders = [
818
834
  viewProvider
819
835
  ];
820
836
 
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
837
  // ../../src/bootstrap/context.ts
884
838
  function collectProviders(modules = discoverModules()) {
885
839
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
@@ -890,7 +844,6 @@ function runProviderPhase(providers, phase, context) {
890
844
  }
891
845
  }
892
846
  function createAppContext() {
893
- assertProductionSecrets();
894
847
  const container = new ServiceContainer;
895
848
  const config = new ConfigStore;
896
849
  const dependencies = {
@@ -953,6 +906,101 @@ function mergeWebRoutes(dependencies, routes) {
953
906
  function createAppDependencies() {
954
907
  return createAppContext().dependencies;
955
908
  }
909
+ // ../../src/bootstrap/health.ts
910
+ import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
911
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
912
+ import { jsonResponse } from "@getstrata/core/http/response";
913
+ var {RedisClient } = globalThis.Bun;
914
+ function resolveRedisUrl(dependencies) {
915
+ if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
916
+ return process.env.REDIS_URL?.trim() || undefined;
917
+ }
918
+ const config = dependencies.container.resolve(CORE_CONFIG_TOKEN);
919
+ const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
920
+ return redisUrl || undefined;
921
+ }
922
+ function resolveHealthDatabase() {
923
+ const bound = getBoundDatabaseConnection();
924
+ if (bound) {
925
+ return bound;
926
+ }
927
+ try {
928
+ return getDefaultDatabasePool();
929
+ } catch {
930
+ return null;
931
+ }
932
+ }
933
+ async function pingDatabaseClient(connection) {
934
+ try {
935
+ await connection.unsafe("SELECT 1");
936
+ return true;
937
+ } catch {
938
+ return false;
939
+ }
940
+ }
941
+ async function checkDatabase() {
942
+ const connection = resolveHealthDatabase();
943
+ if (!connection) {
944
+ return false;
945
+ }
946
+ return await pingDatabaseClient(connection);
947
+ }
948
+ async function pingDatabase() {
949
+ return await checkDatabase();
950
+ }
951
+ async function checkRedis(redisUrl) {
952
+ try {
953
+ const client = new RedisClient(redisUrl);
954
+ const response = await client.ping();
955
+ return response === "PONG";
956
+ } catch {
957
+ return false;
958
+ }
959
+ }
960
+ async function resolveExtraFields(extra) {
961
+ if (!extra) {
962
+ return {};
963
+ }
964
+ return typeof extra === "function" ? await extra() : extra;
965
+ }
966
+ async function collectDependencyChecks(dependencies) {
967
+ const checks = {
968
+ database: await checkDatabase() ? "ok" : "error"
969
+ };
970
+ const redisUrl = resolveRedisUrl(dependencies);
971
+ if (redisUrl) {
972
+ checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
973
+ } else {
974
+ checks.redis = "skipped";
975
+ }
976
+ const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
977
+ return { checks, ready };
978
+ }
979
+ function createHealthRoutes(dependencies, options = {}) {
980
+ return {
981
+ "/health": async () => {
982
+ const extra = await resolveExtraFields(options.extra);
983
+ if (!options.pingOnHealth) {
984
+ return jsonResponse({ status: "ok", ...extra });
985
+ }
986
+ const { checks, ready } = await collectDependencyChecks(dependencies);
987
+ return jsonResponse({
988
+ status: ready ? "ok" : "error",
989
+ checks,
990
+ ...extra
991
+ }, { status: ready ? 200 : 503 });
992
+ },
993
+ "/ready": async () => {
994
+ const extra = await resolveExtraFields(options.extra);
995
+ const { checks, ready } = await collectDependencyChecks(dependencies);
996
+ return jsonResponse({
997
+ status: ready ? "ready" : "not_ready",
998
+ checks,
999
+ ...extra
1000
+ }, { status: ready ? 200 : 503 });
1001
+ }
1002
+ };
1003
+ }
956
1004
  // ../../src/bootstrap/http/securedRouteModelBinding.ts
957
1005
  import {
958
1006
  securedBindRouteModel,
@@ -960,6 +1008,121 @@ import {
960
1008
  } from "@getstrata/core/http/securedRouteModelBinding";
961
1009
  // ../../src/bootstrap/membershipService.ts
962
1010
  import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
1011
+ // ../../src/bootstrap/secretsGuard.ts
1012
+ var DEFAULT_ADMIN_API_TOKEN = "workhub-admin-test-token";
1013
+ var DEFAULT_MEMBER_API_TOKEN = "workhub-member-test-token";
1014
+ var DEFAULT_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
1015
+ var MIN_SESSION_SECRET_LENGTH = 32;
1016
+ var DEFAULT_TOKENS = new Set([DEFAULT_ADMIN_API_TOKEN, DEFAULT_MEMBER_API_TOKEN]);
1017
+ var DEFAULT_SCIM_TOKENS = new Set([DEFAULT_SCIM_BEARER_TOKEN]);
1018
+ function isEnabled(value, defaultEnabled) {
1019
+ if (value === undefined) {
1020
+ return defaultEnabled;
1021
+ }
1022
+ return defaultEnabled ? value !== "false" : value === "true";
1023
+ }
1024
+ function isTokenAuthEnabled(env) {
1025
+ return Boolean(env.ADMIN_API_TOKEN?.trim()) || Boolean(env.MEMBER_API_TOKEN?.trim()) || env.FEATURE_API_TOKENS === "true";
1026
+ }
1027
+ function isOAuthEnabled(env) {
1028
+ return isEnabled(env.FEATURE_OAUTH, false) || isEnabled(env.FEATURE_SAML, false) || Boolean(env.GITHUB_CLIENT_ID?.trim()) || Boolean(env.OIDC_ISSUER?.trim()) || Boolean(env.SAML_LOGIN_URL?.trim());
1029
+ }
1030
+ function isCorsConfigured(env) {
1031
+ return env.CORS_ALLOWED_ORIGINS !== undefined;
1032
+ }
1033
+ function assertAuthDevHeadersDisabled(env) {
1034
+ if (isEnabled(env.AUTH_DEV_HEADERS, true)) {
1035
+ throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
1036
+ }
1037
+ }
1038
+ function assertSessionSecret(env) {
1039
+ const secret = env.SESSION_SECRET?.trim() ?? "";
1040
+ if (secret.length < MIN_SESSION_SECRET_LENGTH) {
1041
+ throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx (32+ characters).");
1042
+ }
1043
+ }
1044
+ function assertWorkHubProductionSecrets(env) {
1045
+ const adminToken = env.ADMIN_API_TOKEN ?? DEFAULT_ADMIN_API_TOKEN;
1046
+ const memberToken = env.MEMBER_API_TOKEN ?? DEFAULT_MEMBER_API_TOKEN;
1047
+ const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
1048
+ const encryptionEnabled = isEnabled(env.FEATURE_FIELD_ENCRYPTION, true);
1049
+ if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
1050
+ throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
1051
+ }
1052
+ if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
1053
+ throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
1054
+ }
1055
+ if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
1056
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
1057
+ }
1058
+ if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, true)) {
1059
+ console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
1060
+ }
1061
+ if (isEnabled(env.FEATURE_BILLING, true) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
1062
+ throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
1063
+ }
1064
+ const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
1065
+ if (corsOrigins.includes("*")) {
1066
+ throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
1067
+ }
1068
+ if (isEnabled(env.FEATURE_PUBLIC_READS, true)) {
1069
+ throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
1070
+ }
1071
+ if (!env.OAUTH_STATE_SECRET?.trim()) {
1072
+ throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
1073
+ }
1074
+ if (!env.TOKEN_HASH_PEPPER?.trim()) {
1075
+ throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
1076
+ }
1077
+ if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
1078
+ throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
1079
+ }
1080
+ }
1081
+ function assertSiblingProductionSecrets(env) {
1082
+ if (isEnabled(env.FEATURE_SCIM, false)) {
1083
+ const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
1084
+ if (DEFAULT_SCIM_TOKENS.has(scimToken) || !env.SCIM_BEARER_TOKEN?.trim()) {
1085
+ throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
1086
+ }
1087
+ }
1088
+ if (isEnabled(env.FEATURE_FIELD_ENCRYPTION, false) && !env.KMS_ENCRYPTION_KEY?.trim()) {
1089
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
1090
+ }
1091
+ if (isEnabled(env.FEATURE_BILLING, false) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
1092
+ throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
1093
+ }
1094
+ if (isOAuthEnabled(env) && !env.OAUTH_STATE_SECRET?.trim()) {
1095
+ throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
1096
+ }
1097
+ if (isCorsConfigured(env)) {
1098
+ const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
1099
+ if (corsOrigins.includes("*")) {
1100
+ throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
1101
+ }
1102
+ }
1103
+ if (isEnabled(env.FEATURE_PUBLIC_READS, false)) {
1104
+ throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
1105
+ }
1106
+ if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, false)) {
1107
+ console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
1108
+ }
1109
+ }
1110
+ function assertProductionSecrets(env = process.env) {
1111
+ const appEnv = env.APP_ENV ?? "local";
1112
+ if (appEnv !== "production") {
1113
+ return;
1114
+ }
1115
+ assertAuthDevHeadersDisabled(env);
1116
+ if (isTokenAuthEnabled(env)) {
1117
+ assertWorkHubProductionSecrets(env);
1118
+ } else {
1119
+ assertSiblingProductionSecrets(env);
1120
+ }
1121
+ const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
1122
+ if (frontendMode === "server-htmx") {
1123
+ assertSessionSecret(env);
1124
+ }
1125
+ }
963
1126
  // ../../src/bootstrap/web/forms.ts
964
1127
  import {
965
1128
  createCsrfProtection
@@ -993,7 +1156,7 @@ async function parseFormBody(request) {
993
1156
  return { fields, files };
994
1157
  }
995
1158
  // ../../src/bootstrap/web/routing.ts
996
- import { withErrorHandling } from "@getstrata/core/http/response";
1159
+ import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
997
1160
  function routeParams(request) {
998
1161
  const normalized = {};
999
1162
  const raw = request.params;
@@ -1015,14 +1178,14 @@ function toRouteRequest(request) {
1015
1178
  return request;
1016
1179
  }
1017
1180
  function wrapSecuredRouteModelByKey(param, resolver, authorization, handler) {
1018
- const bound = withErrorHandling(securedBindRouteModelByKey(param, resolver, authorization, handler));
1181
+ const bound = withErrorHandling2(securedBindRouteModelByKey(param, resolver, authorization, handler));
1019
1182
  return async (request) => bound(toRouteRequest(request));
1020
1183
  }
1021
1184
  function wrapWebLogin(kernel, handler, onThrottled) {
1022
- return wrapWebThrottle(kernel, "login", handler, onThrottled);
1185
+ return kernel.wrapWeb(wrapWebThrottle(kernel, "login", handler, onThrottled));
1023
1186
  }
1024
1187
  function wrapWebRegister(kernel, handler, onThrottled) {
1025
- return wrapWebThrottle(kernel, "register", handler, onThrottled);
1188
+ return kernel.wrapWeb(wrapWebThrottle(kernel, "register", handler, onThrottled));
1026
1189
  }
1027
1190
  function wrapWebThrottle(kernel, scope, handler, onThrottled) {
1028
1191
  const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);
@@ -1091,15 +1254,40 @@ function createWebServer(options) {
1091
1254
  }
1092
1255
  // ../../src/bootstrap/web/session.ts
1093
1256
  import { createHash, randomBytes } from "crypto";
1257
+ import { AuthManager as AuthManager2 } from "@getstrata/core/auth/guard";
1258
+ import { getBoundDatabaseConnection as getBoundDatabaseConnection2 } from "@getstrata/core/database/boundConnection";
1259
+ import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
1094
1260
  import { readRequestCookie } from "@getstrata/core/http/cookies";
1261
+ function isSqlClient(value) {
1262
+ return typeof value.unsafe === "function";
1263
+ }
1264
+ function resolveSql(source) {
1265
+ if (isSqlClient(source)) {
1266
+ return source;
1267
+ }
1268
+ return source();
1269
+ }
1270
+ function defaultSessionSql() {
1271
+ const bound = getBoundDatabaseConnection2();
1272
+ if (bound) {
1273
+ return bound;
1274
+ }
1275
+ return getDefaultDatabasePool2();
1276
+ }
1277
+ function defaultMapSessionUser(user) {
1278
+ return {
1279
+ id: user.id,
1280
+ role: user.is_admin ? "admin" : "member"
1281
+ };
1282
+ }
1095
1283
 
1096
1284
  class CookieSessionStore {
1097
- sql;
1285
+ sqlSource;
1098
1286
  secret;
1099
1287
  cookieName;
1100
1288
  maxAgeSeconds;
1101
- constructor(sql, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
1102
- this.sql = sql;
1289
+ constructor(sqlSource, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
1290
+ this.sqlSource = sqlSource;
1103
1291
  this.secret = secret;
1104
1292
  this.cookieName = cookieName;
1105
1293
  this.maxAgeSeconds = maxAgeSeconds;
@@ -1117,10 +1305,13 @@ class CookieSessionStore {
1117
1305
  }
1118
1306
  return header.includes("Secure") ? header : `${header}; Secure`;
1119
1307
  }
1308
+ sql() {
1309
+ return resolveSql(this.sqlSource);
1310
+ }
1120
1311
  async create(user) {
1121
1312
  const id = randomBytes(32).toString("hex");
1122
1313
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
1123
- await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
1314
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
1124
1315
  id,
1125
1316
  user.id,
1126
1317
  expires
@@ -1128,7 +1319,7 @@ class CookieSessionStore {
1128
1319
  return id;
1129
1320
  }
1130
1321
  async destroy(sessionId) {
1131
- await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
1322
+ await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
1132
1323
  }
1133
1324
  async read(request) {
1134
1325
  const cookie = readRequestCookie(request, this.cookieName);
@@ -1139,7 +1330,7 @@ class CookieSessionStore {
1139
1330
  if (!sessionId || !signature || signature !== this.sign(sessionId)) {
1140
1331
  return null;
1141
1332
  }
1142
- const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
1333
+ const rows = await this.sql().unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
1143
1334
  COALESCE(u.is_admin, false) AS is_admin
1144
1335
  FROM sessions s
1145
1336
  INNER JOIN users u ON u.id = s.user_id
@@ -1159,19 +1350,42 @@ class CookieSessionStore {
1159
1350
  return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
1160
1351
  }
1161
1352
  }
1353
+
1354
+ class CookieSessionGuard {
1355
+ store;
1356
+ mapUser;
1357
+ constructor(store, mapUser = defaultMapSessionUser) {
1358
+ this.store = store;
1359
+ this.mapUser = mapUser;
1360
+ }
1361
+ async resolve(request) {
1362
+ const user = await this.store.read(request);
1363
+ if (!user) {
1364
+ return null;
1365
+ }
1366
+ return this.mapUser(user);
1367
+ }
1368
+ }
1369
+ function createCookieSessionAuthManager(options = {}) {
1370
+ const store = options.store ?? new CookieSessionStore(options.sql ?? defaultSessionSql, options.secret ?? process.env.SESSION_SECRET?.trim() ?? "", options.cookieName, options.maxAgeSeconds);
1371
+ return new AuthManager2(new CookieSessionGuard(store, options.mapUser ?? defaultMapSessionUser));
1372
+ }
1162
1373
  // ../../src/bootstrap/web/slug.ts
1163
1374
  function slugify(value) {
1164
1375
  return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
1165
1376
  }
1166
1377
  export {
1167
1378
  APP_PORT_CONFIG_KEY,
1379
+ CORE_ABILITY_CHECKER_TOKEN,
1168
1380
  CORE_AUTH_TOKEN,
1381
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
1169
1382
  CORE_CACHE_TOKEN,
1170
1383
  CORE_CONFIG_TOKEN,
1171
1384
  CORE_EVENT_BUS_TOKEN,
1172
1385
  CORE_POLICY_GATE_TOKEN,
1173
1386
  CORE_QUEUE_TOKEN,
1174
1387
  CORE_TOKEN_SERVICE_TOKEN,
1388
+ CookieSessionGuard,
1175
1389
  CookieSessionStore,
1176
1390
  DATABASE_URL_CONFIG_KEY,
1177
1391
  DEFAULT_APP_PORT,
@@ -1185,12 +1399,16 @@ export {
1185
1399
  buildModuleRoutes,
1186
1400
  buildWebModuleRoutes,
1187
1401
  cacheTagsForModelWrite,
1402
+ checkDatabase,
1403
+ checkRedis,
1188
1404
  collectProviders,
1189
1405
  configureModulesDirectory,
1190
1406
  coreProviders,
1191
1407
  createAppContext,
1192
1408
  createAppDependencies,
1409
+ createCookieSessionAuthManager,
1193
1410
  createCsrfProtection,
1411
+ createHealthRoutes,
1194
1412
  createHttpKernel,
1195
1413
  createRouteKernel,
1196
1414
  createWebRoutes,
@@ -1201,6 +1419,7 @@ export {
1201
1419
  getRequiredDependency,
1202
1420
  mergeWebRoutes,
1203
1421
  parseFormBody,
1422
+ pingDatabase,
1204
1423
  prefixRouteMap,
1205
1424
  registerDefaultJobs,
1206
1425
  resolveApplicationAuth,