@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.
- package/CHANGELOG.md +8 -0
- package/README.md +3 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/health.d.ts +8 -2
- package/dist/bootstrap/public-api.d.ts +4 -2
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/session.d.ts +25 -3
- package/dist/entries/buildModuleRoutes.js +19 -4
- package/dist/entries/buildWebModuleRoutes.js +19 -4
- package/dist/entries/config.js +4 -0
- package/dist/entries/context.js +2 -0
- package/dist/entries/createRoutes.js +203 -155
- package/dist/entries/createSpaRoutes.js +1 -1
- package/dist/entries/createWebRoutes.js +19 -4
- package/dist/entries/dependencies.js +2 -0
- package/dist/entries/health.js +69 -152
- package/dist/entries/httpKernel.js +19 -4
- package/dist/entries/providers.js +2 -0
- package/dist/entries/secretsGuard.js +64 -10
- package/dist/entries/web/routing.js +19 -4
- package/dist/entries/web/session.js +59 -7
- package/dist/index.js +240 -20
- package/package.json +5 -4
package/dist/entries/health.js
CHANGED
|
@@ -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
|
-
|
|
179
|
-
|
|
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
|
|
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 () =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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);
|
|
@@ -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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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"
|
|
62
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
34
|
+
sqlSource;
|
|
10
35
|
secret;
|
|
11
36
|
cookieName;
|
|
12
37
|
maxAgeSeconds;
|
|
13
|
-
constructor(
|
|
14
|
-
this.
|
|
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
|
-
|
|
123
|
+
CookieSessionGuard,
|
|
124
|
+
CookieSessionStore,
|
|
125
|
+
createCookieSessionAuthManager,
|
|
126
|
+
defaultMapSessionUser,
|
|
127
|
+
defaultSessionSql
|
|
76
128
|
};
|