@getstrata/bootstrap 0.4.3 → 1.0.1

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 (40) hide show
  1. package/CHANGELOG.md +27 -12
  2. package/README.md +32 -12
  3. package/dist/bootstrap/web/server.d.ts +3 -3
  4. package/dist/entries/applicationRegistry.js +0 -2
  5. package/dist/entries/buildModuleRoutes.js +2 -4
  6. package/dist/entries/buildWebModuleRoutes.js +2 -4
  7. package/dist/entries/cache/modelCacheTags.js +0 -2
  8. package/dist/entries/config.js +0 -2
  9. package/dist/entries/context.js +0 -2
  10. package/dist/entries/contracts.js +0 -2
  11. package/dist/entries/createSpaRoutes.js +1 -3
  12. package/dist/entries/createWebRoutes.js +2 -4
  13. package/dist/entries/dependencies.js +0 -2
  14. package/dist/entries/discoverModules.js +0 -2
  15. package/dist/entries/health.js +0 -2
  16. package/dist/entries/http/securedRouteModelBinding.js +0 -2
  17. package/dist/entries/httpKernel.js +2 -4
  18. package/dist/entries/listeners/invalidateCacheOnModelWrite.js +0 -2
  19. package/dist/entries/membershipService.js +0 -2
  20. package/dist/entries/metricsRoutes.js +0 -2
  21. package/dist/entries/providers/view.js +0 -2
  22. package/dist/entries/providers.js +0 -2
  23. package/dist/entries/queue/defaultJobs.js +0 -2
  24. package/dist/entries/routeRegistry.js +0 -2
  25. package/dist/entries/schedule.js +0 -2
  26. package/dist/entries/secretsGuard.js +39 -9
  27. package/dist/entries/web/forms.js +0 -2
  28. package/dist/entries/web/routing.js +2 -4
  29. package/dist/entries/web/server.js +8 -4
  30. package/dist/entries/web/session.js +10 -6
  31. package/dist/entries/web/slug.js +0 -2
  32. package/dist/index.js +59 -15
  33. package/package.json +7 -9
  34. package/dist/_.._/_.._/index.html +0 -174
  35. package/dist/bootstrap/createRoutes.d.ts +0 -3
  36. package/dist/bootstrap/dogfoodApp.d.ts +0 -7
  37. package/dist/bootstrap/preloadModules.d.ts +0 -1
  38. package/dist/bootstrap/routes.d.ts +0 -3
  39. package/dist/entries/createRoutes.js +0 -801
  40. package/dist/index-sfreg6q3.js +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @getstrata/bootstrap changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - `assertProductionSecrets()` rejects secrets still holding a generated `change-me` placeholder. The shipped `SESSION_SECRET` placeholder was 38 characters, so it passed the length check and booted in production unchanged.
6
+ - Cookie sessions write timestamps through the active dialect, so `CookieSessionStore.create()` works on MySQL.
7
+ - Removed the `createRoutes` subpath. It was in-repo fixture HTTP, documented as not an app API, and it packed a stray `dist/_.._/_.._/index.html` asset and dogfood helpers into the tarball.
8
+ - Removed leftover third-party product token strings from the production blocklist.
9
+ - `assertProductionSecrets()` requires `APP_URL` to be a public `http(s)` origin (localhost rejected, plain http warns). Signed links and redirects are built from it.
10
+ - `createWebServer()` records the socket address (IPv4-mapped addresses normalized) in the request context, so throttles, request logs, and session rows see the real client.
11
+ - `CookieSessionAuthManager.signIn()` fills `ip_address` and `user_agent` from the request context when the caller passes no meta. The `Secure` cookie flag now keys on `APP_ENV=production` (or `NODE_ENV`), matching the rest of the framework; generated apps never set `NODE_ENV`, so production cookies were not marked `Secure`.
12
+ - `engines.bun` is declared.
13
+
14
+ ## 1.0.0
15
+
16
+ - First stable release of `@getstrata/bootstrap`.
17
+
3
18
  ## 0.4.3
4
19
 
5
20
  - Cookie session `mapUser` / row mapping can pass `email_verified_at` through as `emailVerifiedAt` so HTML email verification works.
@@ -19,7 +34,7 @@
19
34
 
20
35
  - Peer `@getstrata/core` `^0.7.0`.
21
36
  - 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.
37
+ - Hybrid SPA routes stay under `/app/*`. They do not redirect `/`, so HTML at `/` keeps the home path.
23
38
  - `appEnvSchema` validates `FRONTEND_MODE` with the core pattern, including `hybrid`.
