@getstrata/core 0.5.57 → 0.5.59

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 ADDED
@@ -0,0 +1,14 @@
1
+ # @getstrata/core changelog
2
+
3
+ ## 0.5.59
4
+
5
+ Sibling HTMX apps can consume published packages without WorkHub-only glue.
6
+
7
+ - Emit `@getstrata/core/facades` types at `dist/core/facades/index.d.ts` (CI checks every `exports.*.types` path after build).
8
+ - Session and CSRF cookie names are configurable (`SESSION_COOKIE_NAME`, `CSRF_COOKIE_NAME`). Defaults remain `workhub_session` and `workhub_csrf` for WorkHub.
9
+ - Split `CORE_ABILITY_CHECKER_TOKEN` and `CORE_AUTH_USER_DIRECTORY_TOKEN` from `CORE_TOKEN_SERVICE_TOKEN`. HttpKernel prefers the ability-checker token; HMAC `SessionGuard` / layout data prefer the user directory. A compatibility shim uses `CORE_TOKEN_SERVICE_TOKEN` only when the bound value matches the requested type.
10
+ - `configureWebLayoutData` lets apps choose `currentUser` vs `authUser`, a custom user loader, and extra template fields. WorkHub still gets `{ authUser, csrfToken, flash }`.
11
+ - `redirectResponse`, `notFoundHtmlResponse`, `textResponse`, and `xmlResponse` join `htmlResponse` / `isHtmxRequest` on `@getstrata/core/view`.
12
+ - `LOGIN_RATE_LIMIT_WINDOW_MS` is a deprecated alias for `LOGIN_RATE_LIMIT_WINDOW_SECONDS`.
13
+
14
+ HMAC `SessionGuard` is unchanged for WorkHub API/token apps. Sibling HTMX apps should use `@getstrata/bootstrap` `CookieSessionStore` + `createCookieSessionAuthManager`.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Stable **Strata** framework surface for application modules.
5
5
  **Source:** `src/framework/public-api.ts` (monorepo)
6
6
  **Repository:** [EyK-26/strata](https://github.com/EyK-26/strata), directory `packages/strata-core`
7
7
 
8
- WorkHub is the reference application built on Strata; import the framework from this package in your own modules.
8
+ WorkHub is the reference application built on Strata; import the framework from this package in your own modules. Sibling HTMX apps (Eta, cookie sessions, no API tokens) should follow [docs/SIBLING-HTMX.md](../../docs/SIBLING-HTMX.md).
9
9
 
10
10
  ## Usage
11
11
 
@@ -82,7 +82,7 @@ import type { Migration } from "@getstrata/core/database/migrations/types";
82
82
  Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
83
83
 
84
84
  1. Add `NPM_TOKEN` to GitHub repository secrets.
85
- 2. Tag a release: `git tag v0.5.57 && git push origin v0.5.57`
85
+ 2. Tag a release: `git tag v0.5.60 && git push origin v0.5.60`
86
86
  3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
87
87
 
88
88
  Previously published as `@eyk-workhub/framework@0.1.0`, deprecated in favor of this package.
@@ -4,7 +4,7 @@ declare const CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
4
4
  declare const CACHE_DRIVER_CONFIG_KEY = "cache.driver";
5
5
  declare const REDIS_URL_CONFIG_KEY = "cache.redisUrl";
6
6
  declare const DATABASE_URL_CONFIG_KEY = "database.url";
7
- export { CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "@getstrata/core/contracts/serviceTokens";
7
+ export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "@getstrata/core/contracts/serviceTokens";
8
8
  declare const AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
9
9
  declare const DEFAULT_APP_PORT = 3000;
10
10
  declare const DEFAULT_CACHE_TTL_MS = 3600000;
@@ -1,6 +1,7 @@
1
1
  declare const SESSION_COOKIE = "workhub_session";
2
2
  declare const SESSION_TTL_SECONDS: number;
3
+ declare function sessionCookieName(): string;
3
4
  declare function readSessionUserId(request: Request): number | null;
4
5
  declare function createSessionCookie(userId: number): string;
5
6
  declare function clearSessionCookie(): string;
6
- export { clearSessionCookie, createSessionCookie, readSessionUserId, SESSION_COOKIE, SESSION_TTL_SECONDS, };
7
+ export { clearSessionCookie, createSessionCookie, readSessionUserId, SESSION_COOKIE, SESSION_TTL_SECONDS, sessionCookieName, };
@@ -1,3 +1,6 @@
1
+ import type { AbilityChecker } from "../auth/abilityChecker";
2
+ import type { AuthUserDirectory } from "./authUserDirectory";
3
+ import type { ServiceContainerLike } from "./container";
1
4
  declare const CORE_CONFIG_TOKEN = "core.config";
2
5
  declare const CORE_CACHE_TOKEN = "core.cache";
3
6
  declare const CORE_QUEUE_TOKEN = "core.queue";
@@ -5,4 +8,10 @@ declare const CORE_EVENT_BUS_TOKEN = "core.eventBus";
5
8
  declare const CORE_POLICY_GATE_TOKEN = "core.policyGate";
6
9
  declare const CORE_AUTH_TOKEN = "core.auth";
7
10
  declare const CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
8
- export { CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, };
11
+ declare const CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
12
+ declare const CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
13
+ declare function isAbilityChecker(value: unknown): value is AbilityChecker;
14
+ declare function isAuthUserDirectory(value: unknown): value is AuthUserDirectory;
15
+ declare function resolveAbilityChecker(container: ServiceContainerLike): AbilityChecker;
16
+ declare function resolveAuthUserDirectory(container: ServiceContainerLike): AuthUserDirectory | null;
17
+ export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, isAbilityChecker, isAuthUserDirectory, resolveAbilityChecker, resolveAuthUserDirectory, };
@@ -1,4 +1,5 @@
1
1
  declare const CSRF_COOKIE = "workhub_csrf";
2
+ declare function csrfCookieName(): string;
2
3
  declare function createCsrfTokenCookie(): {
3
4
  token: string;
4
5
  cookie: string;
@@ -11,4 +12,4 @@ declare function readSubmittedCsrfToken(request: Request): string | null;
11
12
  declare function readSubmittedCsrfTokenFromBody(request: Request): Promise<string | null>;
12
13
  declare function verifyCsrfToken(request: Request, submittedToken: string | null): boolean;
13
14
  declare function resolveCsrfTokenForRequest(request: Request): string;
14
- export { CSRF_COOKIE, createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, };
15
+ export { CSRF_COOKIE, createCsrfTokenCookie, csrfCookieName, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, };
@@ -3,4 +3,12 @@ declare function htmlResponse(html: string, init?: {
3
3
  statusText?: string;
4
4
  }): Response;
5
5
  declare function isHtmxRequest(request: Request): boolean;
6
- export { htmlResponse, isHtmxRequest };
6
+ declare function redirectResponse(location: string, status?: number): Response;
7
+ declare function notFoundHtmlResponse(body?: string): Response;
8
+ declare function textResponse(body: string, init?: {
9
+ status?: number;
10
+ }): Response;
11
+ declare function xmlResponse(body: string, init?: {
12
+ status?: number;
13
+ }): Response;
14
+ export { htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, textResponse, xmlResponse, };
@@ -1,5 +1,5 @@
1
1
  export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine } from "./etaViewEngine";
2
- export { htmlResponse, isHtmxRequest } from "./htmlResponse";
2
+ export { htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, textResponse, xmlResponse, } from "./htmlResponse";
3
3
  export type { ViewEngine } from "./viewEngine";
4
- export type { WebLayoutAuthUser, WebLayoutData } from "./webLayoutData";
5
- export { resolveWebLayoutData } from "./webLayoutData";
4
+ export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "./webLayoutData";
5
+ export { configureWebLayoutData, resolveWebLayoutData } from "./webLayoutData";
@@ -3,15 +3,26 @@ interface WebLayoutAuthUser {
3
3
  id: number;
4
4
  email: string;
5
5
  role: string;
6
+ [key: string]: unknown;
6
7
  }
7
8
  interface WebLayoutData {
8
- authUser: WebLayoutAuthUser | null;
9
+ authUser?: WebLayoutAuthUser | null;
10
+ currentUser?: WebLayoutAuthUser | null;
9
11
  csrfToken: string;
10
12
  flash: {
11
13
  level: string;
12
14
  message: string;
13
15
  } | null;
16
+ [key: string]: unknown;
14
17
  }
15
- declare function resolveWebLayoutData(container: ServiceContainerLike, request?: Request): Promise<Record<string, unknown>>;
16
- export type { WebLayoutAuthUser, WebLayoutData };
17
- export { resolveWebLayoutData };
18
+ type WebLayoutUserKey = "authUser" | "currentUser";
19
+ interface WebLayoutDataOptions {
20
+ userKey?: WebLayoutUserKey;
21
+ loadUser?: (container: ServiceContainerLike, request: Request | undefined) => Promise<Record<string, unknown> | null>;
22
+ extra?: Record<string, unknown> | ((user: Record<string, unknown> | null) => Record<string, unknown> | Promise<Record<string, unknown>>);
23
+ }
24
+ declare function configureWebLayoutData(options: WebLayoutDataOptions): void;
25
+ declare function resetWebLayoutDataConfigForTests(): void;
26
+ declare function resolveWebLayoutData(container: ServiceContainerLike, request?: Request, options?: WebLayoutDataOptions): Promise<Record<string, unknown>>;
27
+ export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey };
28
+ export { configureWebLayoutData, resetWebLayoutDataConfigForTests, resolveWebLayoutData };
@@ -3,6 +3,9 @@
3
3
  import { createHmac, timingSafeEqual } from "crypto";
