@maestro-js/init 1.0.0-alpha.21 → 1.0.0-alpha.22

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/dist/bin.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  buildProjectTree,
4
4
  getInitVersion,
5
5
  scaffold
6
- } from "./chunk-52NA54G4.js";
6
+ } from "./chunk-FO3IYFDW.js";
7
7
 
8
8
  // src/bin.ts
9
9
  import crypto from "crypto";
@@ -909,34 +909,93 @@ export function getRequestContext(context: Readonly<RouterContextProvider>) {
909
909
  return request
910
910
  }
911
911
  `;
912
- var csrfMiddlewareServer = `import type { MiddlewareFunction } from 'react-router'
912
+ var csrfMiddlewareServer = `import { timingSafeEqual } from 'node:crypto'
913
+ import type { MiddlewareFunction } from 'react-router'
913
914
  import { getRequestContext } from './request-context.server'
914
915
 
916
+ const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
917
+
915
918
  export const csrfMiddleware: MiddlewareFunction<Response> = async ({ request, context }, next) => {
916
- const mutatingMethods = ['POST', 'PUT', 'DELETE']
917
- if (mutatingMethods.includes(request.method)) {
918
- const cloned = request.clone()
919
- const formDataForMiddleware = await cloned.formData()
920
- const clientCsrfToken = formDataForMiddleware.get('__csrfToken')
921
- const requestContext = getRequestContext(context)
922
- const sessionCsrfToken = requestContext.session.getCsrfToken()
923
- if (clientCsrfToken !== sessionCsrfToken) {
924
- throw new Response('Invalid CSRF token', { status: 403 })
919
+ if (!MUTATING_METHODS.has(request.method)) {
920
+ return next()
921
+ }
922
+
923
+ // Routes accepting JSON or other non-form bodies use an X-CSRF-Token header
924
+ // instead of a hidden form field. Either way, never throw 500 from this
925
+ // middleware on a malformed body \u2014 return a clean 403.
926
+ const contentType = request.headers.get('content-type') ?? ''
927
+ let clientCsrfToken: string | null = null
928
+ if (contentType.startsWith('application/x-www-form-urlencoded') || contentType.startsWith('multipart/form-data')) {
929
+ try {
930
+ const cloned = request.clone()
931
+ const formData = await cloned.formData()
932
+ const value = formData.get('__csrfToken')
933
+ clientCsrfToken = typeof value === 'string' ? value : null
934
+ } catch {
935
+ clientCsrfToken = null
925
936
  }
937
+ } else {
938
+ clientCsrfToken = request.headers.get('x-csrf-token')
939
+ }
940
+
941
+ const requestContext = getRequestContext(context)
942
+ const sessionCsrfToken = requestContext.session.getCsrfToken()
943
+
944
+ if (!clientCsrfToken || !constantTimeEqual(clientCsrfToken, sessionCsrfToken)) {
945
+ throw new Response('Invalid CSRF token', { status: 403 })
926
946
  }
927
947
  return next()
928
948
  }
949
+
950
+ function constantTimeEqual(a: string, b: string): boolean {
951
+ const aBuf = Buffer.from(a)
952
+ const bBuf = Buffer.from(b)
953
+ if (aBuf.length !== bBuf.length) return false
954
+ return timingSafeEqual(aBuf, bBuf)
955
+ }
929
956
  `;
930
957
  var securityHeadersMiddlewareServer = `import type { MiddlewareFunction } from 'react-router'
931
958
 
959
+ // Security-headers middleware must be the OUTERMOST middleware so that headers
960
+ // are applied to thrown Responses (e.g. 403s from csrfMiddleware) as well as
961
+ // returned ones \u2014 try/finally ensures both code paths get the headers.
932
962
  export const securityHeadersMiddleware: MiddlewareFunction<Response> = async (_args, next) => {
933
- const response = await next()
934
- response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
935
- response.headers.set('X-Content-Type-Options', 'nosniff')
936
- response.headers.set('X-Frame-Options', 'DENY')
937
- response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
963
+ let response: Response
964
+ try {
965
+ response = await next()
966
+ } catch (thrown) {
967
+ if (thrown instanceof Response) {
968
+ applySecurityHeaders(thrown.headers)
969
+ }
970
+ throw thrown
971
+ }
972
+ applySecurityHeaders(response.headers)
938
973
  return response
939
974
  }
975
+
976
+ function applySecurityHeaders(headers: Headers) {
977
+ headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')
978
+ headers.set('X-Content-Type-Options', 'nosniff')
979
+ headers.set('X-Frame-Options', 'DENY')
980
+ headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
981
+ // Default-deny CSP. Tighten with route-level nonces for inline <script>/<style>.
982
+ // Apps that need third-party origins (analytics, fonts, CDNs) should extend
983
+ // this list \u2014 ship something restrictive by default rather than leaving it
984
+ // off entirely.
985
+ if (!headers.has('Content-Security-Policy')) {
986
+ headers.set(
987
+ 'Content-Security-Policy',
988
+ "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; " +
989
+ "script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
990
+ )
991
+ }
992
+ headers.set(
993
+ 'Permissions-Policy',
994
+ 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()'
995
+ )
996
+ headers.set('Cross-Origin-Opener-Policy', 'same-origin')
997
+ headers.set('Cross-Origin-Resource-Policy', 'same-origin')
998
+ }
940
999
  `;
941
1000
  var customErrorStatusCodeMiddlewareServer = `import * as Sentry from '@sentry/react-router'
942
1001
  import { type MiddlewareFunction } from 'react-router'
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  buildProjectTree,
4
4
  getInitVersion,
5
5
  scaffold
6
- } from "./chunk-52NA54G4.js";
6
+ } from "./chunk-FO3IYFDW.js";
7
7
  export {
8
8
  buildProjectTree,
9
9
  getInitVersion,
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "devDependencies": {
15
15
  "@types/node": "^22.19.11"
16
16
  },
17
- "version": "1.0.0-alpha.21",
17
+ "version": "1.0.0-alpha.22",
18
18
  "publishConfig": {
19
19
  "access": "restricted"
20
20
  },