@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 +39 -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/bootstrap/web/session.d.ts +1 -0
- 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 +47 -26
- package/dist/index.js +105 -42
- 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
|
}
|
|
@@ -26,25 +34,39 @@ function defaultSessionSql() {
|
|
|
26
34
|
function defaultMapSessionUser(user) {
|
|
27
35
|
return {
|
|
28
36
|
id: user.id,
|
|
29
|
-
role: user.is_admin ? "admin" : "member"
|
|
37
|
+
role: user.is_admin ? "admin" : "member",
|
|
38
|
+
...user.email_verified_at !== undefined ? { emailVerifiedAt: user.email_verified_at } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function sessionDisplayName(row, email) {
|
|
42
|
+
if (typeof row.name === "string" && row.name.trim() !== "") {
|
|
43
|
+
return row.name;
|
|
44
|
+
}
|
|
45
|
+
const first = typeof row.first_name === "string" ? row.first_name.trim() : "";
|
|
46
|
+
const last = typeof row.last_name === "string" ? row.last_name.trim() : "";
|
|
47
|
+
const composed = `${first} ${last}`.trim();
|
|
48
|
+
return composed || email;
|
|
49
|
+
}
|
|
50
|
+
function mapSessionUserRow(row) {
|
|
51
|
+
const email = typeof row.email === "string" ? row.email : "";
|
|
52
|
+
return {
|
|
53
|
+
id: Number(row.user_id ?? row.id),
|
|
54
|
+
name: sessionDisplayName(row, email),
|
|
55
|
+
email,
|
|
56
|
+
learn_subscriber: Boolean(row.learn_subscriber),
|
|
57
|
+
is_admin: Boolean(row.is_admin),
|
|
58
|
+
...row.email_verified_at !== undefined ? { email_verified_at: row.email_verified_at } : {}
|
|
30
59
|
};
|
|
31
60
|
}
|
|
32
61
|
async function defaultLoadSessionUser(sql, sessionId) {
|
|
33
|
-
const rows = await sql.unsafe(`SELECT s.
|
|
34
|
-
COALESCE(u.is_admin, false) AS is_admin
|
|
62
|
+
const rows = await sql.unsafe(`SELECT s.user_id, s.expires_at, u.*
|
|
35
63
|
FROM sessions s
|
|
36
64
|
INNER JOIN users u ON u.id = s.user_id
|
|
37
|
-
WHERE s.id = $1 AND s.expires_at >
|
|
65
|
+
WHERE s.id = ${sqlPlaceholder(1)} AND s.expires_at > ${sqlNow()}`, [sessionId]);
|
|
38
66
|
const row = rows[0];
|
|
39
67
|
if (!row)
|
|
40
68
|
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
|
-
};
|
|
69
|
+
return mapSessionUserRow(row);
|
|
48
70
|
}
|
|
49
71
|
function redirectWithCookie(location, setCookie, status) {
|
|
50
72
|
return new Response(null, {
|
|
@@ -82,7 +104,7 @@ class CookieSessionStore {
|
|
|
82
104
|
if (!raw)
|
|
83
105
|
return null;
|
|
84
106
|
const [sessionId, signature] = raw.split(".");
|
|
85
|
-
if (!sessionId || !signature || signature
|
|
107
|
+
if (!sessionId || !signature || !timingSafeCompareString(signature, this.sign(sessionId))) {
|
|
86
108
|
return null;
|
|
87
109
|
}
|
|
88
110
|
return sessionId;
|
|
@@ -100,28 +122,24 @@ class CookieSessionStore {
|
|
|
100
122
|
const id = randomBytes(32).toString("hex");
|
|
101
123
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
102
124
|
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,
|
|
125
|
+
VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, expires.toISOString(), meta.userAgent ?? null, meta.ipAddress ?? null]);
|
|
104
126
|
return id;
|
|
105
127
|
}
|
|
106
128
|
async destroy(sessionId) {
|
|
107
|
-
await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
|
|
129
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
|
|
108
130
|
}
|
|
109
131
|
async destroyOtherSessions(userId, keepSessionId) {
|
|
110
|
-
await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = $1 AND id <> $2`, [
|
|
111
|
-
userId,
|
|
112
|
-
keepSessionId
|
|
113
|
-
]);
|
|
132
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
|
|
114
133
|
}
|
|
115
134
|
async listForUser(userId) {
|
|
135
|
+
const dialect = currentSqlDialect();
|
|
116
136
|
return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
|
|
117
137
|
FROM sessions
|
|
118
|
-
WHERE user_id = $1 AND expires_at >
|
|
119
|
-
ORDER BY last_active_at DESC
|
|
138
|
+
WHERE user_id = ${dialect.placeholder(1)} AND expires_at > ${dialect.nowExpression()}
|
|
139
|
+
ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]);
|
|
120
140
|
}
|
|
121
141
|
async touch(sessionId) {
|
|
122
|
-
await this.sql().unsafe(`UPDATE sessions SET last_active_at =
|
|
123
|
-
sessionId
|
|
124
|
-
]);
|
|
142
|
+
await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
|
|
125
143
|
}
|
|
126
144
|
async read(request) {
|
|
127
145
|
const sessionId = this.sessionIdFromRequest(request);
|
|
@@ -130,7 +148,7 @@ class CookieSessionStore {
|
|
|
130
148
|
return this.loadSessionUser(this.sql(), sessionId);
|
|
131
149
|
}
|
|
132
150
|
sign(value) {
|
|
133
|
-
return
|
|
151
|
+
return createHmac("sha256", this.secret).update(value).digest("hex").slice(0, 32);
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
154
|
|
|
@@ -153,8 +171,11 @@ class CookieSessionGuard {
|
|
|
153
171
|
class CookieSessionAuthManager extends AuthManager {
|
|
154
172
|
store;
|
|
155
173
|
constructor(store, mapUser = defaultMapSessionUser) {
|
|
156
|
-
|
|
174
|
+
const guard = new CookieSessionGuard(store, mapUser);
|
|
175
|
+
super(guard);
|
|
157
176
|
this.store = store;
|
|
177
|
+
this.registerGuard("web", guard);
|
|
178
|
+
this.registerGuard("session", guard);
|
|
158
179
|
}
|
|
159
180
|
async signIn(user, meta = {}) {
|
|
160
181
|
const sessionId = await this.store.create(user, meta);
|