4
4
  var SESSION_COOKIE = "workhub_session";
5
5
  var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
6
+ function sessionCookieName() {
7
+ return process.env.SESSION_COOKIE_NAME?.trim() || SESSION_COOKIE;
8
+ }
6
9
  function resolveSessionSecret() {
7
10
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-session-secret";
8
11
  }
@@ -25,7 +28,7 @@ function readCookieValue(request, cookieName) {
25
28
  return null;
26
29
  }
27
30
  function readSessionUserId(request) {
28
- const cookieValue = readCookieValue(request, SESSION_COOKIE);
31
+ const cookieValue = readCookieValue(request, sessionCookieName());
29
32
  if (!cookieValue) {
30
33
  return null;
31
34
  }
@@ -60,16 +63,17 @@ function createSessionCookie(userId) {
60
63
  const issuedAt = Date.now();
61
64
  const value = signSession(userId, issuedAt);
62
65
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
63
- return `${SESSION_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}${secure}`;
66
+ return `${sessionCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}${secure}`;
64
67
  }
65
68
  function clearSessionCookie() {
66
69
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
67
- return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`;
70
+ return `${sessionCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`;
68
71
  }
69
72
  export {
70
73
  SESSION_COOKIE,
71
74
  SESSION_TTL_SECONDS,
72
75
  clearSessionCookie,
73
76
  createSessionCookie,
74
- readSessionUserId
77
+ readSessionUserId,
78
+ sessionCookieName
75
79
  };
@@ -7,6 +7,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
7
7
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
8
8
  var CORE_AUTH_TOKEN = "core.auth";
9
9
  var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
10
+ var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
11
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
12
+ function isAbilityChecker(value) {
13
+ if (!value || typeof value !== "object") {
14
+ return false;
15
+ }
16
+ const candidate = value;
17
+ return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
18
+ }
19
+ function isAuthUserDirectory(value) {
20
+ if (!value || typeof value !== "object") {
21
+ return false;
22
+ }
23
+ return typeof value.findByIdOrThrow === "function";
24
+ }
25
+ function resolveAbilityChecker(container) {
26
+ if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
27
+ return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
28
+ }
29
+ return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
30
+ }
31
+ function resolveAuthUserDirectory(container) {
32
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
33
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
34
+ if (isAuthUserDirectory(directory)) {
35
+ return directory;
36
+ }
37
+ }
38
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
39
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
40
+ if (isAuthUserDirectory(tokenService)) {
41
+ return tokenService;
42
+ }
43
+ }
44
+ return null;
45
+ }
10
46
 
11
47
  // ../../src/core/auth/abilityCatalog.ts
12
48
  var MEMBER_ABILITIES = [
@@ -50,6 +86,9 @@ function resolveAbilitiesForRole(role) {
50
86
  import { createHmac, timingSafeEqual } from "crypto";
51
87
  var SESSION_COOKIE = "workhub_session";
52
88
  var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
89
+ function sessionCookieName() {
90
+ return process.env.SESSION_COOKIE_NAME?.trim() || SESSION_COOKIE;
91
+ }
53
92
  function resolveSessionSecret() {
54
93
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-session-secret";
55
94
  }
@@ -72,7 +111,7 @@ function readCookieValue(request, cookieName) {
72
111
  return null;
73
112
  }
74
113
  function readSessionUserId(request) {
75
- const cookieValue = readCookieValue(request, SESSION_COOKIE);
114
+ const cookieValue = readCookieValue(request, sessionCookieName());
76
115
  if (!cookieValue) {
77
116
  return null;
78
117
  }
@@ -107,11 +146,11 @@ function createSessionCookie(userId) {
107
146
  const issuedAt = Date.now();
108
147
  const value = signSession(userId, issuedAt);
109
148
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
110
- return `${SESSION_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}${secure}`;
149
+ return `${sessionCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}${secure}`;
111
150
  }
112
151
  function clearSessionCookie() {
113
152
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
114
- return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`;
153
+ return `${sessionCookieName()}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`;
115
154
  }
116
155
 
117
156
  // ../../src/core/auth/sessionGuard.ts
@@ -125,10 +164,10 @@ class SessionGuard {
125
164
  if (!userId) {
126
165
  return null;
127
166
  }
128
- if (!this.container.has(CORE_TOKEN_SERVICE_TOKEN)) {
167
+ const tokenService = resolveAuthUserDirectory(this.container);
168
+ if (!tokenService) {
129
169
  return null;
130
170
  }
131
- const tokenService = this.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
132
171
  try {
133
172
  const user = await tokenService.findByIdOrThrow(userId);
134
173
  return {
@@ -7,12 +7,54 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
7
7
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
8
8
  var CORE_AUTH_TOKEN = "core.auth";
9
9
  var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
10
+ var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
11
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
12
+ function isAbilityChecker(value) {
13
+ if (!value || typeof value !== "object") {
14
+ return false;
15
+ }
16
+ const candidate = value;
17
+ return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
18
+ }
19
+ function isAuthUserDirectory(value) {
20
+ if (!value || typeof value !== "object") {
21
+ return false;
22
+ }
23
+ return typeof value.findByIdOrThrow === "function";
24
+ }
25
+ function resolveAbilityChecker(container) {
26
+ if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
27
+ return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
28
+ }
29
+ return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
30
+ }
31
+ function resolveAuthUserDirectory(container) {
32
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
33
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
34
+ if (isAuthUserDirectory(directory)) {
35
+ return directory;
36
+ }
37
+ }
38
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
39
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
40
+ if (isAuthUserDirectory(tokenService)) {
41
+ return tokenService;
42
+ }
43
+ }
44
+ return null;
45
+ }
10
46
  export {
47
+ CORE_ABILITY_CHECKER_TOKEN,
11
48
  CORE_AUTH_TOKEN,
49
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
12
50
  CORE_CACHE_TOKEN,
13
51
  CORE_CONFIG_TOKEN,
14
52
  CORE_EVENT_BUS_TOKEN,
15
53
  CORE_POLICY_GATE_TOKEN,
16
54
  CORE_QUEUE_TOKEN,
17
- CORE_TOKEN_SERVICE_TOKEN
55
+ CORE_TOKEN_SERVICE_TOKEN,
56
+ isAbilityChecker,
57
+ isAuthUserDirectory,
58
+ resolveAbilityChecker,
59
+ resolveAuthUserDirectory
18
60
  };
@@ -62,6 +62,9 @@ function currentRequestMeta() {
62
62
  // ../../src/core/http/csrfToken.ts
63
63
  var CSRF_COOKIE = "workhub_csrf";
64
64
  var CSRF_TTL_MS = 60 * 60 * 1000;
65
+ function csrfCookieName() {
66
+ return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
67
+ }
65
68
  function resolveCsrfSecret() {
66
69
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
67
70
  }
@@ -81,11 +84,11 @@ function createCsrfTokenCookie() {
81
84
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
82
85
  return {
83
86
  token,
84
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
87
+ cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
85
88
  };
86
89
  }
87
90
  function resolveCsrfToken(request) {
88
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
91
+ const cookieValue = readRequestCookie(request, csrfCookieName());
89
92
  if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
90
93
  return { token: cookieValue };
91
94
  }
@@ -121,7 +124,7 @@ function verifyCsrfToken(request, submittedToken) {
121
124
  if (!submittedToken) {
122
125
  return false;
123
126
  }
124
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
127
+ const cookieValue = readRequestCookie(request, csrfCookieName());
125
128
  if (!cookieValue) {
126
129
  return false;
127
130
  }
@@ -59,6 +59,9 @@ function currentRequestMeta() {
59
59
  // ../../src/core/http/csrfToken.ts
60
60
  var CSRF_COOKIE = "workhub_csrf";
61
61
  var CSRF_TTL_MS = 60 * 60 * 1000;
62
+ function csrfCookieName() {
63
+ return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
64
+ }
62
65
  function resolveCsrfSecret() {
63
66
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
64
67
  }
@@ -78,11 +81,11 @@ function createCsrfTokenCookie() {
78
81
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
79
82
  return {
80
83
  token,
81
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
84
+ cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
82
85
  };
83
86
  }
84
87
  function resolveCsrfToken(request) {
85
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
88
+ const cookieValue = readRequestCookie(request, csrfCookieName());
86
89
  if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
87
90
  return { token: cookieValue };
88
91
  }
@@ -118,7 +121,7 @@ function verifyCsrfToken(request, submittedToken) {
118
121
  if (!submittedToken) {
119
122
  return false;
120
123
  }
121
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
124
+ const cookieValue = readRequestCookie(request, csrfCookieName());
122
125
  if (!cookieValue) {
123
126
  return false;
124
127
  }
@@ -137,6 +140,7 @@ function resolveCsrfTokenForRequest(request) {
137
140
  export {
138
141
  CSRF_COOKIE,
139
142
  createCsrfTokenCookie,
143
+ csrfCookieName,
140
144
  readSubmittedCsrfToken,
141
145
  readSubmittedCsrfTokenFromBody,
142
146
  resolveCsrfToken,
@@ -309,6 +309,33 @@ function htmlResponse(html, init = {}) {
309
309
  function isHtmxRequest(request) {
310
310
  return request.headers.get("HX-Request") === "true";
311
311
  }
312
+ function redirectResponse(location, status = 302) {
313
+ return new Response(null, {
314
+ status,
315
+ headers: {
316
+ Location: location
317
+ }
318
+ });
319
+ }
320
+ function notFoundHtmlResponse(body = "Not Found") {
321
+ return htmlResponse(body, { status: 404 });
322
+ }
323
+ function textResponse(body, init = {}) {
324
+ return new Response(body, {
325
+ status: init.status ?? 200,
326
+ headers: {
327
+ "Content-Type": "text/plain; charset=utf-8"
328
+ }
329
+ });
330
+ }
331
+ function xmlResponse(body, init = {}) {
332
+ return new Response(body, {
333
+ status: init.status ?? 200,
334
+ headers: {
335
+ "Content-Type": "application/xml; charset=utf-8"
336
+ }
337
+ });
338
+ }
312
339
  // ../../src/core/view/webLayoutData.ts
313
340
  import { currentAuthUser } from "@getstrata/core/auth/authContext";
314
341
  import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
@@ -321,6 +348,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
321
348
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
322
349
  var CORE_AUTH_TOKEN = "core.auth";
323
350
  var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
351
+ var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
352
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
353
+ function isAbilityChecker(value) {
354
+ if (!value || typeof value !== "object") {
355
+ return false;
356
+ }
357
+ const candidate = value;
358
+ return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
359
+ }
360
+ function isAuthUserDirectory(value) {
361
+ if (!value || typeof value !== "object") {
362
+ return false;
363
+ }
364
+ return typeof value.findByIdOrThrow === "function";
365
+ }
366
+ function resolveAbilityChecker(container) {
367
+ if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
368
+ return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
369
+ }
370
+ return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
371
+ }
372
+ function resolveAuthUserDirectory(container) {
373
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
374
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
375
+ if (isAuthUserDirectory(directory)) {
376
+ return directory;
377
+ }
378
+ }
379
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
380
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
381
+ if (isAuthUserDirectory(tokenService)) {
382
+ return tokenService;
383
+ }
384
+ }
385
+ return null;
386
+ }
324
387
 
325
388
  // ../../src/core/http/csrfToken.ts
326
389
  import { timingSafeEqual } from "crypto";
@@ -382,6 +445,9 @@ function currentRequestMeta() {
382
445
  // ../../src/core/http/csrfToken.ts
383
446
  var CSRF_COOKIE = "workhub_csrf";
384
447
  var CSRF_TTL_MS = 60 * 60 * 1000;
448
+ function csrfCookieName() {
449
+ return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
450
+ }
385
451
  function resolveCsrfSecret() {
386
452
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
387
453
  }
@@ -401,11 +467,11 @@ function createCsrfTokenCookie() {
401
467
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
402
468
  return {
403
469
  token,
404
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
470
+ cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
405
471
  };
406
472
  }
407
473
  function resolveCsrfToken(request) {
408
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
474
+ const cookieValue = readRequestCookie(request, csrfCookieName());
409
475
  if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
410
476
  return { token: cookieValue };
411
477
  }
@@ -441,7 +507,7 @@ function verifyCsrfToken(request, submittedToken) {
441
507
  if (!submittedToken) {
442
508
  return false;
443
509
  }
444
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
510
+ const cookieValue = readRequestCookie(request, csrfCookieName());
445
511
  if (!cookieValue) {
446
512
  return false;
447
513
  }
@@ -558,44 +624,56 @@ function withFlashClear(response) {
558
624
  }
559
625
 
560
626
  // ../../src/core/view/webLayoutData.ts
561
- async function resolveWebLayoutData(container, request) {
562
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
563
- const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
627
+ var configuredLayoutOptions = {};
628
+ function configureWebLayoutData(options) {
629
+ configuredLayoutOptions = { ...options };
630
+ }
631
+ function layoutUserKey(options) {
632
+ return options.userKey ?? "authUser";
633
+ }
634
+ async function defaultLoadLayoutUser(container, _request) {
564
635
  const authUser = currentAuthUser();
565
636
  if (!authUser) {
566
- return { authUser: null, csrfToken, flash };
637
+ return null;
567
638
  }
568
639
  const userId = Number(authUser.id);
569
640
  if (!Number.isInteger(userId) || userId <= 0) {
570
- return { authUser: null, csrfToken, flash };
641
+ return null;
571
642
  }
572
- if (!container.has(CORE_TOKEN_SERVICE_TOKEN)) {
643
+ const directory = resolveAuthUserDirectory(container);
644
+ if (!directory) {
573
645
  return {
574
- authUser: {
575
- id: userId,
576
- email: "",
577
- role: authUser.role ?? "member"
578
- },
579
- csrfToken,
580
- flash
646
+ id: userId,
647
+ email: "",
648
+ role: authUser.role ?? "member"
581
649
  };
582
650
  }
583
- const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
584
651
  try {
585
- const user = await tokenService.findByIdOrThrow(userId);
652
+ const user = await directory.findByIdOrThrow(userId);
586
653
  return {
587
- authUser: {
588
- id: userId,
589
- email: user.email ?? "",
590
- role: authUser.role ?? user.role ?? "member"
591
- },
592
- csrfToken,
593
- flash
654
+ id: userId,
655
+ email: user.email ?? "",
656
+ role: authUser.role ?? user.role ?? "member"
594
657
  };
595
658
  } catch {
596
- return { authUser: null, csrfToken, flash };
659
+ return null;
597
660
  }
598
661
  }
662
+ async function resolveWebLayoutData(container, request, options = {}) {
663
+ const resolved = { ...configuredLayoutOptions, ...options };
664
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
665
+ const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
666
+ const userKey = layoutUserKey(resolved);
667
+ const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
668
+ const user = await loadUser(container, request);
669
+ const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
670
+ return {
671
+ [userKey]: user,
672
+ csrfToken,
673
+ flash,
674
+ ...extra
675
+ };
676
+ }
599
677
  // ../../src/core/http/contentNegotiation.ts
600
678
  function requestPrefersJson(request) {
601
679
  if (!request) {
@@ -306,6 +306,33 @@ function htmlResponse(html, init = {}) {
306
306
  function isHtmxRequest(request) {
307
307
  return request.headers.get("HX-Request") === "true";
308
308
  }
309
+ function redirectResponse(location, status = 302) {
310
+ return new Response(null, {
311
+ status,
312
+ headers: {
313
+ Location: location
314
+ }
315
+ });
316
+ }
317
+ function notFoundHtmlResponse(body = "Not Found") {
318
+ return htmlResponse(body, { status: 404 });
319
+ }
320
+ function textResponse(body, init = {}) {
321
+ return new Response(body, {
322
+ status: init.status ?? 200,
323
+ headers: {
324
+ "Content-Type": "text/plain; charset=utf-8"
325
+ }
326
+ });
327
+ }
328
+ function xmlResponse(body, init = {}) {
329
+ return new Response(body, {
330
+ status: init.status ?? 200,
331
+ headers: {
332
+ "Content-Type": "application/xml; charset=utf-8"
333
+ }
334
+ });
335
+ }
309
336
  // ../../src/core/view/webLayoutData.ts
310
337
  import { currentAuthUser } from "@getstrata/core/auth/authContext";
311
338
  import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
@@ -318,6 +345,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
318
345
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
319
346
  var CORE_AUTH_TOKEN = "core.auth";
320
347
  var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
348
+ var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
349
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
350
+ function isAbilityChecker(value) {
351
+ if (!value || typeof value !== "object") {
352
+ return false;
353
+ }
354
+ const candidate = value;
355
+ return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
356
+ }
357
+ function isAuthUserDirectory(value) {
358
+ if (!value || typeof value !== "object") {
359
+ return false;
360
+ }
361
+ return typeof value.findByIdOrThrow === "function";
362
+ }
363
+ function resolveAbilityChecker(container) {
364
+ if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
365
+ return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
366
+ }
367
+ return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
368
+ }
369
+ function resolveAuthUserDirectory(container) {
370
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
371
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
372
+ if (isAuthUserDirectory(directory)) {
373
+ return directory;
374
+ }
375
+ }
376
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
377
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
378
+ if (isAuthUserDirectory(tokenService)) {
379
+ return tokenService;
380
+ }
381
+ }
382
+ return null;
383
+ }
321
384
 
322
385
  // ../../src/core/http/csrfToken.ts
323
386
  import { timingSafeEqual } from "crypto";
@@ -379,6 +442,9 @@ function currentRequestMeta() {
379
442
  // ../../src/core/http/csrfToken.ts
380
443
  var CSRF_COOKIE = "workhub_csrf";
381
444
  var CSRF_TTL_MS = 60 * 60 * 1000;
445
+ function csrfCookieName() {
446
+ return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
447
+ }
382
448
  function resolveCsrfSecret() {
383
449
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
384
450
  }
@@ -398,11 +464,11 @@ function createCsrfTokenCookie() {
398
464
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
399
465
  return {
400
466
  token,
401
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
467
+ cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
402
468
  };
403
469
  }
404
470
  function resolveCsrfToken(request) {
405
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
471
+ const cookieValue = readRequestCookie(request, csrfCookieName());
406
472
  if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
407
473
  return { token: cookieValue };
408
474
  }
@@ -438,7 +504,7 @@ function verifyCsrfToken(request, submittedToken) {
438
504
  if (!submittedToken) {
439
505
  return false;
440
506
  }
441
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
507
+ const cookieValue = readRequestCookie(request, csrfCookieName());
442
508
  if (!cookieValue) {
443
509
  return false;
444
510
  }
@@ -555,44 +621,56 @@ function withFlashClear(response) {
555
621
  }
556
622
 
557
623
  // ../../src/core/view/webLayoutData.ts
558
- async function resolveWebLayoutData(container, request) {
559
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
560
- const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
624
+ var configuredLayoutOptions = {};
625
+ function configureWebLayoutData(options) {
626
+ configuredLayoutOptions = { ...options };
627
+ }
628
+ function layoutUserKey(options) {
629
+ return options.userKey ?? "authUser";
630
+ }
631
+ async function defaultLoadLayoutUser(container, _request) {
561
632
  const authUser = currentAuthUser();
562
633
  if (!authUser) {
563
- return { authUser: null, csrfToken, flash };
634
+ return null;
564
635
  }
565
636
  const userId = Number(authUser.id);
566
637
  if (!Number.isInteger(userId) || userId <= 0) {
567
- return { authUser: null, csrfToken, flash };
638
+ return null;
568
639
  }
569
- if (!container.has(CORE_TOKEN_SERVICE_TOKEN)) {
640
+ const directory = resolveAuthUserDirectory(container);
641
+ if (!directory) {
570
642
  return {
571
- authUser: {
572
- id: userId,
573
- email: "",
574
- role: authUser.role ?? "member"
575
- },
576
- csrfToken,
577
- flash
643
+ id: userId,
644
+ email: "",
645
+ role: authUser.role ?? "member"
578
646
  };
579
647
  }
580
- const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
581
648
  try {
582
- const user = await tokenService.findByIdOrThrow(userId);
649
+ const user = await directory.findByIdOrThrow(userId);
583
650
  return {
584
- authUser: {
585
- id: userId,
586
- email: user.email ?? "",
587
- role: authUser.role ?? user.role ?? "member"
588
- },
589
- csrfToken,
590
- flash
651
+ id: userId,
652
+ email: user.email ?? "",
653
+ role: authUser.role ?? user.role ?? "member"
591
654
  };
592
655
  } catch {
593
- return { authUser: null, csrfToken, flash };
656
+ return null;
594
657
  }
595
658
  }
659
+ async function resolveWebLayoutData(container, request, options = {}) {
660
+ const resolved = { ...configuredLayoutOptions, ...options };
661
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
662
+ const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
663
+ const userKey = layoutUserKey(resolved);
664
+ const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
665
+ const user = await loadUser(container, request);
666
+ const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
667
+ return {
668
+ [userKey]: user,
669
+ csrfToken,
670
+ flash,
671
+ ...extra
672
+ };
673
+ }
596
674
  // ../../src/core/http/contentNegotiation.ts
597
675
  function requestPrefersJson(request) {
598
676
  if (!request) {
@@ -102,7 +102,9 @@ var jobRegistry = readSharedJobRegistry();
102
102
 
103
103
  // ../../src/bootstrap/config.ts
104
104
  import {
105
+ CORE_ABILITY_CHECKER_TOKEN,
105
106
  CORE_AUTH_TOKEN,
107
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
106
108
  CORE_CACHE_TOKEN,
107
109
  CORE_CONFIG_TOKEN,
108
110
  CORE_EVENT_BUS_TOKEN,
@@ -1,7 +1,9 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/config.ts
3
3
  import {
4
+ CORE_ABILITY_CHECKER_TOKEN,
4
5
  CORE_AUTH_TOKEN,
6
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
5
7
  CORE_CACHE_TOKEN,
6
8
  CORE_CONFIG_TOKEN,
7
9
  CORE_EVENT_BUS_TOKEN,
@@ -102,7 +102,9 @@ var jobRegistry = readSharedJobRegistry();
102
102
 
103
103
  // ../../src/bootstrap/config.ts
104
104
  import {
105
+ CORE_ABILITY_CHECKER_TOKEN,
105
106
  CORE_AUTH_TOKEN,
107
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
106
108
  CORE_CACHE_TOKEN,
107
109
  CORE_CONFIG_TOKEN,
108
110
  CORE_EVENT_BUS_TOKEN,
@@ -4,7 +4,9 @@ var {RedisClient: RedisClient2 } = globalThis.Bun;
4
4
 
5
5
  // ../../src/bootstrap/config.ts
6
6
  import {
7
+ CORE_ABILITY_CHECKER_TOKEN,
7
8
  CORE_AUTH_TOKEN,
9
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
8
10
  CORE_CACHE_TOKEN,
9
11
  CORE_CONFIG_TOKEN,
10
12
  CORE_EVENT_BUS_TOKEN,
@@ -41,7 +41,9 @@ var jobRegistry = readSharedJobRegistry();
41
41
 
42
42
  // ../../src/bootstrap/config.ts
43
43
  import {
44
+ CORE_ABILITY_CHECKER_TOKEN,
44
45
  CORE_AUTH_TOKEN,
46
+ CORE_AUTH_USER_DIRECTORY_TOKEN,
45
47
  CORE_CACHE_TOKEN,
46
48
  CORE_CONFIG_TOKEN,
47
49
  CORE_EVENT_BUS_TOKEN,
@@ -225,6 +225,33 @@ function htmlResponse(html, init = {}) {
225
225
  function isHtmxRequest(request) {
226
226
  return request.headers.get("HX-Request") === "true";
227
227
  }
228
+ function redirectResponse(location, status = 302) {
229
+ return new Response(null, {
230
+ status,
231
+ headers: {
232
+ Location: location
233
+ }
234
+ });
235
+ }
236
+ function notFoundHtmlResponse(body = "Not Found") {
237
+ return htmlResponse(body, { status: 404 });
238
+ }
239
+ function textResponse(body, init = {}) {
240
+ return new Response(body, {
241
+ status: init.status ?? 200,
242
+ headers: {
243
+ "Content-Type": "text/plain; charset=utf-8"
244
+ }
245
+ });
246
+ }
247
+ function xmlResponse(body, init = {}) {
248
+ return new Response(body, {
249
+ status: init.status ?? 200,
250
+ headers: {
251
+ "Content-Type": "application/xml; charset=utf-8"
252
+ }
253
+ });
254
+ }
228
255
  // ../../src/core/view/webLayoutData.ts
229
256
  import { currentAuthUser } from "@getstrata/core/auth/authContext";
230
257
  import { currentRequestMeta as currentRequestMeta2 } from "@getstrata/core/http/requestMetaContext";
@@ -237,6 +264,42 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
237
264
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
238
265
  var CORE_AUTH_TOKEN = "core.auth";
239
266
  var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
267
+ var CORE_ABILITY_CHECKER_TOKEN = "core.abilityChecker";
268
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
269
+ function isAbilityChecker(value) {
270
+ if (!value || typeof value !== "object") {
271
+ return false;
272
+ }
273
+ const candidate = value;
274
+ return typeof candidate.tokenCan === "function" && typeof candidate.requireAbility === "function";
275
+ }
276
+ function isAuthUserDirectory(value) {
277
+ if (!value || typeof value !== "object") {
278
+ return false;
279
+ }
280
+ return typeof value.findByIdOrThrow === "function";
281
+ }
282
+ function resolveAbilityChecker(container) {
283
+ if (container.has(CORE_ABILITY_CHECKER_TOKEN)) {
284
+ return container.resolve(CORE_ABILITY_CHECKER_TOKEN);
285
+ }
286
+ return container.resolve(CORE_TOKEN_SERVICE_TOKEN);
287
+ }
288
+ function resolveAuthUserDirectory(container) {
289
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
290
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
291
+ if (isAuthUserDirectory(directory)) {
292
+ return directory;
293
+ }
294
+ }
295
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
296
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
297
+ if (isAuthUserDirectory(tokenService)) {
298
+ return tokenService;
299
+ }
300
+ }
301
+ return null;
302
+ }
240
303
 
241
304
  // ../../src/core/http/csrfToken.ts
242
305
  import { timingSafeEqual } from "crypto";
@@ -298,6 +361,9 @@ function currentRequestMeta() {
298
361
  // ../../src/core/http/csrfToken.ts
299
362
  var CSRF_COOKIE = "workhub_csrf";
300
363
  var CSRF_TTL_MS = 60 * 60 * 1000;
364
+ function csrfCookieName() {
365
+ return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
366
+ }
301
367
  function resolveCsrfSecret() {
302
368
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
303
369
  }
@@ -317,11 +383,11 @@ function createCsrfTokenCookie() {
317
383
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
318
384
  return {
319
385
  token,
320
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
386
+ cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
321
387
  };
322
388
  }
323
389
  function resolveCsrfToken(request) {
324
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
390
+ const cookieValue = readRequestCookie(request, csrfCookieName());
325
391
  if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
326
392
  return { token: cookieValue };
327
393
  }
@@ -357,7 +423,7 @@ function verifyCsrfToken(request, submittedToken) {
357
423
  if (!submittedToken) {
358
424
  return false;
359
425
  }
360
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
426
+ const cookieValue = readRequestCookie(request, csrfCookieName());
361
427
  if (!cookieValue) {
362
428
  return false;
363
429
  }
@@ -474,48 +540,65 @@ function withFlashClear(response) {
474
540
  }
475
541
 
476
542
  // ../../src/core/view/webLayoutData.ts
477
- async function resolveWebLayoutData(container, request) {
478
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
479
- const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
543
+ var configuredLayoutOptions = {};
544
+ function configureWebLayoutData(options) {
545
+ configuredLayoutOptions = { ...options };
546
+ }
547
+ function layoutUserKey(options) {
548
+ return options.userKey ?? "authUser";
549
+ }
550
+ async function defaultLoadLayoutUser(container, _request) {
480
551
  const authUser = currentAuthUser();
481
552
  if (!authUser) {
482
- return { authUser: null, csrfToken, flash };
553
+ return null;
483
554
  }
484
555
  const userId = Number(authUser.id);
485
556
  if (!Number.isInteger(userId) || userId <= 0) {
486
- return { authUser: null, csrfToken, flash };
557
+ return null;
487
558
  }
488
- if (!container.has(CORE_TOKEN_SERVICE_TOKEN)) {
559
+ const directory = resolveAuthUserDirectory(container);
560
+ if (!directory) {
489
561
  return {
490
- authUser: {
491
- id: userId,
492
- email: "",
493
- role: authUser.role ?? "member"
494
- },
495
- csrfToken,
496
- flash
562
+ id: userId,
563
+ email: "",
564
+ role: authUser.role ?? "member"
497
565
  };
498
566
  }
499
- const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
500
567
  try {
501
- const user = await tokenService.findByIdOrThrow(userId);
568
+ const user = await directory.findByIdOrThrow(userId);
502
569
  return {
503
- authUser: {
504
- id: userId,
505
- email: user.email ?? "",
506
- role: authUser.role ?? user.role ?? "member"
507
- },
508
- csrfToken,
509
- flash
570
+ id: userId,
571
+ email: user.email ?? "",
572
+ role: authUser.role ?? user.role ?? "member"
510
573
  };
511
574
  } catch {
512
- return { authUser: null, csrfToken, flash };
575
+ return null;
513
576
  }
514
577
  }
578
+ async function resolveWebLayoutData(container, request, options = {}) {
579
+ const resolved = { ...configuredLayoutOptions, ...options };
580
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
581
+ const flash = request ? currentRequestMeta2().flash ?? pullFlash(request) : null;
582
+ const userKey = layoutUserKey(resolved);
583
+ const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
584
+ const user = await loadUser(container, request);
585
+ const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
586
+ return {
587
+ [userKey]: user,
588
+ csrfToken,
589
+ flash,
590
+ ...extra
591
+ };
592
+ }
515
593
  export {
516
594
  DEFAULT_VIEWS_DIRECTORY,
517
595
  EtaViewEngine,
596
+ configureWebLayoutData,
518
597
  htmlResponse,
519
598
  isHtmxRequest,
520
- resolveWebLayoutData
599
+ notFoundHtmlResponse,
600
+ redirectResponse,
601
+ resolveWebLayoutData,
602
+ textResponse,
603
+ xmlResponse
521
604
  };
@@ -118,6 +118,6 @@ export { currentTraceId, runWithTraceContext } from "../core/tracing/traceContex
118
118
  export { createTracingMiddleware } from "../core/tracing/tracingMiddleware.ts";
119
119
  export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
120
120
  export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
121
- export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, resolveWebLayoutData, } from "../core/view/index.ts";
121
+ export { configureWebLayoutData, DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, notFoundHtmlResponse, redirectResponse, resolveWebLayoutData, textResponse, xmlResponse, } from "../core/view/index.ts";
122
122
  export type { ViewEngine } from "../core/view/viewEngine.ts";
123
- export type { WebLayoutAuthUser, WebLayoutData } from "../core/view/webLayoutData.ts";
123
+ export type { WebLayoutAuthUser, WebLayoutData, WebLayoutDataOptions, WebLayoutUserKey, } from "../core/view/webLayoutData.ts";
package/dist/index.js CHANGED
@@ -172,6 +172,28 @@ var CORE_EVENT_BUS_TOKEN = "core.eventBus";
172
172
  var CORE_POLICY_GATE_TOKEN = "core.policyGate";
173
173
  var CORE_AUTH_TOKEN = "core.auth";
174
174
  var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
175
+ var CORE_AUTH_USER_DIRECTORY_TOKEN = "core.authUserDirectory";
176
+ function isAuthUserDirectory(value) {
177
+ if (!value || typeof value !== "object") {
178
+ return false;
179
+ }
180
+ return typeof value.findByIdOrThrow === "function";
181
+ }
182
+ function resolveAuthUserDirectory(container) {
183
+ if (container.has(CORE_AUTH_USER_DIRECTORY_TOKEN)) {
184
+ const directory = container.resolve(CORE_AUTH_USER_DIRECTORY_TOKEN);
185
+ if (isAuthUserDirectory(directory)) {
186
+ return directory;
187
+ }
188
+ }
189
+ if (container.has(CORE_TOKEN_SERVICE_TOKEN)) {
190
+ const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
191
+ if (isAuthUserDirectory(tokenService)) {
192
+ return tokenService;
193
+ }
194
+ }
195
+ return null;
196
+ }
175
197
 
176
198
  // ../../src/core/auth/abilityCatalog.ts
177
199
  var MEMBER_ABILITIES = [
@@ -276,10 +298,10 @@ class DatabaseTokenGuard {
276
298
  if (!token) {
277
299
  return null;
278
300
  }
279
- if (!this.container.has(CORE_TOKEN_SERVICE_TOKEN)) {
301
+ const tokenService = resolveAuthUserDirectory(this.container);
302
+ if (!tokenService || typeof tokenService.resolveUserFromToken !== "function") {
280
303
  return null;
281
304
  }
282
- const tokenService = this.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
283
305
  return await tokenService.resolveUserFromToken(token);
284
306
  }
285
307
  }
@@ -4366,6 +4388,9 @@ function currentRequestMeta() {
4366
4388
  // ../../src/core/http/csrfToken.ts
4367
4389
  var CSRF_COOKIE = "workhub_csrf";
4368
4390
  var CSRF_TTL_MS = 60 * 60 * 1000;
4391
+ function csrfCookieName() {
4392
+ return process.env.CSRF_COOKIE_NAME?.trim() || CSRF_COOKIE;
4393
+ }
4369
4394
  function resolveCsrfSecret() {
4370
4395
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
4371
4396
  }
@@ -4385,11 +4410,11 @@ function createCsrfTokenCookie() {
4385
4410
  const secure = process.env.APP_ENV === "production" ? "; Secure" : "";
4386
4411
  return {
4387
4412
  token,
4388
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
4413
+ cookie: `${csrfCookieName()}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}${secure}`
4389
4414
  };
4390
4415
  }
4391
4416
  function resolveCsrfToken(request) {
4392
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
4417
+ const cookieValue = readRequestCookie(request, csrfCookieName());
4393
4418
  if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
4394
4419
  return { token: cookieValue };
4395
4420
  }
@@ -4425,7 +4450,7 @@ function verifyCsrfToken(request, submittedToken) {
4425
4450
  if (!submittedToken) {
4426
4451
  return false;
4427
4452
  }
4428
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
4453
+ const cookieValue = readRequestCookie(request, csrfCookieName());
4429
4454
  if (!cookieValue) {
4430
4455
  return false;
4431
4456
  }
@@ -5094,45 +5119,84 @@ function htmlResponse(html, init = {}) {
5094
5119
  function isHtmxRequest(request) {
5095
5120
  return request.headers.get("HX-Request") === "true";
5096
5121
  }
5122
+ function redirectResponse(location, status = 302) {
5123
+ return new Response(null, {
5124
+ status,
5125
+ headers: {
5126
+ Location: location
5127
+ }
5128
+ });
5129
+ }
5130
+ function notFoundHtmlResponse(body = "Not Found") {
5131
+ return htmlResponse(body, { status: 404 });
5132
+ }
5133
+ function textResponse(body, init = {}) {
5134
+ return new Response(body, {
5135
+ status: init.status ?? 200,
5136
+ headers: {
5137
+ "Content-Type": "text/plain; charset=utf-8"
5138
+ }
5139
+ });
5140
+ }
5141
+ function xmlResponse(body, init = {}) {
5142
+ return new Response(body, {
5143
+ status: init.status ?? 200,
5144
+ headers: {
5145
+ "Content-Type": "application/xml; charset=utf-8"
5146
+ }
5147
+ });
5148
+ }
5097
5149
  // ../../src/core/view/webLayoutData.ts
5098
- async function resolveWebLayoutData(container, request) {
5099
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
5100
- const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
5150
+ var configuredLayoutOptions = {};
5151
+ function configureWebLayoutData(options) {
5152
+ configuredLayoutOptions = { ...options };
5153
+ }
5154
+ function layoutUserKey(options) {
5155
+ return options.userKey ?? "authUser";
5156
+ }
5157
+ async function defaultLoadLayoutUser(container, _request) {
5101
5158
  const authUser = currentAuthUser();
5102
5159
  if (!authUser) {
5103
- return { authUser: null, csrfToken, flash };
5160
+ return null;
5104
5161
  }
5105
5162
  const userId = Number(authUser.id);
5106
5163
  if (!Number.isInteger(userId) || userId <= 0) {
5107
- return { authUser: null, csrfToken, flash };
5164
+ return null;
5108
5165
  }
5109
- if (!container.has(CORE_TOKEN_SERVICE_TOKEN)) {
5166
+ const directory = resolveAuthUserDirectory(container);
5167
+ if (!directory) {
5110
5168
  return {
5111
- authUser: {
5112
- id: userId,
5113
- email: "",
5114
- role: authUser.role ?? "member"
5115
- },
5116
- csrfToken,
5117
- flash
5169
+ id: userId,
5170
+ email: "",
5171
+ role: authUser.role ?? "member"
5118
5172
  };
5119
5173
  }
5120
- const tokenService = container.resolve(CORE_TOKEN_SERVICE_TOKEN);
5121
5174
  try {
5122
- const user = await tokenService.findByIdOrThrow(userId);
5175
+ const user = await directory.findByIdOrThrow(userId);
5123
5176
  return {
5124
- authUser: {
5125
- id: userId,
5126
- email: user.email ?? "",
5127
- role: authUser.role ?? user.role ?? "member"
5128
- },
5129
- csrfToken,
5130
- flash
5177
+ id: userId,
5178
+ email: user.email ?? "",
5179
+ role: authUser.role ?? user.role ?? "member"
5131
5180
  };
5132
5181
  } catch {
5133
- return { authUser: null, csrfToken, flash };
5182
+ return null;
5134
5183
  }
5135
5184
  }
5185
+ async function resolveWebLayoutData(container, request, options = {}) {
5186
+ const resolved = { ...configuredLayoutOptions, ...options };
5187
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
5188
+ const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
5189
+ const userKey = layoutUserKey(resolved);
5190
+ const loadUser = resolved.loadUser ?? defaultLoadLayoutUser;
5191
+ const user = await loadUser(container, request);
5192
+ const extra = typeof resolved.extra === "function" ? await resolved.extra(user) : resolved.extra ?? {};
5193
+ return {
5194
+ [userKey]: user,
5195
+ csrfToken,
5196
+ flash,
5197
+ ...extra
5198
+ };
5199
+ }
5136
5200
  // ../../src/core/http/contentNegotiation.ts
5137
5201
  function requestPrefersJson(request) {
5138
5202
  if (!request) {
@@ -6683,6 +6747,7 @@ export {
6683
6747
  conditionalJsonResponse,
6684
6748
  config,
6685
6749
  configureMembershipLookup,
6750
+ configureWebLayoutData,
6686
6751
  createAuthMiddleware,
6687
6752
  createAuthorizeMiddleware,
6688
6753
  createBodySizeLimitMiddleware,
@@ -6780,6 +6845,7 @@ export {
6780
6845
  morphTo,
6781
6846
  noContentResponse,
6782
6847
  normalizeMetricPath,
6848
+ notFoundHtmlResponse,
6783
6849
  paginatedResponse,
6784
6850
  parseMultipartUpload,
6785
6851
  parsePaginationQuery,
@@ -6794,6 +6860,7 @@ export {
6794
6860
  readSubmittedCsrfToken,
6795
6861
  readSubmittedCsrfTokenFromBody,
6796
6862
  readTenancyDriver,
6863
+ redirectResponse,
6797
6864
  registerDefaultDatabasePool,
6798
6865
  registerModelRepository,
6799
6866
  registerShutdownHandler,
@@ -6841,6 +6908,7 @@ export {
6841
6908
  storageFacade as storage,
6842
6909
  stringRule,
6843
6910
  stripMarkdown,
6911
+ textResponse,
6844
6912
  toPaginatedResourceCollection,
6845
6913
  toResourceCollection,
6846
6914
  trustForwardedFor,
@@ -6850,5 +6918,6 @@ export {
6850
6918
  withMiddleware,
6851
6919
  withMigrationLock,
6852
6920
  wrapMarkdownMailLayout,
6853
- wrapRouteHandler
6921
+ wrapRouteHandler,
6922
+ xmlResponse
6854
6923
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.57",
3
+ "version": "0.5.59",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -306,7 +306,7 @@
306
306
  "default": "./dist/entries/events.js"
307
307
  },
308
308
  "./facades": {
309
- "types": "./dist/core/facades.d.ts",
309
+ "types": "./dist/core/facades/index.d.ts",
310
310
  "import": "./dist/entries/facades.js",
311
311
  "default": "./dist/entries/facades.js"
312
312
  },
@@ -765,7 +765,8 @@
765
765
  "types": "./dist/framework/public-api.d.ts",
766
766
  "files": [
767
767
  "dist",
768
- "README.md"
768
+ "README.md",
769
+ "CHANGELOG.md"
769
770
  ],
770
771
  "scripts": {
771
772
  "build": "bun run build:bundle && bun run build:shims && bun run build:subpaths && bun run build:types",