24
39
  - `HttpKernel.wrapApi` maps thrown HTTP errors to JSON the same way `wrapWeb` maps them to HTML.
25
40
 
@@ -27,7 +42,7 @@
27
42
 
28
43
  - Peer `@getstrata/core` `^0.6.0`.
29
44
  - 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.
45
+ - Kernel helpers cover guest, verified-email, and password-confirm gates.
31
46
 
32
47
  ## 0.2.68
33
48
 
@@ -42,17 +57,17 @@
42
57
 
43
58
  ## 0.2.66
44
59
 
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).
60
+ - `assertProductionSecrets()` is feature-gated for every app. API tokens no longer imply HiroApp's encryption/CORS/OAuth/public-read checklist. Published test token strings are still denied. HiroApp's extra production profile lives in the app (`src/config/productionSecrets.ts`).
61
+ - **Breaking:** `HttpKernel.wrapWebGuest()` defaults `home` to `/` instead of `/organizations`. Apps with an org home should pass that path (HiroApp already does).
47
62
  - `createWebRoutes()` no longer seeds a `/` → `/organizations` redirect. The in-repo app owns `/` via its organization module.
48
63
 
49
64
  ## 0.2.65
50
65
 
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`).
66
+ - App listener discovery reads `src/listeners` from `process.cwd()` so a built `@getstrata/bootstrap` bundle still finds HiroApp registrars (`import.meta.dir` after `build:bootstrap` is the package dist). Listener boot calls each registrar every time (registrars are idempotent per `eventBus`).
52
67
 
53
68
  ## 0.2.64
54
69
 
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).
70
+ - `registerDefaultJobs()` no longer registers HiroApp `webhook.dispatch`. Apps that dispatch model webhooks should call `registerWebhookJobs()` (HiroApp's webhook provider and `queue:work` do).
56
71
 
57
72
  ## 0.2.63
58
73
 
@@ -60,7 +75,7 @@
60
75
 
61
76
  ## 0.2.62
62
77
 
63
- - `HttpKernel.wrapWebPasswordConfirm()` is the previous PHP framework `password.confirm` for HTML routes. Peer `@getstrata/core` `^0.5.75`.
78
+ - `HttpKernel.wrapWebPasswordConfirm()` is the HTML password-confirm gate. Peer `@getstrata/core` `^0.5.75`.
64
79
 
65
80
  ## 0.2.61
66
81
 
@@ -72,7 +87,7 @@
72
87
 
73
88
  ## 0.2.59
74
89
 
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`.
90
+ - `HttpKernel.wrapWebGuest()` sends signed-in HTML users to `/organizations` (override the home path). Peer `@getstrata/core` `^0.5.72`.
76
91
 
77
92
  ## 0.2.58
78
93
 
@@ -88,7 +103,7 @@
88
103
 
89
104
  ## 0.2.55
90
105
 
91
- - HttpKernel and route builders read `isViewsEnabled` / `isSpaEnabled` from `@getstrata/core/runtime/frontendMode` instead of the previous in-repo app `src/config/frontend`.
106
+ - HttpKernel and route builders read `isViewsEnabled` / `isSpaEnabled` from `@getstrata/core/runtime/frontendMode` instead of HiroApp `src/config/frontend`.
92
107
 
93
108
  ## 0.2.54
94
109
 
@@ -112,7 +127,7 @@
112
127
 
113
128
  ## 0.2.50
114
129
 
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.
130
+ - `createCookieSessionAuthManager` / `CookieSessionGuard` turn `CookieSessionStore` into an `AuthGuard` / `AuthManager` for `CORE_AUTH_TOKEN`. Supply a `mapUser` mapper; HiroApp abilities are not baked in. Omit `sql` to read the client from `bindDatabaseConnection()` on every call.
131
+ - `checkDatabase()` / `createHealthRoutes()` ping the bound database client, not HiroApp's private connection holder. `/health` stays `{ status: "ok" }` unless `{ pingOnHealth: true }`. Extra JSON fields are optional.
132
+ - `assertProductionSecrets()` is feature-gated. A production HTMX app with `SESSION_SECRET` (32+), `AUTH_DEV_HEADERS=false`, and feature flags off does not need HiroApp API tokens. HiroApp's current production env (rotated tokens, CORS, pepper, expiry) still passes.
118
133
  - Re-exports `CORE_ABILITY_CHECKER_TOKEN` and `CORE_AUTH_USER_DIRECTORY_TOKEN`.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @getstrata/bootstrap
