@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.
@@ -107,7 +107,9 @@ var authConfig = {
107
107
 
108
108
  // ../../src/bootstrap/config.ts
109
109
  import {
110
+ CORE_ABILITY_CHECKER_TOKEN,
110
111
  CORE_AUTH_TOKEN,
112
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
111
113
  CORE_CACHE_TOKEN,
112
114
  CORE_CONFIG_TOKEN,
113
115
  CORE_EVENT_BUS_TOKEN,
@@ -506,68 +508,6 @@ var coreProviders = [
506
508
  viewProvider
507
509
  ];
508
510
 
509
- // ../../src/bootstrap/secretsGuard.ts
510
- var DEFAULT_ADMIN_API_TOKEN = "workhub-admin-test-token";
511
- var DEFAULT_MEMBER_API_TOKEN = "workhub-member-test-token";
512
- var DEFAULT_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
513
- var DEFAULT_TOKENS = new Set([DEFAULT_ADMIN_API_TOKEN, DEFAULT_MEMBER_API_TOKEN]);
514
- var DEFAULT_SCIM_TOKENS = new Set([DEFAULT_SCIM_BEARER_TOKEN]);
515
- function isEnabled(value, defaultEnabled) {
516
- if (value === undefined) {
517
- return defaultEnabled;
518
- }
519
- return defaultEnabled ? value !== "false" : value === "true";
520
- }
521
- function assertProductionSecrets(env = process.env) {
522
- const appEnv = env.APP_ENV ?? "local";
523
- if (appEnv !== "production") {
524
- return;
525
- }
526
- const adminToken = env.ADMIN_API_TOKEN ?? DEFAULT_ADMIN_API_TOKEN;
527
- const memberToken = env.MEMBER_API_TOKEN ?? DEFAULT_MEMBER_API_TOKEN;
528
- const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
529
- const encryptionEnabled = isEnabled(env.FEATURE_FIELD_ENCRYPTION, true);
530
- const devHeadersEnabled = isEnabled(env.AUTH_DEV_HEADERS, true);
531
- if (devHeadersEnabled) {
532
- throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
533
- }
534
- if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
535
- throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
536
- }
537
- if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
538
- throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
539
- }
540
- if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
541
- throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
542
- }
543
- if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, true)) {
544
- console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
545
- }
546
- if (isEnabled(env.FEATURE_BILLING, true) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
547
- throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
548
- }
549
- const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
550
- if (corsOrigins.includes("*")) {
551
- throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
552
- }
553
- if (isEnabled(env.FEATURE_PUBLIC_READS, true)) {
554
- throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
555
- }
556
- if (!env.OAUTH_STATE_SECRET?.trim()) {
557
- throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
558
- }
559
- if (!env.TOKEN_HASH_PEPPER?.trim()) {
560
- throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
561
- }
562
- if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
563
- throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
564
- }
565
- const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
566
- if (frontendMode === "server-htmx" && !env.SESSION_SECRET?.trim()) {
567
- throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx.");
568
- }
569
- }
570
-
571
511
  // ../../src/bootstrap/context.ts
572
512
  function collectProviders(modules = discoverModules()) {
573
513
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
@@ -578,7 +518,6 @@ function runProviderPhase(providers, phase, context) {
578
518
  }
579
519
  }
