@getstrata/bootstrap 0.2.68 → 0.4.3

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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # @getstrata/bootstrap changelog
2
2
 
3
+ ## 0.4.3
4
+
5
+ - Cookie session `mapUser` / row mapping can pass `email_verified_at` through as `emailVerifiedAt` so HTML email verification works.
6
+ - Session insert binds `expires_at` as an ISO string so SQLite cookie login does not fail parameter binding.
7
+
8
+ ## 0.4.2
9
+
10
+ - `HttpKernel.wrapApi` uses `withJsonErrorHandling`. Hybrid views do not turn API errors into HTML.
11
+
12
+ ## 0.4.1
13
+
14
+ - `createSpaRoutes` reads `SPA_PREFIX`, registers `prefix`, `prefix/`, and `prefix/*`, and accepts `distDirectory` plus `wrap`. Hybrid still does not redirect `/`.
15
+ - `appEnvSchema` defaults `SPA_PREFIX` from core `DEFAULT_SPA_PREFIX`.
16
+ - Peer `@getstrata/core` `^0.7.1`.
17
+
18
+ ## 0.4.0
19
+
20
+ - Peer `@getstrata/core` `^0.7.0`.
21
+ - Production `SESSION_SECRET` is required whenever views are on (`FRONTEND_MODE=server-htmx` or `hybrid`).
22
+ - Hybrid SPA routes stay under `/app/*`. They do not redirect `/`, so staff HTML keeps the home path.
23
+ - `appEnvSchema` validates `FRONTEND_MODE` with the core pattern, including `hybrid`.
24
+ - `HttpKernel.wrapApi` maps thrown HTTP errors to JSON the same way `wrapWeb` maps them to HTML.
25
+
26
+ ## 0.3.0
27
+
28
+ - Peer `@getstrata/core` `^0.6.0`.
29
+ - Cookie session SQL uses the current SQL dialect for placeholders, `NOW()`, and `NULLS LAST`.
30
+ - Kernel comments describe guest, verified-email, and password-confirm gates without referring to another framework.
31
+
3
32
  ## 0.2.68
4
33
 
5
34
  - `configureModulesDirectory()` clears the discovered-module cache when the directory changes, so OpenAPI/`createApp` can load HiroApp after core tests pointed at empty fixtures.
@@ -13,17 +42,17 @@
13
42
 
14
43
  ## 0.2.66
15
44
 
16
- - `assertProductionSecrets()` is feature-gated for every app. API tokens no longer imply WorkHub’s encryption/CORS/OAuth/public-read checklist. Published test token strings are still denied. WorkHub’s extra production profile lives in the app (`src/config/productionSecrets.ts`).
17
- - **Breaking:** `HttpKernel.wrapWebGuest()` defaults `home` to `/` instead of `/organizations`. Apps with an org home should pass that path (WorkHub already does).
45
+ - `assertProductionSecrets()` is feature-gated for every app. API tokens no longer imply the previous in-repo app's encryption/CORS/OAuth/public-read checklist. Published test token strings are still denied. the previous in-repo app's extra production profile lives in the app (`src/config/productionSecrets.ts`).
46
+ - **Breaking:** `HttpKernel.wrapWebGuest()` defaults `home` to `/` instead of `/organizations`. Apps with an org home should pass that path (the previous in-repo app already does).
18
47
  - `createWebRoutes()` no longer seeds a `/` → `/organizations` redirect. The in-repo app owns `/` via its organization module.
19
48
 
20
49
  ## 0.2.65
21
50
 
22
- - App listener discovery reads `src/listeners` from `process.cwd()` so a built `@getstrata/bootstrap` bundle still finds WorkHub registrars (`import.meta.dir` after `build:bootstrap` is the package dist). Listener boot calls each registrar every time (registrars are idempotent per `eventBus`).
51
+ - App listener discovery reads `src/listeners` from `process.cwd()` so a built `@getstrata/bootstrap` bundle still finds the previous in-repo app registrars (`import.meta.dir` after `build:bootstrap` is the package dist). Listener boot calls each registrar every time (registrars are idempotent per `eventBus`).
23
52
 
24
53
  ## 0.2.64
25
54
 
