@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.
- package/CHANGELOG.md +8 -0
- package/README.md +5 -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/routing.d.ts +2 -2
- package/dist/bootstrap/web/session.d.ts +25 -3
- package/dist/entries/buildModuleRoutes.js +24 -8
- package/dist/entries/buildWebModuleRoutes.js +24 -8
- package/dist/entries/config.js +4 -0
- package/dist/entries/context.js +2 -63
- package/dist/entries/createRoutes.js +218 -169
- package/dist/entries/createSpaRoutes.js +1 -1
- package/dist/entries/createWebRoutes.js +24 -8
- package/dist/entries/dependencies.js +2 -63
- package/dist/entries/health.js +69 -152
- package/dist/entries/httpKernel.js +24 -8
- package/dist/entries/providers.js +2 -0
- package/dist/entries/secretsGuard.js +64 -10
- package/dist/entries/web/routing.js +28 -12
- package/dist/entries/web/session.js +59 -7
- package/dist/index.js +300 -81
- package/package.json +5 -4
|
@@ -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 = {
|
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";
|
|
@@ -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:
|
|
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:
|
|
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.
|
|
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
|
|
202
|
+
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
187
203
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
188
204
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
189
|
-
return this.
|
|
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.
|
|
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
|
|
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);
|
|
@@ -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 {
|
|
@@ -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:
|
|
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:
|
|
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.
|
|
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
|
|
211
|
+
const abilityChecker = resolveAbilityChecker(this.dependencies.container);
|
|
196
212
|
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
197
213
|
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
198
|
-
return this.
|
|
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.
|
|
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
|
|
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 =
|
|
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);
|