@getstrata/core 0.5.59 → 0.5.61
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 +17 -0
- package/README.md +2 -2
- package/dist/core/database/bindConnection.d.ts +1 -3
- package/dist/core/database/bunSql.d.ts +13 -0
- package/dist/core/http/contentSecurityPolicy.d.ts +27 -0
- package/dist/core/http/memoryThrottleMiddleware.d.ts +2 -1
- package/dist/core/http/requestMetaContext.d.ts +1 -0
- package/dist/core/http/safeInternalPath.d.ts +4 -0
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -0
- package/dist/core/http/securityHeadersMiddleware.d.ts +5 -2
- package/dist/core/http/webErrorResponse.d.ts +1 -1
- package/dist/core/view/etaViewEngine.d.ts +3 -2
- package/dist/core/view/htmlResponse.d.ts +5 -2
- package/dist/core/view/index.d.ts +4 -1
- package/dist/core/view/viewEngine.d.ts +1 -0
- package/dist/core/view/webErrorView.d.ts +21 -0
- package/dist/entries/database/bunSql.js +1 -0
- package/dist/entries/http/contentSecurityPolicy.js +1 -0
- package/dist/entries/http/memoryThrottleMiddleware.js +10 -2
- package/dist/entries/http/requireWebAuthMiddleware.js +34 -2
- package/dist/entries/http/response.js +133 -555
- package/dist/entries/http/safeInternalPath.js +38 -0
- package/dist/entries/http/securedRouteModelBinding.js +27 -5
- package/dist/entries/http/securityHeadersMiddleware.js +33 -60
- package/dist/entries/http/webErrorResponse.js +132 -554
- package/dist/entries/logging/requestLoggingMiddleware.js +2 -1
- package/dist/entries/view.js +96 -9
- package/dist/framework/public-api.d.ts +7 -3
- package/dist/index.js +665 -384
- package/package.json +17 -2
- package/dist/config/contentSecurityPolicy.d.ts +0 -5
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/http/safeInternalPath.ts
|
|
3
|
+
function looksLikeExternalTarget(value) {
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.includes("\\")) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (trimmed.includes("://") || trimmed.includes(":/") || trimmed.includes(":\\")) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const decoded = decodeURIComponent(trimmed);
|
|
13
|
+
if (decoded.startsWith("//") || decoded.includes("\\") || /https?:/i.test(decoded)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
function sanitizeInternalPath(raw, fallback = "/") {
|
|
22
|
+
if (looksLikeExternalTarget(raw)) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
function safeInternalRedirectPath(request, fallback = "/") {
|
|
28
|
+
const url = new URL(request.url);
|
|
29
|
+
return sanitizeInternalPath(`${url.pathname}${url.search}`, fallback);
|
|
30
|
+
}
|
|
31
|
+
function loginRedirectLocation(request) {
|
|
32
|
+
return `/login?redirect=${encodeURIComponent(safeInternalRedirectPath(request))}`;
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
loginRedirectLocation,
|
|
36
|
+
safeInternalRedirectPath,
|
|
37
|
+
sanitizeInternalPath
|
|
38
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/securedRouteModelBinding.ts
|
|
3
3
|
import { currentAuthUser as currentAuthUser2 } from "@getstrata/core/auth/authContext";
|
|
4
|
-
import { BadRequestError as BadRequestError2 } from "@getstrata/core/errors/http";
|
|
4
|
+
import { BadRequestError as BadRequestError2, NotFoundError } from "@getstrata/core/errors/http";
|
|
5
5
|
import {
|
|
6
6
|
resolveApplicationAuth,
|
|
7
7
|
resolveApplicationPolicyGate
|
|
@@ -238,10 +238,32 @@ function parsePositiveIntParam(value, name = "id") {
|
|
|
238
238
|
function isMutatingPolicyAction(action) {
|
|
239
239
|
return action === "update" || action === "delete";
|
|
240
240
|
}
|
|
241
|
+
function modelHasIdentity(model) {
|
|
242
|
+
return !!model && typeof model === "object" && "id" in model && model.id !== undefined && model.id !== null;
|
|
243
|
+
}
|
|
244
|
+
function shouldApplyViewEtag(response, model, authorization) {
|
|
245
|
+
if (authorization.etag === false) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (authorization.etag === true) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
252
|
+
if (contentType.includes("text/html")) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
return modelHasIdentity(model);
|
|
256
|
+
}
|
|
257
|
+
function requireResolvedModel(model) {
|
|
258
|
+
if (model == null) {
|
|
259
|
+
throw new NotFoundError;
|
|
260
|
+
}
|
|
261
|
+
return model;
|
|
262
|
+
}
|
|
241
263
|
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
242
264
|
return async (request) => {
|
|
243
265
|
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
244
|
-
const model = await resolver(id, request);
|
|
266
|
+
const model = requireResolvedModel(await resolver(id, request));
|
|
245
267
|
const gate = resolveApplicationPolicyGate();
|
|
246
268
|
const auth = resolveApplicationAuth();
|
|
247
269
|
const user = currentAuthUser2() ?? await auth.resolve(request);
|
|
@@ -252,7 +274,7 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
252
274
|
});
|
|
253
275
|
}
|
|
254
276
|
const response = await handler(request, model);
|
|
255
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
277
|
+
if (isEtagEnabled() && authorization.action === "view" && shouldApplyViewEtag(response, model, authorization)) {
|
|
256
278
|
return applyConditionalGet(request, response, etagFromResource(model));
|
|
257
279
|
}
|
|
258
280
|
return response;
|
|
@@ -264,7 +286,7 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
264
286
|
if (!key) {
|
|
265
287
|
throw new BadRequestError2(`Missing route parameter "${String(param)}".`);
|
|
266
288
|
}
|
|
267
|
-
const model = await resolver(key, request);
|
|
289
|
+
const model = requireResolvedModel(await resolver(key, request));
|
|
268
290
|
const gate = resolveApplicationPolicyGate();
|
|
269
291
|
const auth = resolveApplicationAuth();
|
|
270
292
|
const user = currentAuthUser2() ?? await auth.resolve(request);
|
|
@@ -275,7 +297,7 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
275
297
|
});
|
|
276
298
|
}
|
|
277
299
|
const response = await handler(request, model);
|
|
278
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
300
|
+
if (isEtagEnabled() && authorization.action === "view" && shouldApplyViewEtag(response, model, authorization)) {
|
|
279
301
|
return applyConditionalGet(request, response, etagFromResource(model));
|
|
280
302
|
}
|
|
281
303
|
return response;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
3
|
+
import {
|
|
4
|
+
configureContentSecurityPolicy,
|
|
5
|
+
generateCspNonce,
|
|
6
|
+
resolveContentSecurityPolicy
|
|
7
|
+
} from "@getstrata/core/http/contentSecurityPolicy";
|
|
8
|
+
import { currentRequestMeta, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
9
|
+
|
|
2
10
|
// ../../src/config/app.ts
|
|
3
11
|
var appConfig = {
|
|
4
12
|
name: "WorkHub",
|
|
@@ -8,70 +16,35 @@ var appConfig = {
|
|
|
8
16
|
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
9
17
|
};
|
|
10
18
|
|
|
11
|
-
// ../../src/config/contentSecurityPolicy.ts
|
|
12
|
-
function strictApiContentSecurityPolicy() {
|
|
13
|
-
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
14
|
-
}
|
|
15
|
-
function serverHtmxContentSecurityPolicy() {
|
|
16
|
-
return [
|
|
17
|
-
"default-src 'self'",
|
|
18
|
-
"script-src 'self' https://unpkg.com",
|
|
19
|
-
"style-src 'self'",
|
|
20
|
-
"connect-src 'self'",
|
|
21
|
-
"img-src 'self'",
|
|
22
|
-
"font-src 'self'",
|
|
23
|
-
"form-action 'self'",
|
|
24
|
-
"frame-ancestors 'none'",
|
|
25
|
-
"base-uri 'self'"
|
|
26
|
-
].join("; ");
|
|
27
|
-
}
|
|
28
|
-
function spaContentSecurityPolicy() {
|
|
29
|
-
return [
|
|
30
|
-
"default-src 'self'",
|
|
31
|
-
"script-src 'self'",
|
|
32
|
-
"style-src 'self'",
|
|
33
|
-
"connect-src 'self'",
|
|
34
|
-
"img-src 'self'",
|
|
35
|
-
"font-src 'self'",
|
|
36
|
-
"frame-ancestors 'none'",
|
|
37
|
-
"base-uri 'self'"
|
|
38
|
-
].join("; ");
|
|
39
|
-
}
|
|
40
|
-
function resolveContentSecurityPolicy(response) {
|
|
41
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
42
|
-
if (!contentType.includes("text/html")) {
|
|
43
|
-
return strictApiContentSecurityPolicy();
|
|
44
|
-
}
|
|
45
|
-
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
46
|
-
if (frontendMode === "server-htmx") {
|
|
47
|
-
return serverHtmxContentSecurityPolicy();
|
|
48
|
-
}
|
|
49
|
-
if (frontendMode === "spa-react") {
|
|
50
|
-
return spaContentSecurityPolicy();
|
|
51
|
-
}
|
|
52
|
-
return strictApiContentSecurityPolicy();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
19
|
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
56
|
-
function createSecurityHeadersMiddleware() {
|
|
57
|
-
return async (
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
headers
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
headers
|
|
20
|
+
function createSecurityHeadersMiddleware(options = {}) {
|
|
21
|
+
return async (request, next) => {
|
|
22
|
+
const nonce = generateCspNonce();
|
|
23
|
+
const existing = currentRequestMeta();
|
|
24
|
+
return await runWithRequestMeta({
|
|
25
|
+
...existing,
|
|
26
|
+
request: existing.request ?? request,
|
|
27
|
+
cspNonce: nonce
|
|
28
|
+
}, async () => {
|
|
29
|
+
const response = await next();
|
|
30
|
+
const headers = new Headers(response.headers);
|
|
31
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
32
|
+
headers.set("X-Frame-Options", "DENY");
|
|
33
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
34
|
+
headers.set("X-XSS-Protection", "0");
|
|
35
|
+
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response, { ...options, nonce }));
|
|
36
|
+
if (appConfig.env === "production") {
|
|
37
|
+
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
38
|
+
}
|
|
39
|
+
return new Response(response.body, {
|
|
40
|
+
status: response.status,
|
|
41
|
+
statusText: response.statusText,
|
|
42
|
+
headers
|
|
43
|
+
});
|
|
72
44
|
});
|
|
73
45
|
};
|
|
74
46
|
}
|
|
75
47
|
export {
|
|
48
|
+
configureContentSecurityPolicy,
|
|
76
49
|
createSecurityHeadersMiddleware
|
|
77
50
|
};
|