@getstrata/bootstrap 0.2.49 → 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.
@@ -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";
@@ -63,24 +64,38 @@ function parsePositiveInt(value, fallback) {
63
64
  }
64
65
  return Math.trunc(parsed);
65
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
+ }
66
79
  function resolveLoginRateLimit() {
67
80
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
68
81
  return {
69
82
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
70
- 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)
71
84
  };
72
85
  }
73
86
  function resolveRegisterRateLimit() {
74
87
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
75
88
  return {
76
89
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
77
- 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)
78
91
  };
79
92
  }
80
93
 
81
94
  // ../../src/bootstrap/config.ts
82
95
  import {
96
+ CORE_ABILITY_CHECKER_TOKEN,
83
97
  CORE_AUTH_TOKEN,
98
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
84
99
  CORE_CACHE_TOKEN,
85
100
  CORE_CONFIG_TOKEN,
86
101
  CORE_EVENT_BUS_TOKEN,
@@ -184,7 +199,7 @@ class HttpKernel {
184
199
  }
185
200
  wrapWebAbility(ability, handler) {
186
201
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
187
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
202
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
188
203
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
189
204
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
190
205
  return this.wrapWeb(withMiddleware(...middleware)(handler));
@@ -208,7 +223,7 @@ class HttpKernel {
208
223
  return withMiddleware(...middleware)(handler);
209
224
  }
210
225
  wrapAbility(ability, handler) {
211
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
226
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
212
227
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
213
228
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
214
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 {
@@ -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";
@@ -72,24 +73,38 @@ function parsePositiveInt(value, fallback) {
72
73
  }
73
74
  return Math.trunc(parsed);
74
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
+ }
75
88
  function resolveLoginRateLimit() {
76
89
  const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
77
90
  return {
78
91
  maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
79
- 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)
80
93
  };
81
94
  }
82
95
  function resolveRegisterRateLimit() {
83
96
  const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
84
97
  return {
85
98
  maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
86
- 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)
87
100
  };
88
101
  }
89
102
 
90
103
  // ../../src/bootstrap/config.ts
91
104
  import {
105
+ CORE_ABILITY_CHECKER_TOKEN,
92
106
  CORE_AUTH_TOKEN,
107
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
93
108
  CORE_CACHE_TOKEN,
94
109
  CORE_CONFIG_TOKEN,
95
110
  CORE_EVENT_BUS_TOKEN,
@@ -193,7 +208,7 @@ class HttpKernel {
193
208
  }
194
209
  wrapWebAbility(ability, handler) {
195
210
  const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
196
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
211
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
197
212
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
198
213
  const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
199
214
  return this.wrapWeb(withMiddleware(...middleware)(handler));
@@ -217,7 +232,7 @@ class HttpKernel {
217
232
  return withMiddleware(...middleware)(handler);
218
233
  }
219
234
  wrapAbility(ability, handler) {
220
- const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
235
+ const abilityChecker = resolveAbilityChecker(this.dependencies.container);
221
236
  const requireAbility = createRequireAbilityMiddleware(abilityChecker);
222
237
  const middleware = [...this.group("authenticated"), requireAbility(ability)];
223
238
  return withMiddleware(...middleware)(handler);
@@ -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
  };