2
2
 
3
- Application bootstrap for Strata apps: HttpKernel, service container, and web utilities.
3
+ Application bootstrap for [Strata](https://github.com/EyK-26/strata) apps: HttpKernel, service container, and web session helpers. Requires Bun (tested on 1.4.x).
4
+
5
+ Generate an app rather than wiring this by hand:
6
+
7
+ ```bash
8
+ bunx create-strata my-app
9
+ ```
4
10
 
5
11
  ## Install
6
12
 
@@ -8,6 +14,16 @@ Application bootstrap for Strata apps: HttpKernel, service container, and web ut
8
14
  bun add @getstrata/bootstrap @getstrata/core
9
15
  ```
10
16
 
17
+ ## HttpKernel and providers
18
+
19
+ ```typescript
20
+ import { coreProviders, createAppContext, createHttpKernel } from "@getstrata/bootstrap";
21
+ ```
22
+
23
+ `createAppContext()` does not call `assertProductionSecrets`. Call that yourself when `APP_ENV=production`; it is feature-gated, so an HTML app with API tokens off is not asked for token secrets. It rejects empty secrets, short `SESSION_SECRET` values, wildcard `CORS_ALLOWED_ORIGINS`, `AUTH_DEV_HEADERS=true`, `FEATURE_PUBLIC_READS=true`, and any secret still holding a generated `change-me` placeholder.
24
+
25
+ Build routes with `buildWebModuleRoutes` and `buildModuleRoutes`.
26
+
11
27
  ## Web utilities
12
28
 
13
29
  ```typescript
@@ -20,22 +36,26 @@ import {
20
36
  } from "@getstrata/bootstrap";
21
37
  ```
22
38
 
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.*`.
39
+ `CookieSessionStore` reads `is_admin` from the user row for `wrapWebGlobalAdmin`. Pass `loadSessionUser` when your `users` table does not match the default `SELECT u.*`.
24
40
 
25
- ## HttpKernel
41
+ HTML apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstrap/web/session` rather than the HMAC `SessionGuard`, then use `signIn` / `signOut` or the redirect helpers. Pass `mapUser` to map roles.
26
42
 
27
- ```typescript
28
- import { createHttpKernel, createAppContext, coreProviders } from "@getstrata/bootstrap";
29
- ```
43
+ `wrapWeb` applies the web group (CSRF plus flash) and `withErrorHandling`, so a 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` and `wrapWebRegister` already include the web group plus throttle, so do not wrap them with `wrapWeb` again. The throttle callback should return an HTML form at 429.
44
+
45
+ ## SPA routes
46
+
47
+ `createSpaRoutes` and `mergeSpaRoutes` serve `SPA_PREFIX` (default `/app`) as `prefix`, `prefix/`, and `prefix/*`. Hybrid keeps HTML at `/`. Pass `distDirectory` and `wrap` when the app is not the process cwd. Until the SPA is built, the prefix answers 503 telling you to run `bun run frontend:build`; generated apps ship that script.
30
48
 
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).
49
+ ## Queue
32
50
 
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.
51
+ `registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Apps that dispatch model webhooks call `registerWebhookJobs()` themselves.
34
52
 
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.
53
+ ## Not a starter API
36
54
 
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.
55
+ `@getstrata/bootstrap/createWebRoutes` and `@getstrata/bootstrap/schedule` assemble in-repo fixture HTTP for framework tests. They are not a general app API. New apps use `buildWebModuleRoutes` and `buildModuleRoutes`.
38
56
 
39
- `registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Apps that dispatch model webhooks should call `registerWebhookJobs()` themselves.
57
+ ## Docs
40
58
 
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`.
59
+ - [Building apps](https://github.com/EyK-26/strata/blob/main/docs/BUILDING-APPS.md)
60
+ - [Production](https://github.com/EyK-26/strata/blob/main/docs/PRODUCTION.md)
61
+ - [Auth choices](https://github.com/EyK-26/strata/blob/main/docs/AUTH.md)
@@ -1,4 +1,4 @@
1
- import type { BunRequest } from "bun";
1
+ import type { BunRequest, Server } from "bun";
2
2
  import type { AppRouteMap } from "../contracts.ts";
3
3
  export interface WebServerOptions {
4
4
  port: number;
@@ -7,7 +7,7 @@ export interface WebServerOptions {
7
7
  publicDir?: string;
8
8
  onRequest?: (request: Request) => Promise<void> | void;
9
9
  }
10
- type BunRouteHandler = (request: BunRequest) => Response | Promise<Response>;
10
+ type BunRouteHandler = (request: BunRequest, server: Server<unknown>) => Response | Promise<Response>;
11
11
  declare function convertAppRoutesToBunRoutes(routes: AppRouteMap): Record<string, Record<string, BunRouteHandler>>;
12
- export declare function createWebServer(options: WebServerOptions): Bun.Server<undefined>;
12
+ export declare function createWebServer(options: WebServerOptions): Server<undefined>;
13
13
  export { convertAppRoutesToBunRoutes };
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/applicationRegistry.ts
5
3
  import {
6
4
  resolveApplicationAuth,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/buildModuleRoutes.ts
5
3
  import { conditionalJsonResponse } from "@getstrata/core/http/conditionalResponse";
6
4
  import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
@@ -146,10 +144,10 @@ class HttpKernel {
146
144
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
147
145
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
148
146
  if (!redisUrl) {
149
- const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
147
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
150
148
  return [
151
149
  createMemoryThrottleMiddleware({
152
- maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
150
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
153
151
  decaySeconds: 60
154
152
  })
155
153
  ];
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/buildWebModuleRoutes.ts
5
3
  import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/core/http/middleware";
6
4
 
@@ -149,10 +147,10 @@ class HttpKernel {
149
147
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
150
148
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
151
149
  if (!redisUrl) {
152
- const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
150
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
153
151
  return [
154
152
  createMemoryThrottleMiddleware({
155
- maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
153
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
156
154
  decaySeconds: 60
157
155
  })
158
156
  ];
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/discoverModules.ts
5
3
  import { readdirSync } from "fs";
6
4
  import { join } from "path";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/config.ts
5
3
  import {
6
4
  CORE_ABILITY_CHECKER_TOKEN,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/applicationRegistry.ts
5
3
  import {
6
4
  resolveApplicationAuth,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/contracts.ts
5
3
  import {
6
4
  ConfigStore,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/createSpaRoutes.ts
5
3
  import { join } from "path";
6
4
  import { jsonResponse } from "@getstrata/core/http/response";
@@ -41,7 +39,7 @@ function createSpaDocumentHandler(prefix, distDirectory) {
41
39
  function createSpaRoutes(_dependencies, options = {}) {
42
40
  const prefix = options.prefix ?? readSpaPrefix();
43
41
  const distDirectory = options.distDirectory ?? SPA_DIST_DIRECTORY;
44
- const wrap = options.wrap ?? ((handler2) => handler2);
42
+ const wrap = options.wrap ?? ((handler) => handler);
45
43
  const handler = wrap(createSpaDocumentHandler(prefix, distDirectory));
46
44
  const routes = {
47
45
  [prefix]: handler,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/createWebRoutes.ts
5
3
  import { join as join2 } from "path";
6
4
  import { isViewsEnabled as isViewsEnabled2 } from "@getstrata/core/runtime/frontendMode";
@@ -154,10 +152,10 @@ class HttpKernel {
154
152
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
155
153
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
156
154
  if (!redisUrl) {
157
- const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
155
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
158
156
  return [
159
157
  createMemoryThrottleMiddleware({
160
- maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
158
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
161
159
  decaySeconds: 60
162
160
  })
163
161
  ];
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/applicationRegistry.ts
5
3
  import {
6
4
  resolveApplicationAuth,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/discoverModules.ts
5
3
  import { readdirSync } from "fs";
6
4
  import { join } from "path";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/health.ts
5
3
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
6
4
  import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/http/securedRouteModelBinding.ts
5
3
  import {
6
4
  securedBindRouteModel,
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/httpKernel.ts
5
3
  import {
6
4
  hasVerifiedEmail,
@@ -142,10 +140,10 @@ class HttpKernel {
142
140
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
143
141
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
144
142
  if (!redisUrl) {
145
- const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
143
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
146
144
  return [
147
145
  createMemoryThrottleMiddleware({
148
- maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
146
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
149
147
  decaySeconds: 60
150
148
  })
151
149
  ];
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
5
3
  import { eventBus, modelEventName } from "@getstrata/core/events";
6
4
  import { InvalidateCacheTagsJob } from "@getstrata/core/jobs/invalidateCacheTagsJob";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/membershipService.ts
5
3
  import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
6
4
  export {
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/metricsRoutes.ts
5
3
  import { timingSafeEqual } from "crypto";
6
4
  import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/providers/view.ts
5
3
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
6
4
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/providers/auth.ts
5
3
  import { BasicAuthGuard } from "@getstrata/core/auth/basicAuthGuard";
6
4
  import {
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/queue/defaultJobs.ts
5
3
  import { ExportAuditLogsJob } from "@getstrata/core/jobs/exportAuditLogsJob";
6
4
  import { InvalidateCacheTagsJob } from "@getstrata/core/jobs/invalidateCacheTagsJob";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/routeRegistry.ts
5
3
  class RouteRegistry {
6
4
  routes = [];
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/schedule.ts
5
3
  import { ExportAuditLogsJob } from "@getstrata/core/jobs/exportAuditLogsJob";
6
4
  import { appLogger } from "@getstrata/core/logging/logger";
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/secretsGuard.ts
5
3
  import { isViewsMode, parseFrontendMode } from "@getstrata/core/runtime/frontendMode";
6
4
  var PUBLISHED_TEST_ADMIN_API_TOKEN = "strata-admin-test-token";
@@ -9,14 +7,28 @@ var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "strata-scim-test-token";
9
7
  var MIN_SESSION_SECRET_LENGTH = 32;
10
8
  var PUBLISHED_TEST_TOKENS = new Set([
11
9
  PUBLISHED_TEST_ADMIN_API_TOKEN,
12
- PUBLISHED_TEST_MEMBER_API_TOKEN,
13
- "workhub-admin-test-token",
14
- "workhub-member-test-token"
15
- ]);
16
- var PUBLISHED_TEST_SCIM_TOKENS = new Set([
17
- PUBLISHED_TEST_SCIM_BEARER_TOKEN,
18
- "workhub-scim-test-token"
10
+ PUBLISHED_TEST_MEMBER_API_TOKEN
19
11
  ]);
12
+ var PUBLISHED_TEST_SCIM_TOKENS = new Set([PUBLISHED_TEST_SCIM_BEARER_TOKEN]);
13
+ var PLACEHOLDER_SECRET_PATTERN = /change-me/i;
14
+ var SECRETS_TO_ROTATE = [
15
+ "SESSION_SECRET",
16
+ "TOKEN_HASH_PEPPER",
17
+ "METRICS_TOKEN",
18
+ "SCIM_BEARER_TOKEN",
19
+ "JWT_SECRET",
20
+ "OAUTH_STATE_SECRET",
21
+ "KMS_ENCRYPTION_KEY",
22
+ "STRIPE_WEBHOOK_SECRET",
23
+ "ADMIN_API_TOKEN",
24
+ "MEMBER_API_TOKEN"
25
+ ];
26
+ function assertNoPlaceholderSecrets(env) {
27
+ const unrotated = SECRETS_TO_ROTATE.filter((name) => PLACEHOLDER_SECRET_PATTERN.test(env[name]?.trim() ?? ""));
28
+ if (unrotated.length > 0) {
29
+ throw new Error(`Production startup blocked: replace the generated placeholder values for ${unrotated.join(", ")}.`);
30
+ }
31
+ }
20
32
  function isEnabled(value, defaultEnabled) {
21
33
  if (value === undefined) {
22
34
  return defaultEnabled;
@@ -92,12 +104,30 @@ function assertFeatureProductionSecrets(env) {
92
104
  console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
93
105
  }
94
106
  }
107
+ var LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
108
+ function assertPublicAppUrl(env) {
109
+ const raw = env.APP_URL?.trim() ?? "";
110
+ let url = null;
111
+ try {
112
+ url = raw ? new URL(raw) : null;
113
+ } catch {
114
+ url = null;
115
+ }
116
+ if (!url || !/^https?:$/.test(url.protocol) || LOCAL_HOSTS.has(url.hostname)) {
117
+ throw new Error("Production startup blocked: set APP_URL to the public origin of this app (for example https://app.example.com). Signed links and redirects are built from it.");
118
+ }
119
+ if (url.protocol === "http:") {
120
+ console.warn(`[secrets] APP_URL uses http; session cookies are marked Secure in production and will not be sent over ${raw}.`);
121
+ }
122
+ }
95
123
  function assertProductionSecrets(env = process.env) {
96
124
  const appEnv = env.APP_ENV ?? "local";
97
125
  if (appEnv !== "production") {
98
126
  return;
99
127
  }
128
+ assertNoPlaceholderSecrets(env);
100
129
  assertAuthDevHeadersDisabled(env);
130
+ assertPublicAppUrl(env);
101
131
  if (isTokenAuthEnabled(env)) {
102
132
  assertTokenAuthProductionSecrets(env);
103
133
  }
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/web/forms.ts
5
3
  import {
6
4
  createCsrfProtection
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/web/routing.ts
5
3
  import { requestPrefersJson } from "@getstrata/core/http/contentNegotiation";
6
4
  import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
@@ -152,10 +150,10 @@ class HttpKernel {
152
150
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
153
151
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
154
152
  if (!redisUrl) {
155
- const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
153
+ const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
156
154
  return [
157
155
  createMemoryThrottleMiddleware({
158
- maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
156
+ maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
159
157
  decaySeconds: 60
160
158
  })
161
159
  ];
@@ -1,17 +1,20 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/web/server.ts
5
3
  import { currentRequestMeta, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
6
4
  import { notFoundHtmlResponse } from "@getstrata/core/view";
5
+ function socketAddress(server, request) {
6
+ const address = server?.requestIP(request)?.address;
7
+ return address ? address.replace(/^::ffff:/i, "") : null;
8
+ }
7
9
  async function missingHtmlResponse() {
8
10
  return notFoundHtmlResponse();
9
11
  }
10
12
  function wrapRouteHandler(handler) {
11
- return async (request) => {
13
+ return async (request, server) => {
12
14
  return await runWithRequestMeta({
13
15
  ...currentRequestMeta(),
14
16
  request,
17
+ ipAddress: socketAddress(server, request),
15
18
  userAgent: request.headers.get("user-agent")
16
19
  }, async () => {
17
20
  const response = await handler(request);
@@ -47,10 +50,11 @@ function createWebServer(options) {
47
50
  return Bun.serve({
48
51
  port: options.port,
49
52
  ...bunRoutes ? { routes: bunRoutes } : {},
50
- async fetch(request) {
53
+ async fetch(request, server) {
51
54
  return await runWithRequestMeta({
52
55
  ...currentRequestMeta(),
53
56
  request,
57
+ ipAddress: socketAddress(server, request),
54
58
  userAgent: request.headers.get("user-agent")
55
59
  }, async () => {
56
60
  await options.onRequest?.(request);
@@ -1,13 +1,13 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/web/session.ts
5
3
  import { createHmac, randomBytes } from "crypto";
6
4
  import { AuthManager } from "@getstrata/core/auth/guard";
7
5
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
8
6
  import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
9
- import { currentSqlDialect } from "@getstrata/core/database/dialect";
7
+ import { currentSqlDialect, sqlTimestamp } from "@getstrata/core/database/dialect";
10
8
  import { readRequestCookie } from "@getstrata/core/http/cookies";
9
+ import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
10
+ import { isProductionEnv } from "@getstrata/core/runtime/appEnv";
11
11
  import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
12
12
  function sqlPlaceholder(index) {
13
13
  return currentSqlDialect().placeholder(index);
@@ -110,7 +110,7 @@ class CookieSessionStore {
110
110
  return sessionId;
111
111
  }
112
112
  withSecureFlag(header) {
113
- if (true) {
113
+ if (!isProductionEnv()) {
114
114
  return header;
115
115
  }
116
116
  return header.includes("Secure") ? header : `${header}; Secure`;
@@ -122,7 +122,7 @@ class CookieSessionStore {
122
122
  const id = randomBytes(32).toString("hex");
123
123
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
124
124
  await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
125
- VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, expires.toISOString(), meta.userAgent ?? null, meta.ipAddress ?? null]);
125
+ VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, sqlTimestamp(expires), meta.userAgent ?? null, meta.ipAddress ?? null]);
126
126
  return id;
127
127
  }
128
128
  async destroy(sessionId) {
@@ -178,7 +178,11 @@ class CookieSessionAuthManager extends AuthManager {
178
178
  this.registerGuard("session", guard);
179
179
  }
180
180
  async signIn(user, meta = {}) {
181
- const sessionId = await this.store.create(user, meta);
181
+ const current = currentRequestMeta();
182
+ const sessionId = await this.store.create(user, {
183
+ userAgent: meta.userAgent ?? current.userAgent ?? null,
184
+ ipAddress: meta.ipAddress ?? current.ipAddress ?? null
185
+ });
182
186
  return { sessionId, setCookie: this.store.cookieHeader(user, sessionId) };
183
187
  }
184
188
  async signOut(request) {
@@ -1,6 +1,4 @@
1
1
  // @bun
2
- var __jsonParse = (a) => JSON.parse(a);
3
-
4
2
  // ../../src/bootstrap/web/slug.ts
5
3
  function slugify(value) {
6
4
  return value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);