@getstrata/core 0.5.78 → 0.5.80
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/dist/core/auth/sessionCookie.d.ts +8 -2
- package/dist/core/contracts/authUserDirectory.d.ts +1 -0
- package/dist/core/runtime/appKeyPrefix.d.ts +3 -1
- package/dist/entries/audit/exportAuditLogs.js +6 -0
- package/dist/entries/audit/siemFormatter.js +6 -0
- package/dist/entries/auth/oauth/providers.js +6 -0
- package/dist/entries/auth/oauth/samlProvider.js +6 -0
- package/dist/entries/auth/passwordConfirmCookie.js +58 -2
- package/dist/entries/auth/sessionCookie.js +75 -4
- package/dist/entries/auth/sessionGuard.js +79 -7
- package/dist/entries/auth/tokenHash.js +58 -2
- package/dist/entries/cache/createCacheStore.js +6 -0
- package/dist/entries/facades.js +6 -0
- package/dist/entries/http/csrfMiddleware.js +56 -2
- package/dist/entries/http/csrfToken.js +56 -2
- package/dist/entries/http/flashMiddleware.js +58 -2
- package/dist/entries/http/flashSession.js +58 -2
- package/dist/entries/http/loginThrottleMiddleware.js +6 -0
- package/dist/entries/http/memoryThrottleMiddleware.js +6 -0
- package/dist/entries/http/scimThrottleMiddleware.js +6 -0
- package/dist/entries/http/securityHeadersMiddleware.js +6 -0
- package/dist/entries/http/signedUrl.js +57 -1
- package/dist/entries/http/throttleMiddleware.js +6 -0
- package/dist/entries/jobs/exportAuditLogsJob.js +6 -0
- package/dist/entries/mail/mailer.js +6 -0
- package/dist/entries/openapi/generator.js +6 -0
- package/dist/entries/queue/createAppQueue.js +6 -0
- package/dist/entries/queue/publicQueue.js +6 -0
- package/dist/entries/queue/queueMetrics.js +6 -0
- package/dist/entries/queue/redisQueue.js +6 -0
- package/dist/entries/runtime/appKeyPrefix.js +8 -0
- package/dist/entries/security/oauthState.js +57 -1
- package/dist/entries/security/safeFetch.js +6 -0
- package/dist/entries/tracing/tracingMiddleware.js +6 -0
- package/dist/entries/view.js +58 -4
- package/dist/index.js +11 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.80
|
|
4
|
+
|
|
5
|
+
- `appCookieName()` / `appDevSecret()` on `@getstrata/core/runtime/appKeyPrefix`. HMAC session, CSRF, flash, password-confirm, signed-URL, OAuth-state, and token-pepper fallbacks follow `APP_KEY_PREFIX` (WorkHub defaults unchanged).
|
|
6
|
+
|
|
7
|
+
## 0.5.79
|
|
8
|
+
|
|
9
|
+
- `readSession()` returns `{ userId, issuedAt }` from the HMAC session cookie. `isSessionInvalidated(issuedAt, session_valid_after)` lets `SessionGuard` reject cookies issued before `AuthUserRecord.session_valid_after` (Jetstream logout-other-devices / password change).
|
|
10
|
+
|
|
3
11
|
## 0.5.78
|
|
4
12
|
|
|
5
13
|
- OpenAPI treats `POST /auth/two-factor-challenge` as a public operation (no bearer), including when routes are registered under `API_PREFIX`.
|
|
@@ -4,11 +4,17 @@ declare const SESSION_REMEMBER_TTL_SECONDS: number;
|
|
|
4
4
|
interface CreateSessionCookieOptions {
|
|
5
5
|
remember?: boolean;
|
|
6
6
|
}
|
|
7
|
+
interface SignedSession {
|
|
8
|
+
userId: number;
|
|
9
|
+
issuedAt: number;
|
|
10
|
+
}
|
|
7
11
|
declare function sessionCookieName(): string;
|
|
8
12
|
declare function sessionTtlSeconds(): number;
|
|
9
13
|
declare function sessionRememberTtlSeconds(): number;
|
|
14
|
+
declare function readSession(request: Request): SignedSession | null;
|
|
10
15
|
declare function readSessionUserId(request: Request): number | null;
|
|
16
|
+
declare function isSessionInvalidated(issuedAt: number, validAfter: Date | string | null | undefined): boolean;
|
|
11
17
|
declare function createSessionCookie(userId: number, options?: CreateSessionCookieOptions): string;
|
|
12
18
|
declare function clearSessionCookie(): string;
|
|
13
|
-
export type { CreateSessionCookieOptions };
|
|
14
|
-
export { clearSessionCookie, createSessionCookie, readSessionUserId, SESSION_COOKIE, SESSION_REMEMBER_TTL_SECONDS, SESSION_TTL_SECONDS, sessionCookieName, sessionRememberTtlSeconds, sessionTtlSeconds, };
|
|
19
|
+
export type { CreateSessionCookieOptions, SignedSession };
|
|
20
|
+
export { clearSessionCookie, createSessionCookie, isSessionInvalidated, readSession, readSessionUserId, SESSION_COOKIE, SESSION_REMEMBER_TTL_SECONDS, SESSION_TTL_SECONDS, sessionCookieName, sessionRememberTtlSeconds, sessionTtlSeconds, };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
declare function appKeyPrefix(): string;
|
|
2
|
+
declare function appCookieName(kind: string): string;
|
|
3
|
+
declare function appDevSecret(kind: string): string;
|
|
2
4
|
declare function namespacedRedisKey(kind: string): string;
|
|
3
5
|
declare function smtpEhloHost(): string;
|
|
4
6
|
declare function siemEventType(): string;
|
|
@@ -10,4 +12,4 @@ declare function appEnv(): string;
|
|
|
10
12
|
declare function appUrl(): string;
|
|
11
13
|
declare function apiPrefix(): string;
|
|
12
14
|
declare function sdkClientClassName(): string;
|
|
13
|
-
export { apiPrefix, appDisplayName, appEnv, appKeyPrefix, appUrl, appUserAgent, namespacedRedisKey, otelServiceName, sdkClientClassName, siemEventType, smtpEhloHost, webhookSignatureHeader, };
|
|
15
|
+
export { apiPrefix, appCookieName, appDevSecret, appDisplayName, appEnv, appKeyPrefix, appUrl, appUserAgent, namespacedRedisKey, otelServiceName, sdkClientClassName, siemEventType, smtpEhloHost, webhookSignatureHeader, };
|
|
@@ -6,6 +6,12 @@ import { repositoryConnection as db2 } from "@getstrata/core/database/repository
|
|
|
6
6
|
function appKeyPrefix() {
|
|
7
7
|
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
8
|
}
|
|
9
|
+
function appCookieName(kind) {
|
|
10
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
11
|
+
}
|
|
12
|
+
function appDevSecret(kind) {
|
|
13
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
|
+
}
|
|
9
15
|
function namespacedRedisKey(kind) {
|
|
10
16
|
return `${appKeyPrefix()}:${kind}`;
|
|
11
17
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
function appKeyPrefix() {
|
|
4
4
|
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
5
5
|
}
|
|
6
|
+
function appCookieName(kind) {
|
|
7
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
8
|
+
}
|
|
9
|
+
function appDevSecret(kind) {
|
|
10
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
11
|
+
}
|
|
6
12
|
function namespacedRedisKey(kind) {
|
|
7
13
|
return `${appKeyPrefix()}:${kind}`;
|
|
8
14
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
function appKeyPrefix() {
|
|
4
4
|
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
5
5
|
}
|
|
6
|
+
function appCookieName(kind) {
|
|
7
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
8
|
+
}
|
|
9
|
+
function appDevSecret(kind) {
|
|
10
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
11
|
+
}
|
|
6
12
|
function namespacedRedisKey(kind) {
|
|
7
13
|
return `${appKeyPrefix()}:${kind}`;
|
|
8
14
|
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
function appKeyPrefix() {
|
|
4
4
|
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
5
5
|
}
|
|
6
|
+
function appCookieName(kind) {
|
|
7
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
8
|
+
}
|
|
9
|
+
function appDevSecret(kind) {
|
|
10
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
11
|
+
}
|
|
6
12
|
function namespacedRedisKey(kind) {
|
|
7
13
|
return `${appKeyPrefix()}:${kind}`;
|
|
8
14
|
}
|
|
@@ -1,17 +1,73 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/auth/passwordConfirmCookie.ts
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function appCookieName(kind) {
|
|
10
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
11
|
+
}
|
|
12
|
+
function appDevSecret(kind) {
|
|
13
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
|
+
}
|
|
15
|
+
function namespacedRedisKey(kind) {
|
|
16
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
17
|
+
}
|
|
18
|
+
function smtpEhloHost() {
|
|
19
|
+
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
20
|
+
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
21
|
+
return safe || "strata.local";
|
|
22
|
+
}
|
|
23
|
+
function siemEventType() {
|
|
24
|
+
return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
|
|
25
|
+
}
|
|
26
|
+
function appUserAgent() {
|
|
27
|
+
return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
|
|
28
|
+
}
|
|
29
|
+
function otelServiceName() {
|
|
30
|
+
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
31
|
+
}
|
|
32
|
+
function webhookSignatureHeader() {
|
|
33
|
+
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
34
|
+
}
|
|
35
|
+
function appDisplayName() {
|
|
36
|
+
return process.env.APP_NAME?.trim() || "WorkHub";
|
|
37
|
+
}
|
|
38
|
+
function appEnv() {
|
|
39
|
+
return process.env.APP_ENV?.trim() || "local";
|
|
40
|
+
}
|
|
41
|
+
function appUrl() {
|
|
42
|
+
return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
|
|
43
|
+
}
|
|
44
|
+
function apiPrefix() {
|
|
45
|
+
const raw = process.env.API_PREFIX?.trim() || "/api/v1";
|
|
46
|
+
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
47
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
48
|
+
return trimmed || "/api/v1";
|
|
49
|
+
}
|
|
50
|
+
function sdkClientClassName() {
|
|
51
|
+
const override = process.env.APP_SDK_CLASS?.trim();
|
|
52
|
+
if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
|
|
53
|
+
return override;
|
|
54
|
+
}
|
|
55
|
+
const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
|
|
56
|
+
return fromName ? `${fromName}Client` : "AppClient";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ../../src/core/auth/passwordConfirmCookie.ts
|
|
4
60
|
var PASSWORD_CONFIRM_COOKIE = "workhub_password_confirmed";
|
|
5
61
|
var DEFAULT_PASSWORD_CONFIRM_TTL_SECONDS = 3 * 60 * 60;
|
|
6
62
|
function passwordConfirmCookieName() {
|
|
7
|
-
return process.env.PASSWORD_CONFIRM_COOKIE_NAME?.trim() ||
|
|
63
|
+
return process.env.PASSWORD_CONFIRM_COOKIE_NAME?.trim() || appCookieName("password_confirmed");
|
|
8
64
|
}
|
|
9
65
|
function passwordConfirmTtlSeconds() {
|
|
10
66
|
const parsed = Number.parseInt(process.env.PASSWORD_CONFIRM_TIMEOUT ?? "", 10);
|
|
11
67
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_PASSWORD_CONFIRM_TTL_SECONDS;
|
|
12
68
|
}
|
|
13
69
|
function resolvePasswordConfirmSecret() {
|
|
14
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "
|
|
70
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || appDevSecret("session-secret");
|
|
15
71
|
}
|
|
16
72
|
function signPasswordConfirm(userId, confirmedAt) {
|
|
17
73
|
const payload = `${userId}.${confirmedAt}`;
|
|
@@ -1,11 +1,67 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/auth/sessionCookie.ts
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function appCookieName(kind) {
|
|
10
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
11
|
+
}
|
|
12
|
+
function appDevSecret(kind) {
|
|
13
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
|
+
}
|
|
15
|
+
function namespacedRedisKey(kind) {
|
|
16
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
17
|
+
}
|
|
18
|
+
function smtpEhloHost() {
|
|
19
|
+
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
20
|
+
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
21
|
+
return safe || "strata.local";
|
|
22
|
+
}
|
|
23
|
+
function siemEventType() {
|
|
24
|
+
return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
|
|
25
|
+
}
|
|
26
|
+
function appUserAgent() {
|
|
27
|
+
return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
|
|
28
|
+
}
|
|
29
|
+
function otelServiceName() {
|
|
30
|
+
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
31
|
+
}
|
|
32
|
+
function webhookSignatureHeader() {
|
|
33
|
+
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
34
|
+
}
|
|
35
|
+
function appDisplayName() {
|
|
36
|
+
return process.env.APP_NAME?.trim() || "WorkHub";
|
|
37
|
+
}
|
|
38
|
+
function appEnv() {
|
|
39
|
+
return process.env.APP_ENV?.trim() || "local";
|
|
40
|
+
}
|
|
41
|
+
function appUrl() {
|
|
42
|
+
return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
|
|
43
|
+
}
|
|
44
|
+
function apiPrefix() {
|
|
45
|
+
const raw = process.env.API_PREFIX?.trim() || "/api/v1";
|
|
46
|
+
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
47
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
48
|
+
return trimmed || "/api/v1";
|
|
49
|
+
}
|
|
50
|
+
function sdkClientClassName() {
|
|
51
|
+
const override = process.env.APP_SDK_CLASS?.trim();
|
|
52
|
+
if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
|
|
53
|
+
return override;
|
|
54
|
+
}
|
|
55
|
+
const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
|
|
56
|
+
return fromName ? `${fromName}Client` : "AppClient";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ../../src/core/auth/sessionCookie.ts
|
|
4
60
|
var SESSION_COOKIE = "workhub_session";
|
|
5
61
|
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
6
62
|
var SESSION_REMEMBER_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
7
63
|
function sessionCookieName() {
|
|
8
|
-
return process.env.SESSION_COOKIE_NAME?.trim() ||
|
|
64
|
+
return process.env.SESSION_COOKIE_NAME?.trim() || appCookieName("session");
|
|
9
65
|
}
|
|
10
66
|
function parsePositiveSeconds(raw, fallback) {
|
|
11
67
|
const parsed = Number.parseInt(raw ?? "", 10);
|
|
@@ -18,7 +74,7 @@ function sessionRememberTtlSeconds() {
|
|
|
18
74
|
return parsePositiveSeconds(process.env.SESSION_REMEMBER_TTL_SECONDS, SESSION_REMEMBER_TTL_SECONDS);
|
|
19
75
|
}
|
|
20
76
|
function resolveSessionSecret() {
|
|
21
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "
|
|
77
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || appDevSecret("session-secret");
|
|
22
78
|
}
|
|
23
79
|
function signSession(userId, issuedAt, ttlSeconds) {
|
|
24
80
|
const payload = ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
|
|
@@ -62,9 +118,9 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
|
|
|
62
118
|
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
63
119
|
return null;
|
|
64
120
|
}
|
|
65
|
-
return userId;
|
|
121
|
+
return { userId, issuedAt };
|
|
66
122
|
}
|
|
67
|
-
function
|
|
123
|
+
function readSession(request) {
|
|
68
124
|
const cookieValue = readCookieValue(request, sessionCookieName());
|
|
69
125
|
if (!cookieValue) {
|
|
70
126
|
return null;
|
|
@@ -80,6 +136,19 @@ function readSessionUserId(request) {
|
|
|
80
136
|
const [userIdRaw, issuedAtRaw, cookieSignature] = parts;
|
|
81
137
|
return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, sessionTtlSeconds(), false);
|
|
82
138
|
}
|
|
139
|
+
function readSessionUserId(request) {
|
|
140
|
+
return readSession(request)?.userId ?? null;
|
|
141
|
+
}
|
|
142
|
+
function isSessionInvalidated(issuedAt, validAfter) {
|
|
143
|
+
if (!validAfter) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const timestamp = validAfter instanceof Date ? validAfter.getTime() : Date.parse(String(validAfter));
|
|
147
|
+
if (!Number.isFinite(timestamp)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return issuedAt < timestamp;
|
|
151
|
+
}
|
|
83
152
|
function createSessionCookie(userId, options = {}) {
|
|
84
153
|
const issuedAt = Date.now();
|
|
85
154
|
const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
|
|
@@ -97,6 +166,8 @@ export {
|
|
|
97
166
|
SESSION_TTL_SECONDS,
|
|
98
167
|
clearSessionCookie,
|
|
99
168
|
createSessionCookie,
|
|
169
|
+
isSessionInvalidated,
|
|
170
|
+
readSession,
|
|
100
171
|
readSessionUserId,
|
|
101
172
|
sessionCookieName,
|
|
102
173
|
sessionRememberTtlSeconds,
|
|
@@ -84,11 +84,67 @@ function resolveAbilitiesForRole(role) {
|
|
|
84
84
|
|
|
85
85
|
// ../../src/core/auth/sessionCookie.ts
|
|
86
86
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
87
|
+
|
|
88
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
89
|
+
function appKeyPrefix() {
|
|
90
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
91
|
+
}
|
|
92
|
+
function appCookieName(kind) {
|
|
93
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
94
|
+
}
|
|
95
|
+
function appDevSecret(kind) {
|
|
96
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
97
|
+
}
|
|
98
|
+
function namespacedRedisKey(kind) {
|
|
99
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
100
|
+
}
|
|
101
|
+
function smtpEhloHost() {
|
|
102
|
+
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
103
|
+
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
104
|
+
return safe || "strata.local";
|
|
105
|
+
}
|
|
106
|
+
function siemEventType() {
|
|
107
|
+
return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
|
|
108
|
+
}
|
|
109
|
+
function appUserAgent() {
|
|
110
|
+
return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
|
|
111
|
+
}
|
|
112
|
+
function otelServiceName() {
|
|
113
|
+
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
114
|
+
}
|
|
115
|
+
function webhookSignatureHeader() {
|
|
116
|
+
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
117
|
+
}
|
|
118
|
+
function appDisplayName() {
|
|
119
|
+
return process.env.APP_NAME?.trim() || "WorkHub";
|
|
120
|
+
}
|
|
121
|
+
function appEnv() {
|
|
122
|
+
return process.env.APP_ENV?.trim() || "local";
|
|
123
|
+
}
|
|
124
|
+
function appUrl() {
|
|
125
|
+
return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
|
|
126
|
+
}
|
|
127
|
+
function apiPrefix() {
|
|
128
|
+
const raw = process.env.API_PREFIX?.trim() || "/api/v1";
|
|
129
|
+
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
130
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
131
|
+
return trimmed || "/api/v1";
|
|
132
|
+
}
|
|
133
|
+
function sdkClientClassName() {
|
|
134
|
+
const override = process.env.APP_SDK_CLASS?.trim();
|
|
135
|
+
if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
|
|
136
|
+
return override;
|
|
137
|
+
}
|
|
138
|
+
const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
|
|
139
|
+
return fromName ? `${fromName}Client` : "AppClient";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ../../src/core/auth/sessionCookie.ts
|
|
87
143
|
var SESSION_COOKIE = "workhub_session";
|
|
88
144
|
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
89
145
|
var SESSION_REMEMBER_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
90
146
|
function sessionCookieName() {
|
|
91
|
-
return process.env.SESSION_COOKIE_NAME?.trim() ||
|
|
147
|
+
return process.env.SESSION_COOKIE_NAME?.trim() || appCookieName("session");
|
|
92
148
|
}
|
|
93
149
|
function parsePositiveSeconds(raw, fallback) {
|
|
94
150
|
const parsed = Number.parseInt(raw ?? "", 10);
|
|
@@ -101,7 +157,7 @@ function sessionRememberTtlSeconds() {
|
|
|
101
157
|
return parsePositiveSeconds(process.env.SESSION_REMEMBER_TTL_SECONDS, SESSION_REMEMBER_TTL_SECONDS);
|
|
102
158
|
}
|
|
103
159
|
function resolveSessionSecret() {
|
|
104
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "
|
|
160
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || appDevSecret("session-secret");
|
|
105
161
|
}
|
|
106
162
|
function signSession(userId, issuedAt, ttlSeconds) {
|
|
107
163
|
const payload = ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
|
|
@@ -145,9 +201,9 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
|
|
|
145
201
|
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
146
202
|
return null;
|
|
147
203
|
}
|
|
148
|
-
return userId;
|
|
204
|
+
return { userId, issuedAt };
|
|
149
205
|
}
|
|
150
|
-
function
|
|
206
|
+
function readSession(request) {
|
|
151
207
|
const cookieValue = readCookieValue(request, sessionCookieName());
|
|
152
208
|
if (!cookieValue) {
|
|
153
209
|
return null;
|
|
@@ -163,6 +219,19 @@ function readSessionUserId(request) {
|
|
|
163
219
|
const [userIdRaw, issuedAtRaw, cookieSignature] = parts;
|
|
164
220
|
return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, sessionTtlSeconds(), false);
|
|
165
221
|
}
|
|
222
|
+
function readSessionUserId(request) {
|
|
223
|
+
return readSession(request)?.userId ?? null;
|
|
224
|
+
}
|
|
225
|
+
function isSessionInvalidated(issuedAt, validAfter) {
|
|
226
|
+
if (!validAfter) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const timestamp = validAfter instanceof Date ? validAfter.getTime() : Date.parse(String(validAfter));
|
|
230
|
+
if (!Number.isFinite(timestamp)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return issuedAt < timestamp;
|
|
234
|
+
}
|
|
166
235
|
function createSessionCookie(userId, options = {}) {
|
|
167
236
|
const issuedAt = Date.now();
|
|
168
237
|
const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
|
|
@@ -182,8 +251,8 @@ class SessionGuard {
|
|
|
182
251
|
this.container = container;
|
|
183
252
|
}
|
|
184
253
|
async resolve(request) {
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
254
|
+
const session = readSession(request);
|
|
255
|
+
if (!session) {
|
|
187
256
|
return null;
|
|
188
257
|
}
|
|
189
258
|
const tokenService = resolveAuthUserDirectory(this.container);
|
|
@@ -191,7 +260,10 @@ class SessionGuard {
|
|
|
191
260
|
return null;
|
|
192
261
|
}
|
|
193
262
|
try {
|
|
194
|
-
const user = await tokenService.findByIdOrThrow(userId);
|
|
263
|
+
const user = await tokenService.findByIdOrThrow(session.userId);
|
|
264
|
+
if (isSessionInvalidated(session.issuedAt, user.session_valid_after)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
195
267
|
return {
|
|
196
268
|
id: user.id,
|
|
197
269
|
role: user.role,
|
|
@@ -1,12 +1,68 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/auth/tokenHash.ts
|
|
3
3
|
import { createHash, createHmac } from "crypto";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
6
|
+
function appKeyPrefix() {
|
|
7
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
|
+
}
|
|
9
|
+
function appCookieName(kind) {
|
|
10
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
11
|
+
}
|
|
12
|
+
function appDevSecret(kind) {
|
|
13
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
|
+
}
|
|
15
|
+
function namespacedRedisKey(kind) {
|
|
16
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
17
|
+
}
|
|
18
|
+
function smtpEhloHost() {
|
|
19
|
+
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
20
|
+
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
21
|
+
return safe || "strata.local";
|
|
22
|
+
}
|
|
23
|
+
function siemEventType() {
|
|
24
|
+
return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
|
|
25
|
+
}
|
|
26
|
+
function appUserAgent() {
|
|
27
|
+
return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
|
|
28
|
+
}
|
|
29
|
+
function otelServiceName() {
|
|
30
|
+
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
31
|
+
}
|
|
32
|
+
function webhookSignatureHeader() {
|
|
33
|
+
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
34
|
+
}
|
|
35
|
+
function appDisplayName() {
|
|
36
|
+
return process.env.APP_NAME?.trim() || "WorkHub";
|
|
37
|
+
}
|
|
38
|
+
function appEnv() {
|
|
39
|
+
return process.env.APP_ENV?.trim() || "local";
|
|
40
|
+
}
|
|
41
|
+
function appUrl() {
|
|
42
|
+
return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
|
|
43
|
+
}
|
|
44
|
+
function apiPrefix() {
|
|
45
|
+
const raw = process.env.API_PREFIX?.trim() || "/api/v1";
|
|
46
|
+
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
47
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
48
|
+
return trimmed || "/api/v1";
|
|
49
|
+
}
|
|
50
|
+
function sdkClientClassName() {
|
|
51
|
+
const override = process.env.APP_SDK_CLASS?.trim();
|
|
52
|
+
if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
|
|
53
|
+
return override;
|
|
54
|
+
}
|
|
55
|
+
const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
|
|
56
|
+
return fromName ? `${fromName}Client` : "AppClient";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ../../src/core/auth/tokenHash.ts
|
|
4
60
|
function resolveTokenPepper() {
|
|
5
|
-
return process.env.TOKEN_HASH_PEPPER?.trim() ?? "
|
|
61
|
+
return process.env.TOKEN_HASH_PEPPER?.trim() ?? appDevSecret("token-pepper");
|
|
6
62
|
}
|
|
7
63
|
function hashApiToken(token) {
|
|
8
64
|
const pepper = resolveTokenPepper();
|
|
9
|
-
if (pepper && pepper !== "
|
|
65
|
+
if (pepper && pepper !== appDevSecret("token-pepper")) {
|
|
10
66
|
return createHmac("sha256", pepper).update(token).digest("hex");
|
|
11
67
|
}
|
|
12
68
|
return createHash("sha256").update(token).digest("hex");
|
|
@@ -6,6 +6,12 @@ var {RedisClient } = globalThis.Bun;
|
|
|
6
6
|
function appKeyPrefix() {
|
|
7
7
|
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
8
8
|
}
|
|
9
|
+
function appCookieName(kind) {
|
|
10
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
11
|
+
}
|
|
12
|
+
function appDevSecret(kind) {
|
|
13
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
14
|
+
}
|
|
9
15
|
function namespacedRedisKey(kind) {
|
|
10
16
|
return `${appKeyPrefix()}:${kind}`;
|
|
11
17
|
}
|
package/dist/entries/facades.js
CHANGED
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
function appKeyPrefix() {
|
|
16
16
|
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
17
17
|
}
|
|
18
|
+
function appCookieName(kind) {
|
|
19
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
20
|
+
}
|
|
21
|
+
function appDevSecret(kind) {
|
|
22
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
23
|
+
}
|
|
18
24
|
function namespacedRedisKey(kind) {
|
|
19
25
|
return `${appKeyPrefix()}:${kind}`;
|
|
20
26
|
}
|
|
@@ -5,6 +5,60 @@ import { ForbiddenError } from "@getstrata/core/errors/http";
|
|
|
5
5
|
// ../../src/core/http/csrfToken.ts
|
|
6
6
|
import { timingSafeEqual } from "crypto";
|
|
7
7
|
|
|
8
|
+
// ../../src/core/runtime/appKeyPrefix.ts
|
|
9
|
+
function appKeyPrefix() {
|
|
10
|
+
return process.env.APP_KEY_PREFIX?.trim() || "workhub";
|
|
11
|
+
}
|
|
12
|
+
function appCookieName(kind) {
|
|
13
|
+
return `${appKeyPrefix()}_${kind}`;
|
|
14
|
+
}
|
|
15
|
+
function appDevSecret(kind) {
|
|
16
|
+
return `${appKeyPrefix()}-dev-${kind}`;
|
|
17
|
+
}
|
|
18
|
+
function namespacedRedisKey(kind) {
|
|
19
|
+
return `${appKeyPrefix()}:${kind}`;
|
|
20
|
+
}
|
|
21
|
+
function smtpEhloHost() {
|
|
22
|
+
const raw = process.env.MAIL_EHLO?.trim() || `${appKeyPrefix()}.local`;
|
|
23
|
+
const safe = raw.replace(/[^a-zA-Z0-9.-]/g, "");
|
|
24
|
+
return safe || "strata.local";
|
|
25
|
+
}
|
|
26
|
+
function siemEventType() {
|
|
27
|
+
return process.env.SIEM_EVENT_TYPE?.trim() || `${appKeyPrefix()}.audit`;
|
|
28
|
+
}
|
|
29
|
+
function appUserAgent() {
|
|
30
|
+
return process.env.APP_USER_AGENT?.trim() || appKeyPrefix();
|
|
31
|
+
}
|
|
32
|
+
function otelServiceName() {
|
|
33
|
+
return process.env.OTEL_SERVICE_NAME?.trim() || `${appKeyPrefix()}-api`;
|
|
34
|
+
}
|
|
35
|
+
function webhookSignatureHeader() {
|
|
36
|
+
return process.env.WEBHOOK_SIGNATURE_HEADER?.trim() || `x-${appKeyPrefix()}-signature`;
|
|
37
|
+
}
|
|
38
|
+
function appDisplayName() {
|
|
39
|
+
return process.env.APP_NAME?.trim() || "WorkHub";
|
|
40
|
+
}
|
|
41
|
+
function appEnv() {
|
|
42
|
+
return process.env.APP_ENV?.trim() || "local";
|
|
43
|
+
}
|
|
44
|
+
function appUrl() {
|
|
45
|
+
return (process.env.APP_URL?.trim() || "http://localhost:3000").replace(/\/$/, "");
|
|
46
|
+
}
|
|
47
|
+
function apiPrefix() {
|
|
48
|
+
const raw = process.env.API_PREFIX?.trim() || "/api/v1";
|
|
49
|
+
const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
|
|
50
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
51
|
+
return trimmed || "/api/v1";
|
|
52
|
+
}
|
|
53
|
+
function sdkClientClassName() {
|
|
54
|
+
const override = process.env.APP_SDK_CLASS?.trim();
|
|
55
|
+
if (override && /^[A-Za-z_][A-Za-z0-9_]*$/.test(override)) {
|
|
56
|
+
return override;
|
|
57
|
+
}
|
|
58
|
+
const fromName = appDisplayName().replace(/[^A-Za-z0-9]/g, "");
|
|
59
|
+
return fromName ? `${fromName}Client` : "AppClient";
|
|
60
|
+
}
|
|
61
|
+
|
|
8
62
|
// ../../src/core/http/cookies.ts
|
|
9
63
|
function readRequestCookie(request, name) {
|
|
10
64
|
const cookies = request.cookies;
|
|
@@ -63,10 +117,10 @@ function currentRequestMeta() {
|
|
|
63
117
|
var CSRF_COOKIE = "workhub_csrf";
|
|
64
118
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
65
119
|
function csrfCookieName() {
|
|
66
|
-
return process.env.CSRF_COOKIE_NAME?.trim() ||
|
|
120
|
+
return process.env.CSRF_COOKIE_NAME?.trim() || appCookieName("csrf");
|
|
67
121
|
}
|
|
68
122
|
function resolveCsrfSecret() {
|
|
69
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "
|
|
123
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || appDevSecret("csrf-secret");
|
|
70
124
|
}
|
|
71
125
|
function csrfVerifyOptions() {
|
|
72
126
|
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|