580
520
  function createAppContext() {
581
- assertProductionSecrets();
582
521
  const container = new ServiceContainer;
583
522
  const config = new ConfigStore;
584
523
  const dependencies = {
@@ -2,148 +2,16 @@
2
2
  var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/health.ts
5
+ import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
6
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
5
7
  import { jsonResponse } from "@getstrata/core/http/response";
6
8
  var {RedisClient } = globalThis.Bun;
7
9
 
8
- // ../../src/config/database.ts
9
- function readInteger(name, fallback) {
10
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
11
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
12
- }
13
- var databaseConfig = {
14
- url: process.env.DATABASE_URL ?? "",
15
- poolMax: readInteger("DB_POOL_MAX", 10),
16
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
17
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
18
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
19
- };
20
-
21
- // ../../src/core/runtime/asyncContextStore.ts
22
- import { AsyncLocalStorage } from "async_hooks";
23
- function createAsyncContextStore(key) {
24
- const symbol = Symbol.for(key);
25
- const globalRecord = globalThis;
26
- const existing = globalRecord[symbol];
27
- if (existing) {
28
- return existing;
29
- }
30
- const store = new AsyncLocalStorage;
31
- globalRecord[symbol] = store;
32
- return store;
33
- }
34
-
35
- // ../../src/core/database/connectionContext.ts
36
- var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
37
- function getActiveDatabaseConnection(fallback) {
38
- return activeConnection.getStore() ?? fallback;
39
- }
40
-
41
- // ../../src/core/database/queryProxy.ts
42
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
43
- function createDatabaseQueryProxy(pool) {
44
- function resolveDatabase() {
45
- return getActiveDatabaseConnection(pool);
46
- }
47
- function resolveDatabaseForProperty(property) {
48
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
49
- return pool;
50
- }
51
- return resolveDatabase();
52
- }
53
- return new Proxy(function database() {}, {
54
- apply(_target, _thisArg, args) {
55
- return resolveDatabase()(...args);
56
- },
57
- get(_target, property) {
58
- const connection = resolveDatabaseForProperty(property);
59
- const value = connection[property];
60
- return typeof value === "function" ? value.bind(connection) : value;
61
- }
62
- });
63
- }
64
-
65
- // ../../src/core/database/defaultConnection.ts
66
- var defaultPool = {
67
- connection: null
68
- };
69
- var defaultQuery = {
70
- connection: null
71
- };
72
- function registerDefaultDatabasePool(connection) {
73
- defaultPool.connection = connection;
74
- defaultQuery.connection = createDatabaseQueryProxy(connection);
75
- }
76
- function getDefaultDatabaseQuery() {
77
- if (!defaultQuery.connection) {
78
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
79
- }
80
- return defaultQuery.connection;
81
- }
82
-
83
- // ../../src/db/connection/createConnection.ts
84
- var {SQL } = globalThis.Bun;
85
- function createDatabaseConnection(config) {
86
- if (!config.url) {
87
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
88
- }
89
- return new SQL({
90
- url: config.url,
91
- max: config.poolMax,
92
- idleTimeout: config.idleTimeoutSeconds,
93
- maxLifetime: config.maxLifetimeSeconds,
94
- connectionTimeout: config.connectionTimeoutSeconds
95
- });
96
- }
97
-
98
- // ../../src/db/connection/index.ts
99
- var connectionHolder = {
100
- connection: null
101
- };
102
- function getDatabase() {
103
- if (!connectionHolder.connection) {
104
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
105
- registerDefaultDatabasePool(connectionHolder.connection);
106
- }
107
- return connectionHolder.connection;
108
- }
109
- function getDb() {
110
- getDatabase();
111
- return getDefaultDatabaseQuery();
112
- }
113
- async function pingDatabase(connection = getDatabase()) {
114
- try {
115
- await connection`SELECT 1`;
116
- return true;
117
- } catch {
118
- return false;
119
- }
120
- }
121
- async function ensureDatabaseConnection() {
122
- if (await pingDatabase()) {
123
- return getDatabase();
124
- }
125
- await getDatabase().close().catch(() => {
126
- return;
127
- });
128
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
129
- registerDefaultDatabasePool(connectionHolder.connection);
130
- return getDatabase();
131
- }
132
- var db = new Proxy(function database() {}, {
133
- apply(_target, _thisArg, args) {
134
- return getDb()(...args);
135
- },
136
- get(_target, property) {
137
- const connection = getDb();
138
- const value = connection[property];
139
- return typeof value === "function" ? value.bind(connection) : value;
140
- }
141
- });
142
- var connection_default = db;
143
-
144
10
  // ../../src/bootstrap/config.ts
145
11
  import {
12
+ CORE_ABILITY_CHECKER_TOKEN,
146
13
  CORE_AUTH_TOKEN,
14
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
147
15
  CORE_CACHE_TOKEN,
148
16
  CORE_CONFIG_TOKEN,
149
17
  CORE_EVENT_BUS_TOKEN,
@@ -174,9 +42,34 @@ function resolveRedisUrl(dependencies) {
174
42
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
175
43
  return redisUrl || undefined;
176
44
  }
45
+ function resolveHealthDatabase() {
46
+ const bound = getBoundDatabaseConnection();
47
+ if (bound) {
48
+ return bound;
49
+ }
50
+ try {
51
+ return getDefaultDatabasePool();
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ async function pingDatabaseClient(connection) {
57
+ try {
58
+ await connection.unsafe("SELECT 1");
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
177
64
  async function checkDatabase() {
178
- await ensureDatabaseConnection();
179
- return await pingDatabase();
65
+ const connection = resolveHealthDatabase();
66
+ if (!connection) {
67
+ return false;
68
+ }
69
+ return await pingDatabaseClient(connection);
70
+ }
71
+ async function pingDatabase() {
72
+ return await checkDatabase();
180
73
  }
181
74
  async function checkRedis(redisUrl) {
182
75
  try {
@@ -187,23 +80,46 @@ async function checkRedis(redisUrl) {
187
80
  return false;
188
81
  }
189
82
  }
190
- function createHealthRoutes(dependencies) {
83
+ async function resolveExtraFields(extra) {
84
+ if (!extra) {
85
+ return {};
86
+ }
87
+ return typeof extra === "function" ? await extra() : extra;
88
+ }
89
+ async function collectDependencyChecks(dependencies) {
90
+ const checks = {
91
+ database: await checkDatabase() ? "ok" : "error"
92
+ };
93
+ const redisUrl = resolveRedisUrl(dependencies);
94
+ if (redisUrl) {
95
+ checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
96
+ } else {
97
+ checks.redis = "skipped";
98
+ }
99
+ const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
100
+ return { checks, ready };
101
+ }
102
+ function createHealthRoutes(dependencies, options = {}) {
191
103
  return {
192
- "/health": async () => jsonResponse({ status: "ok" }),
193
- "/ready": async () => {
194
- const checks = {
195
- database: await checkDatabase() ? "ok" : "error"
196
- };
197
- const redisUrl = resolveRedisUrl(dependencies);
198
- if (redisUrl) {
199
- checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
200
- } else {
201
- checks.redis = "skipped";
104
+ "/health": async () => {
105
+ const extra = await resolveExtraFields(options.extra);
106
+ if (!options.pingOnHealth) {
107
+ return jsonResponse({ status: "ok", ...extra });
202
108
  }
203
- const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
109
+ const { checks, ready } = await collectDependencyChecks(dependencies);
110
+ return jsonResponse({
111
+ status: ready ? "ok" : "error",
112
+ checks,
113
+ ...extra
114
+ }, { status: ready ? 200 : 503 });
115
+ },
116
+ "/ready": async () => {
117
+ const extra = await resolveExtraFields(options.extra);
118
+ const { checks, ready } = await collectDependencyChecks(dependencies);
204
119
  return jsonResponse({
205
120
  status: ready ? "ready" : "not_ready",
206
- checks
121
+ checks,
122
+ ...extra
207
123
  }, { status: ready ? 200 : 503 });
208
124
  }
209
125
  };
@@ -211,5 +127,6 @@ function createHealthRoutes(dependencies) {
211
127
  export {
212
128
  checkDatabase,
213
129
  checkRedis,
214
- createHealthRoutes
130
+ createHealthRoutes,
131
+ pingDatabase
215
132
  };
@@ -3,6 +3,7 @@ var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/httpKernel.ts
5
5
  import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
6
+ import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
6
7
  import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
7
8
  import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
8
9
  import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
@@ -17,6 +18,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
17
18
  import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
18
19
  import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
19
20
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
21
+ import { withErrorHandling } from "@getstrata/core/http/response";
20
22
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
21
23
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
22
24
  import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
@@ -62,24 +64,38 @@ function parsePositiveInt(value, fallback) {
62
64
  }
63
65
  return Math.trunc(parsed);
64
66
  }
67
+ function parseWindowSeconds(secondsValue, msValue, fallback) {
68
+ if (secondsValue !== undefined && secondsValue.trim() !== "") {
69
+ return parsePositiveInt(secondsValue, fallback);
70
+ }
71
+ if (msValue !== undefined && msValue.trim() !== "") {
72
+ const parsedMs = Number(msValue);
73
+ if (Number.isFinite(parsedMs) && parsedMs > 0) {
74
+ return Math.max(1, Math.trunc(parsedMs / 1000));
75
+ }
76
+ }
77
+ return fallback;
78
+ }
65
79
  function resolveLoginRateLimit() {
66
80
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
67
81
  return {
68
82
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
69
- decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
83
+ decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
70
84
  };
71
85
  }
72
86
  function resolveRegisterRateLimit() {
73
87
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
74
88
  return {
75
89
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
76
- 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)
90
+ decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
77
91
  };
78
92
  }
79
93
 
80
94
  // ../../src/bootstrap/config.ts
81
95
  import {
96
+ CORE_ABILITY_CHECKER_TOKEN,
82
97
  CORE_AUTH_TOKEN,
98
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
83
99
  CORE_CACHE_TOKEN,
84
100
  CORE_CONFIG_TOKEN,
85
101
  CORE_EVENT_BUS_TOKEN,
@@ -169,7 +185,7 @@ class HttpKernel {
169
185
  return this.wrap(["api", "authenticated"], handler);
170
186
  }
171
187
  wrapWeb(handler) {
172
- return this.wrap("web", handler);
188
+ return withErrorHandling(this.wrap("web", handler));
173
189
  }
174
190
  wrapWebPublicRead(handler) {
175
191
  if (isPublicReadsEnabled()) {
@@ -179,19 +195,19 @@ class HttpKernel {
179
195
  }
180
196
  wrapWebAuthenticated(handler) {
181
197
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
182
- return this.wrap("web", withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
198
+ return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
183
199
  }
184
200
  wrapWebAbility(ability, handler) {
185
201
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
186
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
202
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
187
203
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
188
204
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
189
- return this.wrap("web", withMiddleware(...middleware)(handler));
205
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
190
206
  }
191
207
  wrapWebGlobalAdmin(handler) {
192
208
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
193
209
  const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
194
- return this.wrap("web", withMiddleware(...middleware)(handler));
210
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
195
211
  }
196
212
  wrapAuthenticated(handler) {
197
213
  return this.wrap("authenticated", handler);
@@ -207,7 +223,7 @@ class HttpKernel {
207
223
  return withMiddleware(...middleware)(handler);
208
224
  }
209
225
  wrapAbility(ability, handler) {
210
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
226
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
211
227
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
212
228
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
213
229
  return withMiddleware(...middleware)(handler);
@@ -18,7 +18,9 @@ var authConfig = {
18
18
 
19
19
  // ../../src/bootstrap/config.ts
20
20
  import {
21
+ CORE_ABILITY_CHECKER_TOKEN,
21
22
  CORE_AUTH_TOKEN,
23
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
22
24
  CORE_CACHE_TOKEN,
23
25
  CORE_CONFIG_TOKEN,
24
26
  CORE_EVENT_BUS_TOKEN,
@@ -5,6 +5,7 @@ var __jsonParse = (a) => JSON.parse(a);
5
5
  var DEFAULT_ADMIN_API_TOKEN = "workhub-admin-test-token";
6
6
  var DEFAULT_MEMBER_API_TOKEN = "workhub-member-test-token";
7
7
  var DEFAULT_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
8
+ var MIN_SESSION_SECRET_LENGTH = 32;
8
9
  var DEFAULT_TOKENS = new Set([DEFAULT_ADMIN_API_TOKEN, DEFAULT_MEMBER_API_TOKEN]);
9
10
  var DEFAULT_SCIM_TOKENS = new Set([DEFAULT_SCIM_BEARER_TOKEN]);
10
11
  function isEnabled(value, defaultEnabled) {
@@ -13,19 +14,31 @@ function isEnabled(value, defaultEnabled) {
13
14
  }
14
15
  return defaultEnabled ? value !== "false" : value === "true";
15
16
  }
16
- function assertProductionSecrets(env = process.env) {
17
- const appEnv = env.APP_ENV ?? "local";
18
- if (appEnv !== "production") {
19
- return;
17
+ function isTokenAuthEnabled(env) {
18
+ return Boolean(env.ADMIN_API_TOKEN?.trim()) || Boolean(env.MEMBER_API_TOKEN?.trim()) || env.FEATURE_API_TOKENS === "true";
19
+ }
20
+ function isOAuthEnabled(env) {
21
+ 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());
22
+ }
23
+ function isCorsConfigured(env) {
24
+ return env.CORS_ALLOWED_ORIGINS !== undefined;
25
+ }
26
+ function assertAuthDevHeadersDisabled(env) {
27
+ if (isEnabled(env.AUTH_DEV_HEADERS, true)) {
28
+ throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
20
29
  }
30
+ }
31
+ function assertSessionSecret(env) {
32
+ const secret = env.SESSION_SECRET?.trim() ?? "";
33
+ if (secret.length < MIN_SESSION_SECRET_LENGTH) {
34
+ throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx (32+ characters).");
35
+ }
36
+ }
37
+ function assertWorkHubProductionSecrets(env) {
21
38
  const adminToken = env.ADMIN_API_TOKEN ?? DEFAULT_ADMIN_API_TOKEN;
22
39
  const memberToken = env.MEMBER_API_TOKEN ?? DEFAULT_MEMBER_API_TOKEN;
23
40
  const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
24
41
  const encryptionEnabled = isEnabled(env.FEATURE_FIELD_ENCRYPTION, true);
25
- const devHeadersEnabled = isEnabled(env.AUTH_DEV_HEADERS, true);
26
- if (devHeadersEnabled) {
27
- throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
28
- }
29
42
  if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
30
43
  throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
31
44
  }
@@ -57,9 +70,50 @@ function assertProductionSecrets(env = process.env) {
57
70
  if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
58
71
  throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
59
72
  }
73
+ }
74
+ function assertSiblingProductionSecrets(env) {
75
+ if (isEnabled(env.FEATURE_SCIM, false)) {
76
+ const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
77
+ if (DEFAULT_SCIM_TOKENS.has(scimToken) || !env.SCIM_BEARER_TOKEN?.trim()) {
78
+ throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
79
+ }
80
+ }
81
+ if (isEnabled(env.FEATURE_FIELD_ENCRYPTION, false) && !env.KMS_ENCRYPTION_KEY?.trim()) {
82
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
83
+ }
84
+ if (isEnabled(env.FEATURE_BILLING, false) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
85
+ throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
86
+ }
87
+ if (isOAuthEnabled(env) && !env.OAUTH_STATE_SECRET?.trim()) {
88
+ throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
89
+ }
90
+ if (isCorsConfigured(env)) {
91
+ const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
92
+ if (corsOrigins.includes("*")) {
93
+ throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
94
+ }
95
+ }
96
+ if (isEnabled(env.FEATURE_PUBLIC_READS, false)) {
97
+ throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
98
+ }
99
+ if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, false)) {
100
+ console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
101
+ }
102
+ }
103
+ function assertProductionSecrets(env = process.env) {
104
+ const appEnv = env.APP_ENV ?? "local";
105
+ if (appEnv !== "production") {
106
+ return;
107
+ }
108
+ assertAuthDevHeadersDisabled(env);
109
+ if (isTokenAuthEnabled(env)) {
110
+ assertWorkHubProductionSecrets(env);
111
+ } else {
112
+ assertSiblingProductionSecrets(env);
113
+ }
60
114
  const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
61
- if (frontendMode === "server-htmx" && !env.SESSION_SECRET?.trim()) {
62
- throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx.");
115
+ if (frontendMode === "server-htmx") {
116
+ assertSessionSecret(env);
63
117
  }
64
118
  }
65
119
  export {
@@ -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 {
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  // ../../src/bootstrap/httpKernel.ts
14
14
  import { createMembershipMiddleware } from "@getstrata/core/auth/membershipMiddleware";
15
+ import { resolveAbilityChecker } from "@getstrata/core/contracts/serviceTokens";
15
16
  import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
16
17
  import { createAuthorizeMiddleware } from "@getstrata/core/http/authorizeMiddleware";
17
18
  import { createBodySizeLimitMiddleware } from "@getstrata/core/http/bodySizeLimitMiddleware";
@@ -26,6 +27,7 @@ import { createRequireAbilityMiddleware } from "@getstrata/core/http/requireAbil
26
27
  import { createRequireAuthMiddleware } from "@getstrata/core/http/requireAuthMiddleware";
27
28
  import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/requireGlobalAdminMiddleware";
28
29
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
30
+ import { withErrorHandling } from "@getstrata/core/http/response";
29
31
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
30
32
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
31
33
  import { createThrottleMiddleware } from "@getstrata/core/http/throttleMiddleware";
@@ -71,24 +73,38 @@ function parsePositiveInt(value, fallback) {
71
73
  }
72
74
  return Math.trunc(parsed);
73
75
  }
76
+ function parseWindowSeconds(secondsValue, msValue, fallback) {
77
+ if (secondsValue !== undefined && secondsValue.trim() !== "") {
78
+ return parsePositiveInt(secondsValue, fallback);
79
+ }
80
+ if (msValue !== undefined && msValue.trim() !== "") {
81
+ const parsedMs = Number(msValue);
82
+ if (Number.isFinite(parsedMs) && parsedMs > 0) {
83
+ return Math.max(1, Math.trunc(parsedMs / 1000));
84
+ }
85
+ }
86
+ return fallback;
87
+ }
74
88
  function resolveLoginRateLimit() {
75
89
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
76
90
  return {
77
91
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
78
- decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
92
+ decaySeconds: parseWindowSeconds(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, process.env.LOGIN_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
79
93
  };
80
94
  }
81
95
  function resolveRegisterRateLimit() {
82
96
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
83
97
  return {
84
98
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
85
- 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)
99
+ decaySeconds: parseWindowSeconds(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS, process.env.REGISTER_RATE_LIMIT_WINDOW_MS, defaults.decaySeconds)
86
100
  };
87
101
  }
88
102
 
89
103
  // ../../src/bootstrap/config.ts
90
104
  import {
105
+ CORE_ABILITY_CHECKER_TOKEN,
91
106
  CORE_AUTH_TOKEN,
107
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
92
108
  CORE_CACHE_TOKEN,
93
109
  CORE_CONFIG_TOKEN,
94
110
  CORE_EVENT_BUS_TOKEN,
@@ -178,7 +194,7 @@ class HttpKernel {
178
194
  return this.wrap(["api", "authenticated"], handler);
179
195
  }
180
196
  wrapWeb(handler) {
181
- return this.wrap("web", handler);
197
+ return withErrorHandling(this.wrap("web", handler));
182
198
  }
183
199
  wrapWebPublicRead(handler) {
184
200
  if (isPublicReadsEnabled()) {
@@ -188,19 +204,19 @@ class HttpKernel {
188
204
  }
189
205
  wrapWebAuthenticated(handler) {
190
206
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
191
- return this.wrap("web", withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
207
+ return this.wrapWeb(withMiddleware(createRequireWebAuthMiddleware(auth))(handler));
192
208
  }
193
209
  wrapWebAbility(ability, handler) {
194
210
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
195
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
211
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
196
212
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
197
213
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
198
- return this.wrap("web", withMiddleware(...middleware)(handler));
214
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
199
215
  }
200
216
  wrapWebGlobalAdmin(handler) {
201
217
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
202
218
  const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
203
- return this.wrap("web", withMiddleware(...middleware)(handler));
219
+ return this.wrapWeb(withMiddleware(...middleware)(handler));
204
220
  }
205
221
  wrapAuthenticated(handler) {
206
222
  return this.wrap("authenticated", handler);
@@ -216,7 +232,7 @@ class HttpKernel {
216
232
  return withMiddleware(...middleware)(handler);
217
233
  }
218
234
  wrapAbility(ability, handler) {
219
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
235
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
220
236
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
221
237
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
222
238
  return withMiddleware(...middleware)(handler);
@@ -296,14 +312,14 @@ function toRouteRequest(request) {
296
312
  return request;
297
313
  }
298
314
  function wrapSecuredRouteModelByKey(param, resolver, authorization, handler) {
299
- const bound = withErrorHandling(securedBindRouteModelByKey(param, resolver, authorization, handler));
315
+ const bound = withErrorHandling2(securedBindRouteModelByKey(param, resolver, authorization, handler));
300
316
  return async (request) => bound(toRouteRequest(request));
301
317
  }
302
318
  function wrapWebLogin(kernel, handler, onThrottled) {
303
- return wrapWebThrottle(kernel, "login", handler, onThrottled);
319
+ return kernel.wrapWeb(wrapWebThrottle(kernel, "login", handler, onThrottled));
304
320
  }
305
321
  function wrapWebRegister(kernel, handler, onThrottled) {
306
- return wrapWebThrottle(kernel, "register", handler, onThrottled);
322
+ return kernel.wrapWeb(wrapWebThrottle(kernel, "register", handler, onThrottled));
307
323
  }
308
324
  function wrapWebThrottle(kernel, scope, handler, onThrottled) {
309
325
  const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);