@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.
- package/CHANGELOG.md +27 -12
- package/README.md +32 -12
- package/dist/bootstrap/web/server.d.ts +3 -3
- package/dist/entries/applicationRegistry.js +0 -2
- package/dist/entries/buildModuleRoutes.js +2 -4
- package/dist/entries/buildWebModuleRoutes.js +2 -4
- package/dist/entries/cache/modelCacheTags.js +0 -2
- package/dist/entries/config.js +0 -2
- package/dist/entries/context.js +0 -2
- package/dist/entries/contracts.js +0 -2
- package/dist/entries/createSpaRoutes.js +1 -3
- package/dist/entries/createWebRoutes.js +2 -4
- package/dist/entries/dependencies.js +0 -2
- package/dist/entries/discoverModules.js +0 -2
- package/dist/entries/health.js +0 -2
- package/dist/entries/http/securedRouteModelBinding.js +0 -2
- package/dist/entries/httpKernel.js +2 -4
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +0 -2
- package/dist/entries/membershipService.js +0 -2
- package/dist/entries/metricsRoutes.js +0 -2
- package/dist/entries/providers/view.js +0 -2
- package/dist/entries/providers.js +0 -2
- package/dist/entries/queue/defaultJobs.js +0 -2
- package/dist/entries/routeRegistry.js +0 -2
- package/dist/entries/schedule.js +0 -2
- package/dist/entries/secretsGuard.js +39 -9
- package/dist/entries/web/forms.js +0 -2
- package/dist/entries/web/routing.js +2 -4
- package/dist/entries/web/server.js +8 -4
- package/dist/entries/web/session.js +10 -6
- package/dist/entries/web/slug.js +0 -2
- package/dist/index.js +59 -15
- package/package.json +7 -9
- package/dist/_.._/_.._/index.html +0 -174
- package/dist/bootstrap/createRoutes.d.ts +0 -3
- package/dist/bootstrap/dogfoodApp.d.ts +0 -7
- package/dist/bootstrap/preloadModules.d.ts +0 -1
- package/dist/bootstrap/routes.d.ts +0 -3
- package/dist/entries/createRoutes.js +0 -801
- 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
|
|
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
|
|
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
|
|
46
|
-
- **Breaking:** `HttpKernel.wrapWebGuest()` defaults `home` to `/` instead of `/organizations`. Apps with an org home should pass that path (
|
|
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
|
|
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
|
|
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
|
|
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()`
|
|
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
|
|
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;
|
|
116
|
-
- `checkDatabase()` / `createHealthRoutes()` ping the bound database client, not
|
|
117
|
-
- `assertProductionSecrets()` is feature-gated. A production HTMX app with `SESSION_SECRET` (32+), `AUTH_DEV_HEADERS=false`, and feature flags off does not need
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
49
|
+
## Queue
|
|
32
50
|
|
|
33
|
-
|
|
51
|
+
`registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Apps that dispatch model webhooks call `registerWebhookJobs()` themselves.
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
## Not a starter API
|
|
36
54
|
|
|
37
|
-
|
|
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
|
-
|
|
57
|
+
## Docs
|
|
40
58
|
|
|
41
|
-
|
|
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):
|
|
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/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
|
|
147
|
+
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
150
148
|
return [
|
|
151
149
|
createMemoryThrottleMiddleware({
|
|
152
|
-
maxAttempts: Number.isFinite(
|
|
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
|
|
150
|
+
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
153
151
|
return [
|
|
154
152
|
createMemoryThrottleMiddleware({
|
|
155
|
-
maxAttempts: Number.isFinite(
|
|
153
|
+
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
156
154
|
decaySeconds: 60
|
|
157
155
|
})
|
|
158
156
|
];
|
package/dist/entries/config.js
CHANGED
package/dist/entries/context.js
CHANGED
|
@@ -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 ?? ((
|
|
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
|
|
155
|
+
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
158
156
|
return [
|
|
159
157
|
createMemoryThrottleMiddleware({
|
|
160
|
-
maxAttempts: Number.isFinite(
|
|
158
|
+
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
161
159
|
decaySeconds: 60
|
|
162
160
|
})
|
|
163
161
|
];
|
package/dist/entries/health.js
CHANGED
|
@@ -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
|
|
143
|
+
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
146
144
|
return [
|
|
147
145
|
createMemoryThrottleMiddleware({
|
|
148
|
-
maxAttempts: Number.isFinite(
|
|
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";
|
package/dist/entries/schedule.js
CHANGED
|
@@ -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/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
|
|
153
|
+
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
156
154
|
return [
|
|
157
155
|
createMemoryThrottleMiddleware({
|
|
158
|
-
maxAttempts: Number.isFinite(
|
|
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 (
|
|
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
|
|
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
|
|
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) {
|