@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/auth/sessionCookie.d.ts +8 -2
  3. package/dist/core/contracts/authUserDirectory.d.ts +1 -0
  4. package/dist/core/runtime/appKeyPrefix.d.ts +3 -1
  5. package/dist/entries/audit/exportAuditLogs.js +6 -0
  6. package/dist/entries/audit/siemFormatter.js +6 -0
  7. package/dist/entries/auth/oauth/providers.js +6 -0
  8. package/dist/entries/auth/oauth/samlProvider.js +6 -0
  9. package/dist/entries/auth/passwordConfirmCookie.js +58 -2
  10. package/dist/entries/auth/sessionCookie.js +75 -4
  11. package/dist/entries/auth/sessionGuard.js +79 -7
  12. package/dist/entries/auth/tokenHash.js +58 -2
  13. package/dist/entries/cache/createCacheStore.js +6 -0
  14. package/dist/entries/facades.js +6 -0
  15. package/dist/entries/http/csrfMiddleware.js +56 -2
  16. package/dist/entries/http/csrfToken.js +56 -2
  17. package/dist/entries/http/flashMiddleware.js +58 -2
  18. package/dist/entries/http/flashSession.js +58 -2
  19. package/dist/entries/http/loginThrottleMiddleware.js +6 -0
  20. package/dist/entries/http/memoryThrottleMiddleware.js +6 -0
  21. package/dist/entries/http/scimThrottleMiddleware.js +6 -0
  22. package/dist/entries/http/securityHeadersMiddleware.js +6 -0
  23. package/dist/entries/http/signedUrl.js +57 -1
  24. package/dist/entries/http/throttleMiddleware.js +6 -0
  25. package/dist/entries/jobs/exportAuditLogsJob.js +6 -0
  26. package/dist/entries/mail/mailer.js +6 -0
  27. package/dist/entries/openapi/generator.js +6 -0
  28. package/dist/entries/queue/createAppQueue.js +6 -0
  29. package/dist/entries/queue/publicQueue.js +6 -0
  30. package/dist/entries/queue/queueMetrics.js +6 -0
  31. package/dist/entries/queue/redisQueue.js +6 -0
  32. package/dist/entries/runtime/appKeyPrefix.js +8 -0
  33. package/dist/entries/security/oauthState.js +57 -1
  34. package/dist/entries/security/safeFetch.js +6 -0
  35. package/dist/entries/tracing/tracingMiddleware.js +6 -0
  36. package/dist/entries/view.js +58 -4
  37. package/dist/index.js +11 -7
  38. 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, };
@@ -4,6 +4,7 @@ interface AuthUserRecord {
4
4
  email?: string | null;
5
5
  role: string;
6
6
  email_verified_at?: Date | string | null;
7
+ session_valid_after?: Date | string | null;
7
8
  }
8
9
  interface AuthUserDirectory {
9
10
  resolveUserFromToken(token: string): Promise<AuthUser | null>;
@@ -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() || PASSWORD_CONFIRM_COOKIE;
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() || "workhub-dev-session-secret";
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() || SESSION_COOKIE;
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() || "workhub-dev-session-secret";
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 readSessionUserId(request) {
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() || SESSION_COOKIE;
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() || "workhub-dev-session-secret";
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 readSessionUserId(request) {
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 userId = readSessionUserId(request);
186
- if (!userId) {
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() ?? "workhub-dev-token-pepper";
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 !== "workhub-dev-token-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
  }
@@ -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() || CSRF_COOKIE;
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() || "workhub-dev-csrf-secret";
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 };