@getstrata/bootstrap 0.2.68 → 0.4.2
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 +34 -10
- package/README.md +10 -10
- package/dist/_.._/_.._/index.html +2 -2
- package/dist/bootstrap/createSpaRoutes.d.ts +9 -2
- package/dist/bootstrap/dogfoodApp.d.ts +1 -1
- package/dist/bootstrap/httpKernel.d.ts +4 -4
- package/dist/bootstrap/public-api.d.ts +1 -1
- package/dist/bootstrap/schemaTarget.d.ts +5 -0
- package/dist/bootstrap/server.d.ts +1 -1
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/entries/buildModuleRoutes.js +2 -2
- package/dist/entries/buildWebModuleRoutes.js +2 -2
- package/dist/entries/context.js +41 -5
- package/dist/entries/createRoutes.js +57 -37
- package/dist/entries/createSpaRoutes.js +48 -24
- package/dist/entries/createWebRoutes.js +2 -2
- package/dist/entries/dependencies.js +41 -5
- package/dist/entries/httpKernel.js +2 -2
- package/dist/entries/providers.js +41 -5
- package/dist/entries/secretsGuard.js +13 -8
- package/dist/entries/web/routing.js +4 -3
- package/dist/entries/web/session.js +44 -25
- package/dist/index.js +102 -41
- package/package.json +5 -5
|
@@ -4,37 +4,61 @@ var __jsonParse = (a) => JSON.parse(a);
|
|
|
4
4
|
// ../../src/bootstrap/createSpaRoutes.ts
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { jsonResponse } from "@getstrata/core/http/response";
|
|
7
|
-
import { isSpaEnabled } from "@getstrata/core/runtime/frontendMode";
|
|
7
|
+
import { isSpaEnabled, isViewsEnabled, readSpaPrefix } from "@getstrata/core/runtime/frontendMode";
|
|
8
8
|
var SPA_DIST_DIRECTORY = join(process.cwd(), "frontend/dist");
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
function relativeSpaPath(pathname, prefix) {
|
|
10
|
+
if (pathname === prefix || pathname === `${prefix}/`) {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
if (pathname.startsWith(`${prefix}/`)) {
|
|
14
|
+
return pathname.slice(prefix.length + 1);
|
|
15
|
+
}
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
function createSpaDocumentHandler(prefix, distDirectory) {
|
|
19
|
+
const indexFilePath = join(distDirectory, "index.html");
|
|
20
|
+
return async (request) => {
|
|
21
|
+
const pathname = new URL(request.url).pathname;
|
|
22
|
+
if (pathname.startsWith("/api/")) {
|
|
23
|
+
return new Response("Not found", { status: 404 });
|
|
24
|
+
}
|
|
25
|
+
const relativePath = relativeSpaPath(pathname, prefix);
|
|
26
|
+
const assetFile = Bun.file(join(distDirectory, relativePath));
|
|
27
|
+
if (relativePath.length > 0 && await assetFile.exists()) {
|
|
28
|
+
return new Response(assetFile);
|
|
29
|
+
}
|
|
30
|
+
const indexFile = Bun.file(indexFilePath);
|
|
31
|
+
if (await indexFile.exists()) {
|
|
32
|
+
return new Response(indexFile, {
|
|
33
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return jsonResponse({
|
|
37
|
+
error: "SPA build not found. Run `bun run frontend:build` in your app."
|
|
38
|
+
}, { status: 503 });
|
|
30
39
|
};
|
|
31
40
|
}
|
|
32
|
-
function
|
|
41
|
+
function createSpaRoutes(_dependencies, options = {}) {
|
|
42
|
+
const prefix = options.prefix ?? readSpaPrefix();
|
|
43
|
+
const distDirectory = options.distDirectory ?? SPA_DIST_DIRECTORY;
|
|
44
|
+
const wrap = options.wrap ?? ((handler2) => handler2);
|
|
45
|
+
const handler = wrap(createSpaDocumentHandler(prefix, distDirectory));
|
|
46
|
+
const routes = {
|
|
47
|
+
[prefix]: handler,
|
|
48
|
+
[`${prefix}/`]: handler,
|
|
49
|
+
[`${prefix}/*`]: handler
|
|
50
|
+
};
|
|
51
|
+
if (!isViewsEnabled()) {
|
|
52
|
+
routes["/"] = async () => Response.redirect(`${prefix}/`, 302);
|
|
53
|
+
}
|
|
54
|
+
return routes;
|
|
55
|
+
}
|
|
56
|
+
function mergeSpaRoutes(dependencies, routes, options) {
|
|
33
57
|
if (!isSpaEnabled()) {
|
|
34
58
|
return routes;
|
|
35
59
|
}
|
|
36
60
|
return {
|
|
37
|
-
...createSpaRoutes(dependencies),
|
|
61
|
+
...createSpaRoutes(dependencies, options),
|
|
38
62
|
...routes
|
|
39
63
|
};
|
|
40
64
|
}
|
|
@@ -36,7 +36,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
36
36
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
37
37
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
38
38
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
39
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
39
|
+
import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
|
|
40
40
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
41
41
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
42
42
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -184,7 +184,7 @@ class HttpKernel {
|
|
|
184
184
|
return withMiddleware(...middleware)(handler);
|
|
185
185
|
}
|
|
186
186
|
wrapApi(handler) {
|
|
187
|
-
return this.wrap(["api", "authenticated"], handler);
|
|
187
|
+
return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
|
|
188
188
|
}
|
|
189
189
|
wrapWeb(handler) {
|
|
190
190
|
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:
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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: "
|
|
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
|
},
|
|
@@ -24,7 +24,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
24
24
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
25
25
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
26
26
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
27
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
27
|
+
import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
|
|
28
28
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
29
29
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
30
30
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -172,7 +172,7 @@ class HttpKernel {
|
|
|
172
172
|
return withMiddleware(...middleware)(handler);
|
|
173
173
|
}
|
|
174
174
|
wrapApi(handler) {
|
|
175
|
-
return this.wrap(["api", "authenticated"], handler);
|
|
175
|
+
return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
|
|
176
176
|
}
|
|
177
177
|
wrapWeb(handler) {
|
|
178
178
|
return withErrorHandling(this.wrap("web", handler));
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/providers/auth.ts
|
|
5
|
+
import { BasicAuthGuard } from "@getstrata/core/auth/basicAuthGuard";
|
|
5
6
|
import {
|
|
6
7
|
AuthManager,
|
|
7
8
|
CompositeGuard,
|
|
8
9
|
DatabaseTokenGuard,
|
|
9
10
|
GuestGuard
|
|
10
11
|
} from "@getstrata/core/auth/guard";
|
|
12
|
+
import { JwtGuard } from "@getstrata/core/auth/jwtGuard";
|
|
11
13
|
import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
|
|
12
14
|
|
|
13
15
|
// ../../src/config/auth.ts
|
|
14
16
|
var authConfig = {
|
|
15
|
-
allowDevHeaders:
|
|
17
|
+
allowDevHeaders: process.env.AUTH_DEV_HEADERS === "true",
|
|
16
18
|
tokenDefaultAbilities: ["*"]
|
|
17
19
|
};
|
|
18
20
|
|
|
@@ -47,11 +49,22 @@ var authProvider = {
|
|
|
47
49
|
name: "core.auth",
|
|
48
50
|
register({ container, config }) {
|
|
49
51
|
config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
|
|
50
|
-
const
|
|
52
|
+
const apiGuard = new DatabaseTokenGuard(container);
|
|
53
|
+
const sessionGuard = new SessionGuard(container);
|
|
54
|
+
const jwtGuard = new JwtGuard;
|
|
55
|
+
const basicGuard = new BasicAuthGuard(container);
|
|
56
|
+
const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
|
|
51
57
|
if (authConfig.allowDevHeaders) {
|
|
52
58
|
guards.push(new GuestGuard);
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
const auth = new AuthManager(new CompositeGuard(guards));
|
|
61
|
+
auth.registerGuard("api", apiGuard);
|
|
62
|
+
auth.registerGuard("access_token", apiGuard);
|
|
63
|
+
auth.registerGuard("jwt", jwtGuard);
|
|
64
|
+
auth.registerGuard("basic", basicGuard);
|
|
65
|
+
auth.registerGuard("web", sessionGuard);
|
|
66
|
+
auth.registerGuard("session", sessionGuard);
|
|
67
|
+
container.set(CORE_AUTH_TOKEN, auth);
|
|
55
68
|
}
|
|
56
69
|
};
|
|
57
70
|
var auth_default = authProvider;
|
|
@@ -112,8 +125,12 @@ var queueConfig = {
|
|
|
112
125
|
};
|
|
113
126
|
// ../../src/bootstrap/env.ts
|
|
114
127
|
import { defineEnvSchema } from "@getstrata/core/config/envSchema";
|
|
128
|
+
import { DEFAULT_SPA_PREFIX, FRONTEND_MODE_PATTERN } from "@getstrata/core/runtime/frontendMode";
|
|
115
129
|
var appEnvSchema = defineEnvSchema({
|
|
116
|
-
DATABASE_URL: {
|
|
130
|
+
DATABASE_URL: {
|
|
131
|
+
required: true,
|
|
132
|
+
pattern: /^(postgres(ql)?|mysql|sqlite):\/\//i
|
|
133
|
+
},
|
|
117
134
|
PORT: {
|
|
118
135
|
integer: true,
|
|
119
136
|
minimum: 1,
|
|
@@ -141,9 +158,21 @@ var appEnvSchema = defineEnvSchema({
|
|
|
141
158
|
pattern: /^(sync|async|redis)$/
|
|
142
159
|
},
|
|
143
160
|
AUTH_DEV_HEADERS: {
|
|
144
|
-
default: "
|
|
161
|
+
default: "false",
|
|
145
162
|
pattern: /^(true|false|0|1)$/
|
|
146
163
|
},
|
|
164
|
+
DB_CONNECTION: {
|
|
165
|
+
default: "",
|
|
166
|
+
pattern: /^(pgsql|postgres|postgresql|mysql|mariadb|sqlite)?$/i
|
|
167
|
+
},
|
|
168
|
+
AUTH_DEFAULT_GUARD: {
|
|
169
|
+
default: "web"
|
|
170
|
+
},
|
|
171
|
+
JWT_TTL_SECONDS: {
|
|
172
|
+
integer: true,
|
|
173
|
+
minimum: 60,
|
|
174
|
+
default: "3600"
|
|
175
|
+
},
|
|
147
176
|
APP_ENV: {
|
|
148
177
|
default: "local"
|
|
149
178
|
},
|
|
@@ -153,6 +182,13 @@ var appEnvSchema = defineEnvSchema({
|
|
|
153
182
|
APP_URL: {
|
|
154
183
|
default: "http://localhost:3000"
|
|
155
184
|
},
|
|
185
|
+
FRONTEND_MODE: {
|
|
186
|
+
default: "api",
|
|
187
|
+
pattern: FRONTEND_MODE_PATTERN
|
|
188
|
+
},
|
|
189
|
+
SPA_PREFIX: {
|
|
190
|
+
default: DEFAULT_SPA_PREFIX
|
|
191
|
+
},
|
|
156
192
|
API_PREFIX: {
|
|
157
193
|
default: "/api/v1"
|
|
158
194
|
},
|
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/secretsGuard.ts
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
var
|
|
5
|
+
import { isViewsMode, parseFrontendMode } from "@getstrata/core/runtime/frontendMode";
|
|
6
|
+
var PUBLISHED_TEST_ADMIN_API_TOKEN = "strata-admin-test-token";
|
|
7
|
+
var PUBLISHED_TEST_MEMBER_API_TOKEN = "strata-member-test-token";
|
|
8
|
+
var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "strata-scim-test-token";
|
|
8
9
|
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
9
10
|
var PUBLISHED_TEST_TOKENS = new Set([
|
|
10
11
|
PUBLISHED_TEST_ADMIN_API_TOKEN,
|
|
11
|
-
PUBLISHED_TEST_MEMBER_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"
|
|
12
19
|
]);
|
|
13
|
-
var PUBLISHED_TEST_SCIM_TOKENS = new Set([PUBLISHED_TEST_SCIM_BEARER_TOKEN]);
|
|
14
20
|
function isEnabled(value, defaultEnabled) {
|
|
15
21
|
if (value === undefined) {
|
|
16
22
|
return defaultEnabled;
|
|
@@ -34,7 +40,7 @@ function assertAuthDevHeadersDisabled(env) {
|
|
|
34
40
|
function assertSessionSecret(env) {
|
|
35
41
|
const secret = env.SESSION_SECRET?.trim() ?? "";
|
|
36
42
|
if (secret.length < MIN_SESSION_SECRET_LENGTH) {
|
|
37
|
-
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx (32+ characters).");
|
|
43
|
+
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx or hybrid (32+ characters).");
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
function assertPublishedTestTokensRotated(env) {
|
|
@@ -96,8 +102,7 @@ function assertProductionSecrets(env = process.env) {
|
|
|
96
102
|
assertTokenAuthProductionSecrets(env);
|
|
97
103
|
}
|
|
98
104
|
assertFeatureProductionSecrets(env);
|
|
99
|
-
|
|
100
|
-
if (frontendMode === "server-htmx") {
|
|
105
|
+
if (isViewsMode(parseFrontendMode(env.FRONTEND_MODE))) {
|
|
101
106
|
assertSessionSecret(env);
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/web/routing.ts
|
|
5
|
+
import { requestPrefersJson } from "@getstrata/core/http/contentNegotiation";
|
|
5
6
|
import { withErrorHandling as withErrorHandling2 } from "@getstrata/core/http/response";
|
|
6
7
|
|
|
7
8
|
// ../../src/bootstrap/http/securedRouteModelBinding.ts
|
|
@@ -33,7 +34,7 @@ import { createRequireGlobalAdminMiddleware } from "@getstrata/core/http/require
|
|
|
33
34
|
import { createRequirePasswordConfirmMiddleware } from "@getstrata/core/http/requirePasswordConfirmMiddleware";
|
|
34
35
|
import { createRequireVerifiedMiddleware } from "@getstrata/core/http/requireVerifiedMiddleware";
|
|
35
36
|
import { createRequireWebAuthMiddleware } from "@getstrata/core/http/requireWebAuthMiddleware";
|
|
36
|
-
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
37
|
+
import { withErrorHandling, withJsonErrorHandling } from "@getstrata/core/http/response";
|
|
37
38
|
import { withMiddleware } from "@getstrata/core/http/routeMiddleware";
|
|
38
39
|
import { createSecurityHeadersMiddleware } from "@getstrata/core/http/securityHeadersMiddleware";
|
|
39
40
|
import { createValidateSignatureMiddleware } from "@getstrata/core/http/signedUrl";
|
|
@@ -181,7 +182,7 @@ class HttpKernel {
|
|
|
181
182
|
return withMiddleware(...middleware)(handler);
|
|
182
183
|
}
|
|
183
184
|
wrapApi(handler) {
|
|
184
|
-
return this.wrap(["api", "authenticated"], handler);
|
|
185
|
+
return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
|
|
185
186
|
}
|
|
186
187
|
wrapWeb(handler) {
|
|
187
188
|
return withErrorHandling(this.wrap("web", handler));
|
|
@@ -366,7 +367,7 @@ function wrapWebThrottle(kernel, scope, handler, onThrottled) {
|
|
|
366
367
|
const throttled = scope === "login" ? kernel.wrapLogin(handler) : kernel.wrapRegister(handler);
|
|
367
368
|
return async (request) => {
|
|
368
369
|
const response = await throttled(request);
|
|
369
|
-
if (response.status === 429) {
|
|
370
|
+
if (response.status === 429 && !requestPrefersJson(request)) {
|
|
370
371
|
return onThrottled(request);
|
|
371
372
|
}
|
|
372
373
|
return response;
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/web/session.ts
|
|
5
|
-
import {
|
|
5
|
+
import { createHmac, randomBytes } from "crypto";
|
|
6
6
|
import { AuthManager } from "@getstrata/core/auth/guard";
|
|
7
7
|
import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
|
|
8
8
|
import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
|
|
9
|
+
import { currentSqlDialect } from "@getstrata/core/database/dialect";
|
|
9
10
|
import { readRequestCookie } from "@getstrata/core/http/cookies";
|
|
11
|
+
import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
|
|
12
|
+
function sqlPlaceholder(index) {
|
|
13
|
+
return currentSqlDialect().placeholder(index);
|
|
14
|
+
}
|
|
15
|
+
function sqlNow() {
|
|
16
|
+
return currentSqlDialect().nowExpression();
|
|
17
|
+
}
|
|
10
18
|
function isSqlClient(value) {
|
|
11
19
|
return typeof value.unsafe === "function";
|
|
12
20
|
}
|
|
@@ -29,22 +37,34 @@ function defaultMapSessionUser(user) {
|
|
|
29
37
|
role: user.is_admin ? "admin" : "member"
|
|
30
38
|
};
|
|
31
39
|
}
|
|
40
|
+
function sessionDisplayName(row, email) {
|
|
41
|
+
if (typeof row.name === "string" && row.name.trim() !== "") {
|
|
42
|
+
return row.name;
|
|
43
|
+
}
|
|
44
|
+
const first = typeof row.first_name === "string" ? row.first_name.trim() : "";
|
|
45
|
+
const last = typeof row.last_name === "string" ? row.last_name.trim() : "";
|
|
46
|
+
const composed = `${first} ${last}`.trim();
|
|
47
|
+
return composed || email;
|
|
48
|
+
}
|
|
49
|
+
function mapSessionUserRow(row) {
|
|
50
|
+
const email = typeof row.email === "string" ? row.email : "";
|
|
51
|
+
return {
|
|
52
|
+
id: Number(row.user_id ?? row.id),
|
|
53
|
+
name: sessionDisplayName(row, email),
|
|
54
|
+
email,
|
|
55
|
+
learn_subscriber: Boolean(row.learn_subscriber),
|
|
56
|
+
is_admin: Boolean(row.is_admin)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
32
59
|
async function defaultLoadSessionUser(sql, sessionId) {
|
|
33
|
-
const rows = await sql.unsafe(`SELECT s.
|
|
34
|
-
COALESCE(u.is_admin, false) AS is_admin
|
|
60
|
+
const rows = await sql.unsafe(`SELECT s.user_id, s.expires_at, u.*
|
|
35
61
|
FROM sessions s
|
|
36
62
|
INNER JOIN users u ON u.id = s.user_id
|
|
37
|
-
WHERE s.id = $1 AND s.expires_at >
|
|
63
|
+
WHERE s.id = ${sqlPlaceholder(1)} AND s.expires_at > ${sqlNow()}`, [sessionId]);
|
|
38
64
|
const row = rows[0];
|
|
39
65
|
if (!row)
|
|
40
66
|
return null;
|
|
41
|
-
return
|
|
42
|
-
id: row.user_id,
|
|
43
|
-
name: row.name,
|
|
44
|
-
email: row.email,
|
|
45
|
-
learn_subscriber: row.learn_subscriber,
|
|
46
|
-
is_admin: row.is_admin
|
|
47
|
-
};
|
|
67
|
+
return mapSessionUserRow(row);
|
|
48
68
|
}
|
|
49
69
|
function redirectWithCookie(location, setCookie, status) {
|
|
50
70
|
return new Response(null, {
|
|
@@ -82,7 +102,7 @@ class CookieSessionStore {
|
|
|
82
102
|
if (!raw)
|
|
83
103
|
return null;
|
|
84
104
|
const [sessionId, signature] = raw.split(".");
|
|
85
|
-
if (!sessionId || !signature || signature
|
|
105
|
+
if (!sessionId || !signature || !timingSafeCompareString(signature, this.sign(sessionId))) {
|
|
86
106
|
return null;
|
|
87
107
|
}
|
|
88
108
|
return sessionId;
|
|
@@ -100,28 +120,24 @@ class CookieSessionStore {
|
|
|
100
120
|
const id = randomBytes(32).toString("hex");
|
|
101
121
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
102
122
|
await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
|
|
103
|
-
VALUES ($1, $2, $3, $4, $5,
|
|
123
|
+
VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, expires, meta.userAgent ?? null, meta.ipAddress ?? null]);
|
|
104
124
|
return id;
|
|
105
125
|
}
|
|
106
126
|
async destroy(sessionId) {
|
|
107
|
-
await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
|
|
127
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
|
|
108
128
|
}
|
|
109
129
|
async destroyOtherSessions(userId, keepSessionId) {
|
|
110
|
-
await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = $1 AND id <> $2`, [
|
|
111
|
-
userId,
|
|
112
|
-
keepSessionId
|
|
113
|
-
]);
|
|
130
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
|
|
114
131
|
}
|
|
115
132
|
async listForUser(userId) {
|
|
133
|
+
const dialect = currentSqlDialect();
|
|
116
134
|
return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
|
|
117
135
|
FROM sessions
|
|
118
|
-
WHERE user_id = $1 AND expires_at >
|
|
119
|
-
ORDER BY last_active_at DESC
|
|
136
|
+
WHERE user_id = ${dialect.placeholder(1)} AND expires_at > ${dialect.nowExpression()}
|
|
137
|
+
ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]);
|
|
120
138
|
}
|
|
121
139
|
async touch(sessionId) {
|
|
122
|
-
await this.sql().unsafe(`UPDATE sessions SET last_active_at =
|
|
123
|
-
sessionId
|
|
124
|
-
]);
|
|
140
|
+
await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
|
|
125
141
|
}
|
|
126
142
|
async read(request) {
|
|
127
143
|
const sessionId = this.sessionIdFromRequest(request);
|
|
@@ -130,7 +146,7 @@ class CookieSessionStore {
|
|
|
130
146
|
return this.loadSessionUser(this.sql(), sessionId);
|
|
131
147
|
}
|
|
132
148
|
sign(value) {
|
|
133
|
-
return
|
|
149
|
+
return createHmac("sha256", this.secret).update(value).digest("hex").slice(0, 32);
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
152
|
|
|
@@ -153,8 +169,11 @@ class CookieSessionGuard {
|
|
|
153
169
|
class CookieSessionAuthManager extends AuthManager {
|
|
154
170
|
store;
|
|
155
171
|
constructor(store, mapUser = defaultMapSessionUser) {
|
|
156
|
-
|
|
172
|
+
const guard = new CookieSessionGuard(store, mapUser);
|
|
173
|
+
super(guard);
|
|
157
174
|
this.store = store;
|
|
175
|
+
this.registerGuard("web", guard);
|
|
176
|
+
this.registerGuard("session", guard);
|
|
158
177
|
}
|
|
159
178
|
async signIn(user, meta = {}) {
|
|
160
179
|
const sessionId = await this.store.create(user, meta);
|