@getstrata/core 0.5.60 → 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.
@@ -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 (_request, next) => {
58
- const response = await next();
59
- const headers = new Headers(response.headers);
60
- headers.set("X-Content-Type-Options", "nosniff");
61
- headers.set("X-Frame-Options", "DENY");
62
- headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
63
- headers.set("X-XSS-Protection", "0");
64
- headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
65
- if (appConfig.env === "production") {
66
- headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
67
- }
68
- return new Response(response.body, {
69
- status: response.status,
70
- statusText: response.statusText,
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
  };