26
- - `registerDefaultJobs()` no longer registers WorkHub `webhook.dispatch`. Apps that dispatch model webhooks should call `registerWebhookJobs()` (WorkHub’s webhook provider and `queue:work` do).
55
+ - `registerDefaultJobs()` no longer registers the previous in-repo app `webhook.dispatch`. Apps that dispatch model webhooks should call `registerWebhookJobs()` (the previous in-repo app's webhook provider and `queue:work` do).
27
56
 
28
57
  ## 0.2.63
29
58
 
@@ -31,7 +60,7 @@
31
60
 
32
61
  ## 0.2.62
33
62
 
34
- - `HttpKernel.wrapWebPasswordConfirm()` is Laravel `password.confirm` for HTML routes. Peer `@getstrata/core` `^0.5.75`.
63
+ - `HttpKernel.wrapWebPasswordConfirm()` is the previous PHP framework `password.confirm` for HTML routes. Peer `@getstrata/core` `^0.5.75`.
35
64
 
36
65
  ## 0.2.61
37
66
 
@@ -43,7 +72,7 @@
43
72
 
44
73
  ## 0.2.59
45
74
 
46
- - `HttpKernel.wrapWebGuest()` is Laravel `guest` / `RedirectIfAuthenticated`. Signed-in HTML users redirect to `/organizations` (override the home path). Peer `@getstrata/core` `^0.5.72`.
75
+ - `HttpKernel.wrapWebGuest()` is the previous PHP framework `guest` / `RedirectIfAuthenticated`. Signed-in HTML users redirect to `/organizations` (override the home path). Peer `@getstrata/core` `^0.5.72`.
47
76
 
48
77
  ## 0.2.58
49
78
 
@@ -59,7 +88,7 @@
59
88
 
60
89
  ## 0.2.55
61
90
 
62
- - HttpKernel and route builders read `isViewsEnabled` / `isSpaEnabled` from `@getstrata/core/runtime/frontendMode` instead of WorkHub `src/config/frontend`.
91
+ - HttpKernel and route builders read `isViewsEnabled` / `isSpaEnabled` from `@getstrata/core/runtime/frontendMode` instead of the previous in-repo app `src/config/frontend`.
63
92
 
64
93
  ## 0.2.54
65
94
 
@@ -83,7 +112,7 @@
83
112
 
84
113
  ## 0.2.50
85
114
 
86
- - `createCookieSessionAuthManager` / `CookieSessionGuard` turn `CookieSessionStore` into an `AuthGuard` / `AuthManager` for `CORE_AUTH_TOKEN`. Supply a `mapUser` mapper; WorkHub abilities are not baked in. Omit `sql` to read the client from `bindDatabaseConnection()` on every call.
87
- - `checkDatabase()` / `createHealthRoutes()` ping the bound database client, not WorkHub’s private connection holder. `/health` stays `{ status: "ok" }` unless `{ pingOnHealth: true }`. Extra JSON fields are optional.
88
- - `assertProductionSecrets()` is feature-gated. A production HTMX app with `SESSION_SECRET` (32+), `AUTH_DEV_HEADERS=false`, and feature flags off does not need WorkHub API tokens. WorkHub’s current production env (rotated tokens, CORS, pepper, expiry) still passes.
115
+ - `createCookieSessionAuthManager` / `CookieSessionGuard` turn `CookieSessionStore` into an `AuthGuard` / `AuthManager` for `CORE_AUTH_TOKEN`. Supply a `mapUser` mapper; the previous in-repo app abilities are not baked in. Omit `sql` to read the client from `bindDatabaseConnection()` on every call.
116
+ - `checkDatabase()` / `createHealthRoutes()` ping the bound database client, not the previous in-repo app's private connection holder. `/health` stays `{ status: "ok" }` unless `{ pingOnHealth: true }`. Extra JSON fields are optional.
117
+ - `assertProductionSecrets()` is feature-gated. A production HTMX app with `SESSION_SECRET` (32+), `AUTH_DEV_HEADERS=false`, and feature flags off does not need the previous in-repo app API tokens. the previous in-repo app's current production env (rotated tokens, CORS, pepper, expiry) still passes.
89
118
  - Re-exports `CORE_ABILITY_CHECKER_TOKEN` and `CORE_AUTH_USER_DIRECTORY_TOKEN`.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @getstrata/bootstrap
2
2
 
3
- Application bootstrap for Strata sibling apps: HttpKernel, service container, and web utilities.
3
+ Application bootstrap for Strata apps: HttpKernel, service container, and web utilities.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,7 +8,7 @@ Application bootstrap for Strata sibling apps: HttpKernel, service container, an
8
8
  bun add @getstrata/bootstrap @getstrata/core
9
9
  ```
10
10
 
11
- ## Web utilities (getstrata-style apps)
11
+ ## Web utilities
12
12
 
13
13
  ```typescript
14
14
  import {
@@ -20,22 +20,22 @@ import {
20
20
  } from "@getstrata/bootstrap";
21
21
  ```
22
22
 
23
- `CookieSessionStore` reads optional `is_admin` from the user row for global admin routing via `wrapWebGlobalAdmin`.
23
+ `CookieSessionStore` can read `is_admin` from the user row for `wrapWebGlobalAdmin`. Pass `loadSessionUser` when your `users` table does not match the default `SELECT u.*`.
24
24
 
25
- ## HttpKernel (API / module apps)
25
+ ## HttpKernel
26
26
 
27
27
  ```typescript
28
28
  import { createHttpKernel, createAppContext, coreProviders } from "@getstrata/bootstrap";
29
29
  ```
30
30
 
31
- `createAppContext()` does not call `assertProductionSecrets`. That helper is feature-gated for every app (tokens, CORS, OAuth, encryption only when those features are on). WorkHub also runs its own production extras from `App.serve()` / `queue:work`. Sibling HTMX apps can call the published helper without API tokens when feature flags are off see [SIBLING-HTMX.md](../../docs/SIBLING-HTMX.md).
31
+ `createAppContext()` does not call `assertProductionSecrets`. That helper is feature-gated (tokens, CORS, OAuth, encryption only when those features are on). HTML apps can call it without API tokens when those flags are off. See [docs/BUILDING-APPS.md](../../docs/BUILDING-APPS.md) and [docs/PRODUCTION.md](../../docs/PRODUCTION.md).
32
32
 
33
- Sibling HTMX apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstrap/web/session` instead of HMAC `SessionGuard`. Use `signIn` / `signOut` (or the redirect helpers) instead of calling `CookieSessionStore` from controllers. Pass `mapUser` to map roles in the app. Pass `loadSessionUser` when the default `learn_subscriber` / `is_admin` SELECT does not match your schema.
33
+ HTML apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstrap/web/session` instead of HMAC `SessionGuard`. Use `signIn` / `signOut` (or the redirect helpers). Pass `mapUser` to map roles in the app.
34
34
 
35
- `wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 in `FRONTEND_MODE=server-htmx`. `wrapWebGuest` is Laravel `guest` / `RedirectIfAuthenticated` (signed-in users go to `/` by default; pass a string or `(user) => path` to override). `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429.
35
+ `wrapWeb` applies the web group (CSRF + flash) and `withErrorHandling`, so CSRF `ForbiddenError` becomes an HTML 403 when views are on (`FRONTEND_MODE=server-htmx` or `hybrid`). `wrapWebGuest` sends signed-in users to `/` by default (pass a string or `(user) => path` to override). `wrapWebLogin` / `wrapWebRegister` include that web group plus throttle. Do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429.
36
36
 
37
- `registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Apps that dispatch model webhooks should call `registerWebhookJobs()` themselves.
37
+ `createSpaRoutes` / `mergeSpaRoutes` serve `SPA_PREFIX` (default `/app`) as `prefix`, `prefix/`, and `prefix/*`. Hybrid does not redirect `/`. Pass `distDirectory` and `wrap` when the app is not the process cwd. Apps should not copy a second static-file server.
38
38
 
39
- These subpaths assemble the in-repo dogfood app and are not a generic starter API: `@getstrata/bootstrap/createRoutes` (includes SCIM), `@getstrata/bootstrap/schedule`, and `@getstrata/bootstrap/createWebRoutes` (discovers app `webRoutes`; `/` is owned by the app). Sibling apps should use `buildWebModuleRoutes` / `buildModuleRoutes`.
39
+ `registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Apps that dispatch model webhooks should call `registerWebhookJobs()` themselves.
40
40
 
41
- See the [strata](https://github.com/EyK-26/strata) monorepo reference app for full module patterns.
41
+ These subpaths assemble leftover fixture HTTP for framework tests and are not a generic starter API: `@getstrata/bootstrap/createRoutes`, `@getstrata/bootstrap/schedule`, `@getstrata/bootstrap/createWebRoutes`. New apps should use `buildWebModuleRoutes` / `buildModuleRoutes`. Generated examples use `createApp()` in `apps/hiroapp`.
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>WorkHub API</title>
6
+ <title>Strata</title>
7
7
  <style>
8
8
  :root {
9
9
  color-scheme: dark;
@@ -78,7 +78,7 @@
78
78
  <script type="module" crossorigin src="../../index-sfreg6q3.js"></script></head>
79
79
 
80
80
  <body>
81
- <h1>WorkHub API</h1>
81
+ <h1>Strata</h1>
82
82
  <p class="hint">
83
83
  Framework reference domain for organizations, projects, tasks, and
84
84
  comments.
@@ -1,5 +1,12 @@
1
+ import type { RouteHandler } from "@getstrata/core/http/middleware";
1
2
  import type { AppDependencies, AppRouteMap } from "./contracts";
2
3
  declare const SPA_DIST_DIRECTORY: string;
3
- declare function createSpaRoutes(_dependencies: AppDependencies): AppRouteMap;
4
- declare function mergeSpaRoutes(dependencies: AppDependencies, routes: AppRouteMap): AppRouteMap;
4
+ type SpaRouteOptions = {
5
+ prefix?: string;
6
+ distDirectory?: string;
7
+ wrap?: (handler: RouteHandler) => RouteHandler;
8
+ };
9
+ declare function createSpaRoutes(_dependencies: AppDependencies, options?: SpaRouteOptions): AppRouteMap;
10
+ declare function mergeSpaRoutes(dependencies: AppDependencies, routes: AppRouteMap, options?: SpaRouteOptions): AppRouteMap;
11
+ export type { SpaRouteOptions };
5
12
  export { createSpaRoutes, mergeSpaRoutes, SPA_DIST_DIRECTORY };
@@ -1,4 +1,4 @@
1
- type DogfoodApp = "workhub" | "hiroapp";
1
+ type DogfoodApp = "hiroapp";
2
2
  declare function readDogfoodApp(): DogfoodApp;
3
3
  declare function isHiroappDogfood(): boolean;
4
4
  declare function hiroappSourcePath(...segments: string[]): string;
@@ -12,19 +12,19 @@ declare class HttpKernel {
12
12
  wrap(groups: MiddlewareGroupName | MiddlewareGroupName[], handler: RouteHandler): RouteHandler;
13
13
  wrapApi(handler: RouteHandler): RouteHandler;
14
14
  wrapWeb(handler: RouteHandler): RouteHandler;
15
- /** Laravel `guest` / `RedirectIfAuthenticated` signed-in users go to `home`. */
15
+ /** Signed-in HTML users redirect to `home` instead of seeing guest pages. */
16
16
  wrapWebGuest(handler: RouteHandler, home?: WebGuestHome): RouteHandler;
17
17
  wrapWebPublicRead(handler: RouteHandler): RouteHandler;
18
18
  wrapWebAuthenticated(handler: RouteHandler): RouteHandler;
19
- /** Signed-in HTML without Laravel `verified` (logout, verification notice). */
19
+ /** Signed-in HTML without the email-verified gate (logout, verification notice). */
20
20
  wrapWebAuthenticatedAllowUnverified(handler: RouteHandler): RouteHandler;
21
21
  wrapWebVerified(handler: RouteHandler): RouteHandler;
22
- /** Laravel `password.confirm` — requires a fresh signed confirmation cookie. */
22
+ /** Requires a fresh signed password-confirmation cookie. */
23
23
  wrapWebPasswordConfirm(handler: RouteHandler): RouteHandler;
24
24
  wrapWebAbility(ability: string, handler: RouteHandler): RouteHandler;
25
25
  wrapWebGlobalAdmin(handler: RouteHandler): RouteHandler;
26
26
  wrapAuthenticated(handler: RouteHandler): RouteHandler;
27
- /** Laravel `verified` for JSON/API routes. No-op when `FEATURE_EMAIL_VERIFICATION` is off. */
27
+ /** JSON routes that require a verified email. No-op when `FEATURE_EMAIL_VERIFICATION` is off. */
28
28
  wrapVerified(handler: RouteHandler): RouteHandler;
29
29
  wrapPublicRead(handler: RouteHandler): RouteHandler;
30
30
  wrapGlobalAdmin(handler: RouteHandler): RouteHandler;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @getstrata/bootstrap application shell for Strata sibling apps.
2
+ * @getstrata/bootstrap: application shell for Strata apps.
3
3
  */
4
4
  export type { ScheduledTask } from "@getstrata/core/scheduler/schedule";
5
5
  export { appSchedule, runDueScheduledTasks, Schedule } from "@getstrata/core/scheduler/schedule";
@@ -0,0 +1,5 @@
1
+ type SchemaTarget = "app" | "fixture";
2
+ declare function readSchemaTarget(): SchemaTarget;
3
+ declare function isFixtureSchema(): boolean;
4
+ export type { SchemaTarget };
5
+ export { isFixtureSchema, readSchemaTarget };
@@ -1 +1 @@
1
- import "./preloadModules.ts";
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Web-focused bootstrap utilities for sibling apps (getstrata, marketing sites).
2
+ * Web-focused bootstrap utilities for HTML and cookie-session apps.
3
3
  */
4
4
  export { createCsrfProtection, type ParsedForm, parseFormBody } from "./forms.ts";
5
5
  export { createRouteKernel, routeParams, toRouteRequest, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./routing.ts";
@@ -10,7 +10,7 @@ export declare function routeParams(request: Request): Record<string, string>;
10
10
  */
11
11
  export declare function toRouteRequest<TParams extends Record<string, string>>(request: Request): RouteRequest<TParams>;
12
12
  /**
13
- * Laravel-style secured route-model binding for string keys (slugs, UUIDs).
13
+ * Secured route-model binding for string keys (slugs, UUIDs).
14
14
  * Public HTML show pages should look up the model in the controller instead.
15
15
  * If this wrapper is still used on HTML, a missing model becomes a styled 404
16
16
  * and GET ETags are skipped for HTML / composite objects unless `etag: true`.
@@ -24,5 +24,5 @@ export declare function wrapSecuredRouteModelByKey<TParams extends Record<string
24
24
  export declare function wrapWebLogin(kernel: HttpKernel, handler: RouteHandler, onThrottled: (request: Request) => Response | Promise<Response>): RouteHandler;
25
25
  /** Web group (CSRF + flash + HTML errors) plus register throttle. Do not wrap with wrapWeb again. */
26
26
  export declare function wrapWebRegister(kernel: HttpKernel, handler: RouteHandler, onThrottled: (request: Request) => Response | Promise<Response>): RouteHandler;
27
- /** Convenience alias: HttpKernel is the Laravel-style router middleware wrapper. */
27
+ /** Convenience alias for `createHttpKernel`. */
28
28
  export declare function createRouteKernel(dependencies: AppDependencies): HttpKernel;
@@ -6,6 +6,7 @@ export interface SessionUser {
6
6
  email: string;
7
7
  learn_subscriber?: boolean;
8
8
  is_admin?: boolean;
9
+ email_verified_at?: Date | string | null;
9
10
  }
10
11
  type SqlClient = {
11
12
  unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
@@ -28,7 +28,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
28
28
  import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
29
29
  import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
30
30
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
31
- import { withErrorHandling } from "@getstrata/core/http/response";
31
+ import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
32
32
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
33
33
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
34
34
  import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
@@ -176,7 +176,7 @@ class HttpKernel {
176
176
  return withMiddleware(...middleware)(handler);
177
177
  }
178
178
  wrapApi(handler) {
179
- return this.wrap(["api", "authenticated"], handler);
179
+ return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
180
180
  }
181
181
  wrapWeb(handler) {
182
182
  return withErrorHandling(this.wrap("web", handler));
@@ -31,7 +31,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
31
31
  import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
32
32
  import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
33
33
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
34
- import { withErrorHandling } from "@getstrata/core/http/response";
34
+ import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
35
35
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
36
36
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
37
37
  import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
@@ -179,7 +179,7 @@ class HttpKernel {
179
179
  return withMiddleware(...middleware)(handler);
180
180
  }
181
181
  wrapApi(handler) {
182
- return this.wrap(["api", "authenticated"], handler);
182
+ return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
183
183
  }
184
184
  wrapWeb(handler) {
185
185
  return withErrorHandling(this.wrap("web", handler));
@@ -96,17 +96,19 @@ function resetDiscoverModulesForTests() {
96
96
  state.modulesReady = undefined;
97
97
  }
98
98
  // ../../src/bootstrap/providers/auth.ts
99
+ import { BasicAuthGuard } from "@getstrata/core/auth/basicAuthGuard";
99
100
  import {
100
101
  AuthManager,
101
102
  CompositeGuard,
102
103
  DatabaseTokenGuard,
103
104
  GuestGuard
104
105
  } from "@getstrata/core/auth/guard";
106
+ import { JwtGuard } from "@getstrata/core/auth/jwtGuard";
105
107
  import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
106
108
 
107
109
  // ../../src/config/auth.ts
108
110
  var authConfig = {
109
- allowDevHeaders: (process.env.AUTH_DEV_HEADERS ?? "true") !== "false",
111
+ allowDevHeaders: process.env.AUTH_DEV_HEADERS === "true",
110
112
  tokenDefaultAbilities: ["*"]
111
113
  };
112
114
 
@@ -141,11 +143,22 @@ var authProvider = {
141
143
  name: "core.auth",
142
144
  register({ container, config }) {
143
145
  config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
144
- const guards = [new DatabaseTokenGuard(container), new SessionGuard(container)];
146
+ const apiGuard = new DatabaseTokenGuard(container);
147
+ const sessionGuard = new SessionGuard(container);
148
+ const jwtGuard = new JwtGuard;
149
+ const basicGuard = new BasicAuthGuard(container);
150
+ const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
145
151
  if (authConfig.allowDevHeaders) {
146
152
  guards.push(new GuestGuard);
147
153
  }
148
- container.set(CORE_AUTH_TOKEN, new AuthManager(new CompositeGuard(guards)));
154
+ const auth = new AuthManager(new CompositeGuard(guards));
155
+ auth.registerGuard("api", apiGuard);
156
+ auth.registerGuard("access_token", apiGuard);
157
+ auth.registerGuard("jwt", jwtGuard);
158
+ auth.registerGuard("basic", basicGuard);
159
+ auth.registerGuard("web", sessionGuard);
160
+ auth.registerGuard("session", sessionGuard);
161
+ container.set(CORE_AUTH_TOKEN, auth);
149
162
  }
150
163
  };
151
164
  var auth_default = authProvider;
@@ -206,8 +219,12 @@ var queueConfig = {
206
219
  };
207
220
  // ../../src/bootstrap/env.ts
208
221
  import { defineEnvSchema } from "@getstrata/core/config/envSchema";
222
+ import { DEFAULT_SPA_PREFIX, FRONTEND_MODE_PATTERN } from "@getstrata/core/runtime/frontendMode";
209
223
  var appEnvSchema = defineEnvSchema({
210
- DATABASE_URL: { required: true, pattern: /^postgres(ql)?:\/\// },
224
+ DATABASE_URL: {
225
+ required: true,
226
+ pattern: /^(postgres(ql)?|mysql|sqlite):\/\//i
227
+ },
211
228
  PORT: {
212
229
  integer: true,
213
230
  minimum: 1,
@@ -235,9 +252,21 @@ var appEnvSchema = defineEnvSchema({
235
252
  pattern: /^(sync|async|redis)$/
236
253
  },
237
254
  AUTH_DEV_HEADERS: {
238
- default: "true",
255
+ default: "false",
239
256
  pattern: /^(true|false|0|1)$/
240
257
  },
258
+ DB_CONNECTION: {
259
+ default: "",
260
+ pattern: /^(pgsql|postgres|postgresql|mysql|mariadb|sqlite)?$/i
261
+ },
262
+ AUTH_DEFAULT_GUARD: {
263
+ default: "web"
264
+ },
265
+ JWT_TTL_SECONDS: {
266
+ integer: true,
267
+ minimum: 60,
268
+ default: "3600"
269
+ },
241
270
  APP_ENV: {
242
271
  default: "local"
243
272
  },
@@ -247,6 +276,13 @@ var appEnvSchema = defineEnvSchema({
247
276
  APP_URL: {
248
277
  default: "http://localhost:3000"
249
278
  },
279
+ FRONTEND_MODE: {
280
+ default: "api",
281
+ pattern: FRONTEND_MODE_PATTERN
282
+ },
283
+ SPA_PREFIX: {
284
+ default: DEFAULT_SPA_PREFIX
285
+ },
250
286
  API_PREFIX: {
251
287
  default: "/api/v1"
252
288
  },
@@ -3,11 +3,11 @@ var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/createRoutes.ts
5
5
  import { jsonResponse as jsonResponse3 } from "@getstrata/core/http/response";
6
- import { isSpaEnabled as isSpaEnabled2, isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
6
+ import { isSpaEnabled as isSpaEnabled2, isViewsEnabled as isViewsEnabled4 } from "@getstrata/core/runtime/frontendMode";
7
7
  import { notFoundHtmlResponse as notFoundHtmlResponse2 } from "@getstrata/core/view";
8
8
 
9
9
  // ../../index.html
10
- var strata_default = __jsonParse("{\"index\":\"_.._/_.._/index.html\",\"files\":[{\"input\":\"../../index.html\",\"path\":\"./index-sfreg6q3.js\",\"loader\":\"js\",\"isEntry\":true,\"headers\":{\"etag\":\"Oc_YLpCmV6M\",\"content-type\":\"text/javascript;charset=utf-8\"}},{\"input\":\"../../index.html\",\"path\":\"_.._/_.._/index.html\",\"loader\":\"html\",\"isEntry\":true,\"headers\":{\"etag\":\"rWY90LWIcx8\",\"content-type\":\"text/html;charset=utf-8\"}}]}");
10
+ var strata_default = __jsonParse("{\"index\":\"_.._/_.._/index.html\",\"files\":[{\"input\":\"../../index.html\",\"path\":\"./index-sfreg6q3.js\",\"loader\":\"js\",\"isEntry\":true,\"headers\":{\"etag\":\"Oc_YLpCmV6M\",\"content-type\":\"text/javascript;charset=utf-8\"}},{\"input\":\"../../index.html\",\"path\":\"_.._/_.._/index.html\",\"loader\":\"html\",\"isEntry\":true,\"headers\":{\"etag\":\"ojosdcZCk7g\",\"content-type\":\"text/html;charset=utf-8\"}}]}");
11
11
 
12
12
  // ../../src/config/app.ts
13
13
  var appConfig = {
@@ -45,7 +45,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
45
45
  import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
46
46
  import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
47
47
  import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
48
- import { withErrorHandling } from "@getstrata/core/http/response";
48
+ import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
49
49
  import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
50
50
  import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
51
51
  import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
@@ -193,7 +193,7 @@ class HttpKernel {
193
193
  return withMiddleware(...middleware)(handler);
194
194
  }
195
195
  wrapApi(handler) {
196
- return this.wrap(["api", "authenticated"], handler);
196
+ return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
197
197
  }
198
198
  wrapWeb(handler) {
199
199
  return withErrorHandling(this.wrap("web", handler));
@@ -497,44 +497,68 @@ function buildModuleRoutes(dependencies, options = {}) {
497
497
  // ../../src/bootstrap/createSpaRoutes.ts
498
498
  import { join as join2 } from "path";
499
499
  import { jsonResponse } from "@getstrata/core/http/response";
500
- import { isSpaEnabled } from "@getstrata/core/runtime/frontendMode";
500
+ import { isSpaEnabled, isViewsEnabled as isViewsEnabled2, readSpaPrefix } from "@getstrata/core/runtime/frontendMode";
501
501
  var SPA_DIST_DIRECTORY = join2(process.cwd(), "frontend/dist");
502
- var SPA_INDEX_FILE = join2(SPA_DIST_DIRECTORY, "index.html");
503
- function createSpaRoutes(_dependencies) {
504
- return {
505
- "/app/*": async (request) => {
506
- const pathname = new URL(request.url).pathname;
507
- const relativePath = pathname.replace(/^\/app\//, "");
508
- const assetFile = Bun.file(join2(SPA_DIST_DIRECTORY, relativePath));
509
- if (relativePath.length > 0 && await assetFile.exists()) {
510
- return new Response(assetFile);
511
- }
512
- const indexFile = Bun.file(SPA_INDEX_FILE);
513
- if (await indexFile.exists()) {
514
- return new Response(indexFile, {
515
- headers: { "Content-Type": "text/html; charset=utf-8" }
516
- });
517
- }
518
- return jsonResponse({
519
- error: "SPA build not found. Run `bun run build:frontend`."
520
- }, { status: 503 });
521
- },
522
- "/": async () => Response.redirect("/app/", 302)
502
+ function relativeSpaPath(pathname, prefix) {
503
+ if (pathname === prefix || pathname === `${prefix}/`) {
504
+ return "";
505
+ }
506
+ if (pathname.startsWith(`${prefix}/`)) {
507
+ return pathname.slice(prefix.length + 1);
508
+ }
509
+ return "";
510
+ }
511
+ function createSpaDocumentHandler(prefix, distDirectory) {
512
+ const indexFilePath = join2(distDirectory, "index.html");
513
+ return async (request) => {
514
+ const pathname = new URL(request.url).pathname;
515
+ if (pathname.startsWith("/api/")) {
516
+ return new Response("Not found", { status: 404 });
517
+ }
518
+ const relativePath = relativeSpaPath(pathname, prefix);
519
+ const assetFile = Bun.file(join2(distDirectory, relativePath));
520
+ if (relativePath.length > 0 && await assetFile.exists()) {
521
+ return new Response(assetFile);
522
+ }
523
+ const indexFile = Bun.file(indexFilePath);
524
+ if (await indexFile.exists()) {
525
+ return new Response(indexFile, {
526
+ headers: { "Content-Type": "text/html; charset=utf-8" }
527
+ });
528
+ }
529
+ return jsonResponse({
530
+ error: "SPA build not found. Run `bun run frontend:build` in your app."
531
+ }, { status: 503 });
532
+ };
533
+ }
534
+ function createSpaRoutes(_dependencies, options = {}) {
535
+ const prefix = options.prefix ?? readSpaPrefix();
536
+ const distDirectory = options.distDirectory ?? SPA_DIST_DIRECTORY;
537
+ const wrap = options.wrap ?? ((handler2) => handler2);
538
+ const handler = wrap(createSpaDocumentHandler(prefix, distDirectory));
539
+ const routes = {
540
+ [prefix]: handler,
541
+ [`${prefix}/`]: handler,
542
+ [`${prefix}/*`]: handler
523
543
  };
544
+ if (!isViewsEnabled2()) {
545
+ routes["/"] = async () => Response.redirect(`${prefix}/`, 302);
546
+ }
547
+ return routes;
524
548
  }
525
- function mergeSpaRoutes(dependencies, routes) {
549
+ function mergeSpaRoutes(dependencies, routes, options) {
526
550
  if (!isSpaEnabled()) {
527
551
  return routes;
528
552
  }
529
553
  return {
530
- ...createSpaRoutes(dependencies),
554
+ ...createSpaRoutes(dependencies, options),
531
555
  ...routes
532
556
  };
533
557
  }
534
558
 
535
559
  // ../../src/bootstrap/createWebRoutes.ts
536
560
  import { join as join3 } from "path";
537
- import { isViewsEnabled as isViewsEnabled2 } from "@getstrata/core/runtime/frontendMode";
561
+ import { isViewsEnabled as isViewsEnabled3 } from "@getstrata/core/runtime/frontendMode";
538
562
  import { notFoundHtmlResponse } from "@getstrata/core/view";
539
563
 
540
564
  // ../../src/bootstrap/buildWebModuleRoutes.ts
@@ -584,7 +608,7 @@ function createWebRoutes(dependencies, options = {}) {
584
608
  return wrappedRoutes;
585
609
  }
586
610
  function mergeWebRoutes(dependencies, routes, options = {}) {
587
- if (!isViewsEnabled2()) {
611
+ if (!isViewsEnabled3()) {
588
612
  return routes;
589
613
  }
590
614
  return {
@@ -594,12 +618,8 @@ function mergeWebRoutes(dependencies, routes, options = {}) {
594
618
  }
595
619
 
596
620
  // ../../src/bootstrap/dogfoodApp.ts
597
- function readDogfoodApp() {
598
- const value = (process.env.DOGFOOD_APP ?? "hiroapp").trim().toLowerCase();
599
- return value === "workhub" ? "workhub" : "hiroapp";
600
- }
601
621
  function isHiroappDogfood() {
602
- return readDogfoodApp() === "hiroapp";
622
+ return true;
603
623
  }
604
624
 
605
625
  // ../../src/bootstrap/health.ts
@@ -757,7 +777,7 @@ function createRoutes(dependencies) {
757
777
  const baseRoutes = {
758
778
  ...healthRoutes,
759
779
  ...metricsRoutes,
760
- ...isViewsEnabled3() || isSpaEnabled2() ? {} : { "/": strata_default },
780
+ ...isViewsEnabled4() || isSpaEnabled2() ? {} : { "/": strata_default },
761
781
  ...wrappedModuleRoutes,
762
782
  [notFoundApiPath]: async () => {
763
783
  registerRoute2("GET", notFoundApiPath, ["global", "api"]);
@@ -765,7 +785,7 @@ function createRoutes(dependencies) {
765
785
  },
766
786
  "/*": async () => {
767
787
  registerRoute2("GET", "/*", []);
768
- if (isViewsEnabled3()) {
788
+ if (isViewsEnabled4()) {
769
789
  return notFoundHtmlResponse2();
770
790
  }
771
791
  if (isSpaEnabled2()) {