├── .changeset ├── README.md └── config.json ├── .github ├── dependabot.yml └── workflows │ ├── release-experimental.yml │ ├── release-preview.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── README.md ├── examples ├── basic │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── entry.server.tsx │ │ ├── root.tsx │ │ ├── routes │ │ │ └── _index.tsx │ │ └── styles │ │ │ └── app.css │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── remix.config.js │ ├── remix.env.d.ts │ └── tsconfig.json ├── react-router-v7 │ ├── .gitignore │ ├── Dockerfile │ ├── Dockerfile.bun │ ├── Dockerfile.pnpm │ ├── README.md │ ├── app │ │ ├── app.css │ │ ├── entry.client.tsx │ │ ├── entry.server.tsx │ │ ├── root.tsx │ │ ├── routes.ts │ │ ├── routes │ │ │ └── home.tsx │ │ └── welcome │ │ │ ├── logo-dark.svg │ │ │ ├── logo-light.svg │ │ │ └── welcome.tsx │ ├── package.json │ ├── prettier.config.js │ ├── public │ │ └── favicon.ico │ ├── react-router.config.ts │ ├── tsconfig.json │ └── vite.config.ts └── vite │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── app │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── root.css │ ├── root.tsx │ └── routes │ │ ├── _index.tsx │ │ └── page.$id.tsx │ ├── package.json │ ├── public │ └── favicon.ico │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages └── http-helmet │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ └── index.test.ts │ ├── package.json │ ├── src │ ├── helmet.ts │ ├── index.ts │ ├── react.tsx │ ├── rules │ │ ├── content-security-policy.ts │ │ ├── permissions.ts │ │ └── strict-transport-security.ts │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js └── scripts ├── publish.js ├── remove-prerelease-changelogs.js └── version.js /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: weekly 12 | time: "10:00" 13 | timezone: "America/Detroit" 14 | groups: 15 | "@remix-run": 16 | patterns: 17 | - "@remix-run/*" 18 | "@react-router": 19 | patterns: 20 | - "@react-router/*" 21 | - "react-router" 22 | "react": 23 | patterns: 24 | - "react" 25 | - "react-dom" 26 | - "@types/react" 27 | - "@types/react-dom" 28 | "eslint": 29 | patterns: 30 | - "@typescript-eslint/eslint-plugin" 31 | - "@typescript-eslint/parser" 32 | - "eslint" 33 | - "eslint-import-resolver-typescript" 34 | - "eslint-plugin-import" 35 | - "eslint-plugin-jsx-a11y" 36 | - "eslint-plugin-react" 37 | - "eslint-plugin-react-hooks" 38 | -------------------------------------------------------------------------------- /.github/workflows/release-experimental.yml: -------------------------------------------------------------------------------- 1 | # Experimental releases are handled a bit differently than standard releases. 2 | # Experimental releases can be branched from anywhere as they are not intended 3 | # for general use, and all packages will be versioned and published with the 4 | # same hash for testing. 5 | # 6 | # This workflow will run when a GitHub release is created from experimental 7 | # version tag. Unlike standard releases created via Changesets, only one tag 8 | # should be created for all packages. 9 | # 10 | # To create a release: 11 | # - Create a new branch for the release: git checkout -b `release-experimental` 12 | # - IMPORTANT: You should always create a new branch so that the version 13 | # changes don't accidentally get merged into `dev` or `main`. The branch 14 | # name must follow the convention of `release-experimental` or 15 | # `release-experimental-[feature]`. 16 | # - Make whatever changes you need and commit them: 17 | # - `git add . && git commit "experimental changes!"` 18 | # - Update version numbers and create a release tag: 19 | # - `yarn run version:experimental` 20 | # - Push to GitHub: 21 | # - `git push origin --follow-tags` 22 | # - Create a new release for the tag on GitHub to trigger the CI workflow that 23 | # will publish the release to npm 24 | 25 | name: 🚀 Release (experimental) 26 | on: 27 | push: 28 | tags: 29 | - "v0.0.0-experimental*" 30 | 31 | permissions: 32 | id-token: write 33 | 34 | concurrency: ${{ github.workflow }}-${{ github.ref }} 35 | 36 | env: 37 | CI: true 38 | 39 | jobs: 40 | release: 41 | name: 🧑‍🔬 Experimental Release 42 | if: | 43 | github.repository == 'mcansh/http-helmet' && 44 | contains(github.ref, 'experimental') 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: ⬇️ Checkout repo 48 | uses: actions/checkout@v4 49 | with: 50 | fetch-depth: 0 51 | 52 | - name: 🟧 Get pnpm version 53 | id: pnpm-version 54 | shell: bash 55 | run: | 56 | # get pnpm version from package.json packageManager field 57 | VERSION=$(node -e "console.log(require('./package.json').packageManager.replace(/pnpm@/, ''))") 58 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 59 | 60 | - name: ⎔ Setup node 61 | uses: actions/setup-node@v4 62 | with: 63 | node-version-file: ".nvmrc" 64 | 65 | - name: 🟧 Setup pnpm 66 | uses: pnpm/action-setup@v4 67 | with: 68 | version: ${{ steps.pnpm-version.outputs.VERSION }} 69 | run_install: | 70 | - recursive: true 71 | args: [--frozen-lockfile, --strict-peer-dependencies] 72 | cwd: ./ 73 | 74 | - name: 🏗 Build 75 | run: yarn build 76 | 77 | - name: 🔐 Setup npm auth 78 | run: | 79 | echo "registry=https://registry.npmjs.org" >> ~/.npmrc 80 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 81 | 82 | - name: 🚀 Publish 83 | run: npm run publish 84 | -------------------------------------------------------------------------------- /.github/workflows/release-preview.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Release (preview) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - ./packages/* 8 | tags: 9 | - "!**" 10 | pull_request: 11 | branches: [main] 12 | 13 | jobs: 14 | preview: 15 | runs-on: ubuntu-latest 16 | if: github.repository == 'mcansh/http-helmet' 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: 🟧 Get pnpm version 23 | id: pnpm-version 24 | shell: bash 25 | run: | 26 | # get pnpm version from package.json packageManager field 27 | VERSION=$(node -e "console.log(require('./package.json').packageManager.replace(/pnpm@/, ''))") 28 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 29 | 30 | - name: ⎔ Setup node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version-file: ".nvmrc" 34 | 35 | - name: 🟧 Setup pnpm 36 | uses: pnpm/action-setup@v4 37 | with: 38 | version: ${{ steps.pnpm-version.outputs.VERSION }} 39 | run_install: | 40 | - recursive: true 41 | args: [--frozen-lockfile, --strict-peer-dependencies] 42 | cwd: ./ 43 | 44 | - name: 🔐 Setup npm auth 45 | run: | 46 | echo "registry=https://registry.npmjs.org" >> ~/.npmrc 47 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 48 | 49 | - name: 🟧 Set publish-branch to current branch 50 | run: | 51 | echo "publish-branch=$(git branch --show-current)" >> ~/.npmrc 52 | 53 | - name: 🏗️ Build 54 | run: pnpm run build 55 | 56 | - name: 🚀 Publish PR 57 | run: pnpx pkg-pr-new publish --compact './packages/*' --template './examples/*' 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🦋 Changesets Release 2 | on: 3 | push: 4 | branches: 5 | - release 6 | - "release-*" 7 | - "!release-experimental" 8 | - "!release-experimental-*" 9 | - "!release-manual" 10 | - "!release-manual-*" 11 | 12 | permissions: 13 | id-token: write 14 | pull-requests: write 15 | contents: write 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | release: 23 | name: 🦋 Changesets Release 24 | if: github.repository == 'mcansh/http-helmet' 25 | runs-on: ubuntu-latest 26 | outputs: 27 | published: ${{ steps.changesets.outputs.published }} 28 | steps: 29 | - name: ⬇️ Checkout repo 30 | uses: actions/checkout@v4 31 | 32 | - name: 🟧 Get pnpm version 33 | id: pnpm-version 34 | shell: bash 35 | run: | 36 | # get pnpm version from package.json packageManager field 37 | VERSION=$(node -e "console.log(require('./package.json').packageManager.replace(/pnpm@/, ''))") 38 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 39 | 40 | - name: ⎔ Setup node 41 | uses: actions/setup-node@v4 42 | with: 43 | node-version-file: ".nvmrc" 44 | 45 | - name: 🟧 Setup pnpm 46 | uses: pnpm/action-setup@v4 47 | with: 48 | version: ${{ steps.pnpm-version.outputs.VERSION }} 49 | run_install: | 50 | - recursive: true 51 | args: [--frozen-lockfile, --strict-peer-dependencies] 52 | cwd: ./ 53 | 54 | - name: 🔐 Setup npm auth 55 | run: | 56 | echo "registry=https://registry.npmjs.org" >> ~/.npmrc 57 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc 58 | 59 | # This action has two responsibilities. The first time the workflow runs 60 | # (initial push to a `release-*` branch) it will create a new branch and 61 | # then open a PR with the related changes for the new version. After the 62 | # PR is merged, the workflow will run again and this action will build + 63 | # publish to npm & github packages. 64 | - name: 🚀 PR / Publish 65 | id: changesets 66 | uses: changesets/action@v1 67 | with: 68 | version: pnpm run changeset:version 69 | commit: "chore: Update version for release" 70 | title: "chore: Update version for release" 71 | publish: pnpm run changeset:release 72 | createGithubReleases: true 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 76 | 77 | comment: 78 | name: 📝 Comment on issues and pull requests 79 | if: github.repository == 'mcansh/http-helmet' && needs.release.outputs.published == 'true' 80 | needs: [release] 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: ⬇️ Checkout repo 84 | uses: actions/checkout@v4 85 | with: 86 | fetch-depth: 0 87 | 88 | - name: 📝 Comment on issues 89 | uses: remix-run/release-comment-action@v0.4.1 90 | with: 91 | DIRECTORY_TO_CHECK: "./packages" 92 | PACKAGE_NAME: "@mcansh/http-helmet" 93 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | name: ⚙️ Build 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: ⬇️ Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: 🟧 Get pnpm version 22 | id: pnpm-version 23 | shell: bash 24 | run: | 25 | # get pnpm version from package.json packageManager field 26 | VERSION=$(node -e "console.log(require('./package.json').packageManager.replace(/pnpm@/, ''))") 27 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 28 | 29 | - name: ⎔ Setup node 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version-file: ".nvmrc" 33 | 34 | - name: 🟧 Setup pnpm 35 | uses: pnpm/action-setup@v4 36 | with: 37 | version: ${{ steps.pnpm-version.outputs.VERSION }} 38 | run_install: | 39 | - recursive: true 40 | args: [--frozen-lockfile, --strict-peer-dependencies] 41 | cwd: ./ 42 | 43 | - name: 🏗 Build 44 | run: npm run build 45 | 46 | test: 47 | name: "🧪 Test: (OS: ${{ matrix.os }} Node: ${{ matrix.node }})" 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | os: 52 | - ubuntu-latest 53 | - macos-latest 54 | - windows-latest 55 | node: 56 | - 18 57 | - 20 58 | runs-on: ${{ matrix.os }} 59 | steps: 60 | - name: ⬇️ Checkout repo 61 | uses: actions/checkout@v4 62 | 63 | - name: 🟧 Get pnpm version 64 | id: pnpm-version 65 | shell: bash 66 | run: | 67 | # get pnpm version from package.json packageManager field 68 | VERSION=$(node -e "console.log(require('./package.json').packageManager.replace(/pnpm@/, ''))") 69 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 70 | 71 | - name: ⎔ Setup node 72 | uses: actions/setup-node@v4 73 | with: 74 | node-version: ${{ matrix.node }} 75 | 76 | - name: 🟧 Setup pnpm 77 | uses: pnpm/action-setup@v4 78 | with: 79 | version: ${{ steps.pnpm-version.outputs.VERSION }} 80 | run_install: | 81 | - recursive: true 82 | args: [--frozen-lockfile, --strict-peer-dependencies] 83 | cwd: ./ 84 | 85 | - name: 🧪 Run Primary Tests 86 | run: pnpm run test 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .ds_store 3 | node_modules 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/http-helmet/README.md -------------------------------------------------------------------------------- /examples/basic/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"], 4 | }; 5 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | From your terminal: 8 | 9 | ```sh 10 | npm run dev 11 | ``` 12 | 13 | This starts your app in development mode, rebuilding assets on file changes. 14 | 15 | ## Deployment 16 | 17 | First, build your app for production: 18 | 19 | ```sh 20 | npm run build 21 | ``` 22 | 23 | Then run the app in production mode: 24 | 25 | ```sh 26 | npm start 27 | ``` 28 | 29 | Now you'll need to pick a host to deploy it to. 30 | 31 | ### DIY 32 | 33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready. 34 | 35 | Make sure to deploy the output of `remix build` 36 | 37 | - `build/` 38 | - `public/build/` 39 | 40 | ### Using a Template 41 | 42 | When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server. 43 | 44 | ```sh 45 | cd .. 46 | # create a new project, and pick a pre-configured host 47 | npx create-remix@latest 48 | cd my-new-remix-app 49 | # remove the new project's app (not the old one!) 50 | rm -rf app 51 | # copy your app over 52 | cp -R ../my-old-remix-app/app app 53 | ``` 54 | -------------------------------------------------------------------------------- /examples/basic/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { PassThrough } from "node:stream"; 2 | 3 | import type { AppLoadContext, EntryContext } from "@remix-run/node"; 4 | import { createReadableStreamFromReadable } from "@remix-run/node"; 5 | import { RemixServer } from "@remix-run/react"; 6 | import { isbot } from "isbot"; 7 | import { renderToPipeableStream } from "react-dom/server"; 8 | import { 9 | createNonce, 10 | createSecureHeaders, 11 | mergeHeaders, 12 | } from "@mcansh/http-helmet"; 13 | import { NonceProvider } from "@mcansh/http-helmet/react"; 14 | 15 | const ABORT_DELAY = 5_000; 16 | 17 | export default function handleRequest( 18 | request: Request, 19 | responseStatusCode: number, 20 | responseHeaders: Headers, 21 | remixContext: EntryContext, 22 | loadContext: AppLoadContext 23 | ) { 24 | let callback = isbot(request.headers.get("user-agent")) 25 | ? "onAllReady" 26 | : "onShellReady"; 27 | 28 | let nonce = createNonce(); 29 | let secureHeaders = createSecureHeaders({ 30 | "Content-Security-Policy": { 31 | defaultSrc: ["'self'"], 32 | scriptSrc: ["'self'", `'nonce-${nonce}'`], 33 | connectSrc: [ 34 | "'self'", 35 | ...(process.env.NODE_ENV === "development" ? ["ws:", ""] : []), 36 | ], 37 | }, 38 | "Strict-Transport-Security": { 39 | maxAge: 31536000, 40 | includeSubDomains: true, 41 | preload: true, 42 | }, 43 | }); 44 | 45 | return new Promise((resolve, reject) => { 46 | let shellRendered = false; 47 | const { pipe, abort } = renderToPipeableStream( 48 | 49 | 54 | , 55 | { 56 | nonce, 57 | [callback]() { 58 | shellRendered = true; 59 | const body = new PassThrough(); 60 | const stream = createReadableStreamFromReadable(body); 61 | 62 | responseHeaders.set("Content-Type", "text/html"); 63 | 64 | resolve( 65 | new Response(stream, { 66 | headers: mergeHeaders(responseHeaders, secureHeaders), 67 | status: responseStatusCode, 68 | }) 69 | ); 70 | 71 | pipe(body); 72 | }, 73 | onShellError(error: unknown) { 74 | reject(error); 75 | }, 76 | onError(error: unknown) { 77 | responseStatusCode = 500; 78 | // Log streaming rendering errors from inside the shell. Don't log 79 | // errors encountered during initial shell rendering since they'll 80 | // reject and get logged in handleDocumentRequest. 81 | if (shellRendered) { 82 | console.error(error); 83 | } 84 | }, 85 | } 86 | ); 87 | 88 | setTimeout(abort, ABORT_DELAY); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /examples/basic/app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { LinksFunction } from "@remix-run/node"; 2 | import { 3 | Links, 4 | LiveReload, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration, 9 | } from "@remix-run/react"; 10 | import { useNonce } from "@mcansh/http-helmet/react"; 11 | 12 | import appStylesHref from "./styles/app.css"; 13 | 14 | export const links: LinksFunction = () => { 15 | return [{ rel: "stylesheet", href: appStylesHref }]; 16 | }; 17 | 18 | export default function App() { 19 | let nonce = useNonce(); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /examples/basic/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "@remix-run/node"; 2 | 3 | export const meta: MetaFunction = () => { 4 | return [ 5 | { title: "New Remix App" }, 6 | { name: "description", content: "Welcome to Remix!" }, 7 | ]; 8 | }; 9 | 10 | export default function Index() { 11 | return ( 12 |
13 |

Welcome to Remix

14 | 15 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /examples/basic/app/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 3 | system-ui, 4 | -apple-system, 5 | BlinkMacSystemFont, 6 | "Segoe UI", 7 | Roboto, 8 | Oxygen, 9 | Ubuntu, 10 | Cantarell, 11 | "Open Sans", 12 | "Helvetica Neue", 13 | sans-serif; 14 | line-height: 1.4; 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-app", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "remix build", 8 | "dev": "remix dev", 9 | "start": "remix-serve ./build/index.js", 10 | "typecheck": "tsc -b" 11 | }, 12 | "dependencies": { 13 | "@mcansh/http-helmet": "workspace:*", 14 | "@remix-run/node": "^2.16.0", 15 | "@remix-run/react": "^2.16.0", 16 | "@remix-run/serve": "^2.16.0", 17 | "isbot": "^5.1.23", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0" 20 | }, 21 | "devDependencies": { 22 | "@remix-run/dev": "^2.16.0", 23 | "@remix-run/eslint-config": "^2.16.0", 24 | "@types/react": "^19.0.10", 25 | "@types/react-dom": "^19.0.4", 26 | "eslint": "^8.57.0", 27 | "typescript": "^5.8.2" 28 | }, 29 | "engines": { 30 | "node": ">=18" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcansh/http-helmet/c508635390344e9bcff76ed9fe883d18a830d0d9/examples/basic/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | export default { 3 | ignoredRouteFiles: ["**/.*"], 4 | // appDirectory: "app", 5 | // assetsBuildDirectory: "public/build", 6 | // serverBuildPath: "build/index.js", 7 | // publicPath: "/build/", 8 | future: { 9 | v3_fetcherPersist: true, 10 | v3_relativeSplatPath: true, 11 | v3_throwAbortReason: true, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /examples/basic/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "Bundler", 9 | "resolveJsonModule": true, 10 | "target": "ES2022", 11 | "strict": true, 12 | "allowJs": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "~/*": ["./app/*"] 17 | }, 18 | 19 | // Remix takes care of building everything in `remix build`. 20 | "noEmit": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-router-v7/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.react-router 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /examples/react-router-v7/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /examples/react-router-v7/Dockerfile.bun: -------------------------------------------------------------------------------- 1 | FROM oven/bun:1 AS dependencies-env 2 | COPY . /app 3 | 4 | FROM dependencies-env AS development-dependencies-env 5 | COPY ./package.json bun.lockb /app/ 6 | WORKDIR /app 7 | RUN bun i --frozen-lockfile 8 | 9 | FROM dependencies-env AS production-dependencies-env 10 | COPY ./package.json bun.lockb /app/ 11 | WORKDIR /app 12 | RUN bun i --production 13 | 14 | FROM dependencies-env AS build-env 15 | COPY ./package.json bun.lockb /app/ 16 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 17 | WORKDIR /app 18 | RUN bun run build 19 | 20 | FROM dependencies-env 21 | COPY ./package.json bun.lockb /app/ 22 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 23 | COPY --from=build-env /app/build /app/build 24 | WORKDIR /app 25 | CMD ["bun", "run", "start"] -------------------------------------------------------------------------------- /examples/react-router-v7/Dockerfile.pnpm: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS dependencies-env 2 | RUN npm i -g pnpm 3 | COPY . /app 4 | 5 | FROM dependencies-env AS development-dependencies-env 6 | COPY ./package.json pnpm-lock.yaml /app/ 7 | WORKDIR /app 8 | RUN pnpm i --frozen-lockfile 9 | 10 | FROM dependencies-env AS production-dependencies-env 11 | COPY ./package.json pnpm-lock.yaml /app/ 12 | WORKDIR /app 13 | RUN pnpm i --prod --frozen-lockfile 14 | 15 | FROM dependencies-env AS build-env 16 | COPY ./package.json pnpm-lock.yaml /app/ 17 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 18 | WORKDIR /app 19 | RUN pnpm build 20 | 21 | FROM dependencies-env 22 | COPY ./package.json pnpm-lock.yaml /app/ 23 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 24 | COPY --from=build-env /app/build /app/build 25 | WORKDIR /app 26 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /examples/react-router-v7/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 📖 [React Router docs](https://reactrouter.com/) 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | Install the dependencies: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | Start the development server with HMR: 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | Your application will be available at `http://localhost:5173`. 34 | 35 | ## Building for Production 36 | 37 | Create a production build: 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | 43 | ## Deployment 44 | 45 | ### Docker Deployment 46 | 47 | This template includes three Dockerfiles optimized for different package managers: 48 | 49 | - `Dockerfile` - for npm 50 | - `Dockerfile.pnpm` - for pnpm 51 | - `Dockerfile.bun` - for bun 52 | 53 | To build and run using Docker: 54 | 55 | ```bash 56 | # For npm 57 | docker build -t my-app . 58 | 59 | # For pnpm 60 | docker build -f Dockerfile.pnpm -t my-app . 61 | 62 | # For bun 63 | docker build -f Dockerfile.bun -t my-app . 64 | 65 | # Run the container 66 | docker run -p 3000:3000 my-app 67 | ``` 68 | 69 | The containerized application can be deployed to any platform that supports Docker, including: 70 | 71 | - AWS ECS 72 | - Google Cloud Run 73 | - Azure Container Apps 74 | - Digital Ocean App Platform 75 | - Fly.io 76 | - Railway 77 | 78 | ### DIY Deployment 79 | 80 | If you're familiar with deploying Node applications, the built-in app server is production-ready. 81 | 82 | Make sure to deploy the output of `npm run build` 83 | 84 | ``` 85 | ├── package.json 86 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) 87 | ├── build/ 88 | │ ├── client/ # Static assets 89 | │ └── server/ # Server-side code 90 | ``` 91 | 92 | ## Styling 93 | 94 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 95 | 96 | --- 97 | 98 | Built with ❤️ using React Router. 99 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { startTransition, StrictMode } from "react"; 2 | import { hydrateRoot } from "react-dom/client"; 3 | import { HydratedRouter } from "react-router/dom"; 4 | 5 | startTransition(() => { 6 | hydrateRoot( 7 | document, 8 | 9 | 10 | , 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { createSecureHeaders, mergeHeaders } from "@mcansh/http-helmet"; 2 | import { NonceProvider } from "@mcansh/http-helmet/react"; 3 | import { createReadableStreamFromReadable } from "@react-router/node"; 4 | import { isbot } from "isbot"; 5 | import { PassThrough } from "node:stream"; 6 | import type { RenderToPipeableStreamOptions } from "react-dom/server"; 7 | import { renderToPipeableStream } from "react-dom/server"; 8 | import type { AppLoadContext, EntryContext } from "react-router"; 9 | import { ServerRouter } from "react-router"; 10 | 11 | const ABORT_DELAY = 5_000; 12 | 13 | export default function handleRequest( 14 | request: Request, 15 | responseStatusCode: number, 16 | responseHeaders: Headers, 17 | routerContext: EntryContext, 18 | _loadContext: AppLoadContext, 19 | ) { 20 | const nonce = createNonce(); 21 | const secureHeaders = createSecureHeaders({ 22 | "Content-Security-Policy": { 23 | "script-src": ["'self'", `'nonce-${nonce}'`], 24 | }, 25 | }); 26 | 27 | return new Promise((resolve, reject) => { 28 | let shellRendered = false; 29 | let userAgent = request.headers.get("user-agent"); 30 | 31 | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding 32 | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation 33 | let readyOption: keyof RenderToPipeableStreamOptions = 34 | (userAgent && isbot(userAgent)) || routerContext.isSpaMode 35 | ? "onAllReady" 36 | : "onShellReady"; 37 | 38 | const { pipe, abort } = renderToPipeableStream( 39 | 40 | 46 | , 47 | { 48 | nonce, 49 | [readyOption]() { 50 | shellRendered = true; 51 | const body = new PassThrough(); 52 | const stream = createReadableStreamFromReadable(body); 53 | 54 | responseHeaders.set("Content-Type", "text/html"); 55 | 56 | resolve( 57 | new Response(stream, { 58 | headers: mergeHeaders(responseHeaders, secureHeaders), 59 | status: responseStatusCode, 60 | }), 61 | ); 62 | 63 | pipe(body); 64 | }, 65 | onShellError(error: unknown) { 66 | reject(error); 67 | }, 68 | onError(error: unknown) { 69 | responseStatusCode = 500; 70 | // Log streaming rendering errors from inside the shell. Don't log 71 | // errors encountered during initial shell rendering since they'll 72 | // reject and get logged in handleDocumentRequest. 73 | if (shellRendered) { 74 | console.error(error); 75 | } 76 | }, 77 | }, 78 | ); 79 | 80 | setTimeout(abort, ABORT_DELAY); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { useNonce } from "@mcansh/http-helmet/react"; 2 | import { 3 | isRouteErrorResponse, 4 | Links, 5 | Meta, 6 | Outlet, 7 | Scripts, 8 | ScrollRestoration, 9 | } from "react-router"; 10 | import type { Route } from "./+types/root"; 11 | import stylesheet from "./app.css?url"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | { rel: "stylesheet", href: stylesheet }, 25 | ]; 26 | 27 | export function Layout({ children }: { children: React.ReactNode }) { 28 | const nonce = useNonce(); 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {children} 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default function App() { 48 | return ; 49 | } 50 | 51 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 52 | let message = "Oops!"; 53 | let details = "An unexpected error occurred."; 54 | let stack: string | undefined; 55 | 56 | if (isRouteErrorResponse(error)) { 57 | message = error.status === 404 ? "404" : "Error"; 58 | details = 59 | error.status === 404 60 | ? "The requested page could not be found." 61 | : error.statusText || details; 62 | } else if (import.meta.env.DEV && error && error instanceof Error) { 63 | details = error.message; 64 | stack = error.stack; 65 | } 66 | 67 | return ( 68 |
69 |

{message}

70 |

{details}

71 | {stack && ( 72 |
73 |           {stack}
74 |         
75 | )} 76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import { Welcome } from "../welcome/welcome"; 2 | import type { Route } from "./+types/home"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export default function Home() { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/welcome/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/welcome/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome() { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | const resources = [ 50 | { 51 | href: "https://reactrouter.com/docs", 52 | text: "React Router Docs", 53 | icon: ( 54 | 62 | 67 | 68 | ), 69 | }, 70 | { 71 | href: "https://rmx.as/discord", 72 | text: "Join Discord", 73 | icon: ( 74 | 82 | 86 | 87 | ), 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /examples/react-router-v7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rr-helmet", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "react-router build", 7 | "dev": "react-router dev", 8 | "start": "react-router-serve ./build/server/index.js", 9 | "typecheck": "react-router typegen && tsc --build --noEmit" 10 | }, 11 | "dependencies": { 12 | "@mcansh/http-helmet": "workspace:*", 13 | "@react-router/node": "^7.3.0", 14 | "@react-router/serve": "^7.3.0", 15 | "isbot": "^5.1.23", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0", 18 | "react-router": "^7.3.0" 19 | }, 20 | "devDependencies": { 21 | "@react-router/dev": "^7.3.0", 22 | "@tailwindcss/vite": "^4.0.12", 23 | "@types/node": "^22", 24 | "@types/react": "^19.0.10", 25 | "@types/react-dom": "^19.0.4", 26 | "prettier": "^3.5.3", 27 | "prettier-plugin-organize-imports": "^4.1.0", 28 | "tailwindcss": "^4.0.12", 29 | "typescript": "^5.8.2", 30 | "vite": "^6.2.1", 31 | "vite-tsconfig-paths": "^5.1.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/react-router-v7/prettier.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: ["prettier-plugin-organize-imports"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/react-router-v7/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcansh/http-helmet/c508635390344e9bcff76ed9fe883d18a830d0d9/examples/react-router-v7/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-router-v7/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /examples/react-router-v7/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "**/.server/**/*", 5 | "**/.client/**/*", 6 | ".react-router/types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "types": ["node", "vite/client"], 11 | "target": "ES2022", 12 | "module": "ES2022", 13 | "moduleResolution": "bundler", 14 | "jsx": "react-jsx", 15 | "rootDirs": [".", "./.react-router/types"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "isolatedModules": true, 23 | "noEmit": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "strict": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/react-router-v7/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [reactRouter(), tsconfigPaths(), tailwindcss()], 8 | }); 9 | -------------------------------------------------------------------------------- /examples/vite/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: "latest", 12 | sourceType: "module", 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | 23 | // Base config 24 | extends: ["eslint:recommended"], 25 | 26 | overrides: [ 27 | // React 28 | { 29 | files: ["**/*.{js,jsx,ts,tsx}"], 30 | plugins: ["react", "jsx-a11y"], 31 | extends: [ 32 | "plugin:react/recommended", 33 | "plugin:react/jsx-runtime", 34 | "plugin:react-hooks/recommended", 35 | "plugin:jsx-a11y/recommended", 36 | ], 37 | settings: { 38 | react: { 39 | version: "detect", 40 | }, 41 | formComponents: ["Form"], 42 | linkComponents: [ 43 | { name: "Link", linkAttribute: "to" }, 44 | { name: "NavLink", linkAttribute: "to" }, 45 | ], 46 | "import/resolver": { 47 | typescript: {}, 48 | }, 49 | }, 50 | }, 51 | 52 | // Typescript 53 | { 54 | files: ["**/*.{ts,tsx}"], 55 | plugins: ["@typescript-eslint", "import"], 56 | parser: "@typescript-eslint/parser", 57 | settings: { 58 | "import/internal-regex": "^~/", 59 | "import/resolver": { 60 | node: { 61 | extensions: [".ts", ".tsx"], 62 | }, 63 | typescript: { 64 | alwaysTryTypes: true, 65 | }, 66 | }, 67 | }, 68 | extends: [ 69 | "plugin:@typescript-eslint/recommended", 70 | "plugin:import/recommended", 71 | "plugin:import/typescript", 72 | ], 73 | }, 74 | 75 | // Node 76 | { 77 | files: [".eslintrc.cjs"], 78 | env: { 79 | node: true, 80 | }, 81 | }, 82 | ], 83 | }; 84 | -------------------------------------------------------------------------------- /examples/vite/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /examples/vite/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix + Vite! 2 | 3 | 📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. 4 | 5 | ## Development 6 | 7 | Run the Vite dev server: 8 | 9 | ```shellscript 10 | npm run dev 11 | ``` 12 | 13 | ## Deployment 14 | 15 | First, build your app for production: 16 | 17 | ```sh 18 | npm run build 19 | ``` 20 | 21 | Then run the app in production mode: 22 | 23 | ```sh 24 | npm start 25 | ``` 26 | 27 | Now you'll need to pick a host to deploy it to. 28 | 29 | ### DIY 30 | 31 | If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. 32 | 33 | Make sure to deploy the output of `npm run build` 34 | 35 | - `build/server` 36 | - `build/client` 37 | -------------------------------------------------------------------------------- /examples/vite/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | , 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/vite/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import { PassThrough } from "node:stream"; 8 | 9 | import type { AppLoadContext, EntryContext } from "@remix-run/node"; 10 | import { createReadableStreamFromReadable } from "@remix-run/node"; 11 | import { RemixServer } from "@remix-run/react"; 12 | import { isbot } from "isbot"; 13 | import { renderToPipeableStream } from "react-dom/server"; 14 | import { 15 | createNonce, 16 | createSecureHeaders, 17 | mergeHeaders, 18 | } from "@mcansh/http-helmet"; 19 | import { NonceProvider } from "@mcansh/http-helmet/react"; 20 | 21 | const ABORT_DELAY = 5_000; 22 | 23 | export default function handleRequest( 24 | request: Request, 25 | responseStatusCode: number, 26 | responseHeaders: Headers, 27 | remixContext: EntryContext, 28 | // This is ignored so we can keep it in the template for visibility. Feel 29 | // free to delete this parameter in your app if you're not using it! 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | _loadContext: AppLoadContext 32 | ) { 33 | const callback = isbot(request.headers.get("user-agent")) 34 | ? "onAllReady" 35 | : "onShellReady"; 36 | 37 | const nonce = createNonce(); 38 | const secureHeaders = createSecureHeaders({ 39 | "Content-Security-Policy": { 40 | "upgrade-insecure-requests": process.env.NODE_ENV === "production", 41 | "default-src": ["'self'"], 42 | "script-src": [ 43 | "'self'", 44 | `'nonce-${nonce}'`, 45 | "'strict-dynamic'", 46 | "'unsafe-inline'", 47 | "'unsafe-eval'", 48 | "'unsafe-hashes'", 49 | ], 50 | "connect-src": [ 51 | "'self'", 52 | ...(process.env.NODE_ENV === "development" ? ["ws:", ""] : []), 53 | ], 54 | "prefetch-src": ["'self'"], 55 | }, 56 | "Strict-Transport-Security": { 57 | maxAge: 31536000, 58 | includeSubDomains: true, 59 | preload: true, 60 | }, 61 | "Referrer-Policy": "origin-when-cross-origin", 62 | "Cross-Origin-Resource-Policy": "same-origin", 63 | "X-Content-Type-Options": "nosniff", 64 | "X-DNS-Prefetch-Control": "on", 65 | "X-XSS-Protection": "1; mode=block", 66 | "X-Frame-Options": "DENY", 67 | }); 68 | 69 | return new Promise((resolve, reject) => { 70 | let shellRendered = false; 71 | const { pipe, abort } = renderToPipeableStream( 72 | 73 | 78 | , 79 | { 80 | nonce, 81 | [callback]() { 82 | shellRendered = true; 83 | const body = new PassThrough(); 84 | const stream = createReadableStreamFromReadable(body); 85 | 86 | responseHeaders.set("Content-Type", "text/html"); 87 | 88 | resolve( 89 | new Response(stream, { 90 | headers: mergeHeaders(responseHeaders, secureHeaders), 91 | status: responseStatusCode, 92 | }) 93 | ); 94 | 95 | pipe(body); 96 | }, 97 | onShellError(error: unknown) { 98 | reject(error); 99 | }, 100 | onError(error: unknown) { 101 | responseStatusCode = 500; 102 | // Log streaming rendering errors from inside the shell. Don't log 103 | // errors encountered during initial shell rendering since they'll 104 | // reject and get logged in handleDocumentRequest. 105 | if (shellRendered) { 106 | console.error(error); 107 | } 108 | }, 109 | } 110 | ); 111 | 112 | setTimeout(abort, ABORT_DELAY); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /examples/vite/app/root.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | line-height: 1.8; 4 | } 5 | -------------------------------------------------------------------------------- /examples/vite/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { useNonce } from "@mcansh/http-helmet/react"; 2 | import { 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "@remix-run/react"; 9 | 10 | import rootStyleHref from "./root.css?url"; 11 | import { LinksFunction } from "@remix-run/node"; 12 | 13 | export let links: LinksFunction = () => { 14 | return [ 15 | { rel: "stylesheet", href: rootStyleHref }, 16 | { rel: "preload", as: "style", href: rootStyleHref }, 17 | ]; 18 | }; 19 | 20 | export function Layout({ children }: { children: React.ReactNode }) { 21 | let nonce = useNonce(); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {children} 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export default function App() { 41 | return ; 42 | } 43 | -------------------------------------------------------------------------------- /examples/vite/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "@remix-run/node"; 2 | import { Link } from "@remix-run/react"; 3 | 4 | export const meta: MetaFunction = () => { 5 | return [ 6 | { title: "New Remix App" }, 7 | { name: "description", content: "Welcome to Remix!" }, 8 | ]; 9 | }; 10 | 11 | export default function Index() { 12 | return ( 13 |
14 |

Welcome to Remix

15 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /examples/vite/app/routes/page.$id.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs } from "@remix-run/node"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | 4 | export function loader({ params }: LoaderFunctionArgs) { 5 | if (!params.id) throw Error("No id provided"); 6 | return { id: params.id }; 7 | } 8 | 9 | export default function Component() { 10 | let data = useLoaderData(); 11 | return

Hello from Page {data.id}

; 12 | } 13 | -------------------------------------------------------------------------------- /examples/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vite-app", 3 | "private": true, 4 | "sideEffects": false, 5 | "type": "module", 6 | "scripts": { 7 | "build": "remix vite:build", 8 | "dev": "remix vite:dev", 9 | "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", 10 | "start": "remix-serve ./build/server/index.js", 11 | "typecheck": "tsc" 12 | }, 13 | "dependencies": { 14 | "@mcansh/http-helmet": "workspace:*", 15 | "@remix-run/node": "2.16.0", 16 | "@remix-run/react": "2.16.0", 17 | "@remix-run/serve": "2.16.0", 18 | "isbot": "^5.1.23", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0" 21 | }, 22 | "devDependencies": { 23 | "@remix-run/dev": "2.16.0", 24 | "@types/react": "^19.0.10", 25 | "@types/react-dom": "^19.0.4", 26 | "@typescript-eslint/eslint-plugin": "^8.26.0", 27 | "@typescript-eslint/parser": "^8.26.0", 28 | "eslint": "^8.57.0", 29 | "eslint-import-resolver-typescript": "^3.8.3", 30 | "eslint-plugin-import": "^2.31.0", 31 | "eslint-plugin-jsx-a11y": "^6.10.2", 32 | "eslint-plugin-react": "^7.37.4", 33 | "eslint-plugin-react-hooks": "^5.2.0", 34 | "typescript": "^5.8.2", 35 | "vite": "^6.2.1", 36 | "vite-tsconfig-paths": "^5.1.4" 37 | }, 38 | "engines": { 39 | "node": ">=18.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/vite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcansh/http-helmet/c508635390344e9bcff76ed9fe883d18a830d0d9/examples/vite/public/favicon.ico -------------------------------------------------------------------------------- /examples/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx" 9 | ], 10 | "compilerOptions": { 11 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 12 | "types": ["@remix-run/node", "vite/client"], 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "jsx": "react-jsx", 16 | "module": "ESNext", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "target": "ES2022", 20 | "strict": true, 21 | "allowJs": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "~/*": ["./app/*"] 27 | }, 28 | 29 | // Vite takes care of building everything, not tsc. 30 | "noEmit": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePlugin as remix } from "@remix-run/dev"; 2 | import { installGlobals } from "@remix-run/node"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | installGlobals(); 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | remix({ 11 | future: { 12 | v3_fetcherPersist: true, 13 | v3_relativeSplatPath: true, 14 | v3_throwAbortReason: true, 15 | }, 16 | }), 17 | tsconfigPaths(), 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "clean": "del dist", 6 | "predev": "pnpm run clean", 7 | "dev": "pnpm run --filter http-helmet --filter example-app --recursive --parallel dev", 8 | "dev:vite": "pnpm run --filter http-helmet --filter example-vite-app --recursive --parallel dev", 9 | "prebuild": "pnpm run clean", 10 | "build": "pnpm run --recursive build", 11 | "test": "pnpm run --recursive --filter ./packages/* test", 12 | "lint": "pnpm run --recursive --filter ./packages/* lint", 13 | "publish": "./scripts/publish.js", 14 | "publint": "publint ./packages/**", 15 | "prepublishOnly": "pnpm run build", 16 | "changeset": "changeset", 17 | "changeset:version": "changeset version && node ./scripts/remove-prerelease-changelogs.js && pnpm install --lockfile-only", 18 | "changeset:release": "pnpm run build && changeset publish", 19 | "format": "prettier --cache --ignore-path .gitignore --ignore-path .prettierignore --write .", 20 | "validate": "run-p build lint format publint typecheck", 21 | "typecheck": "pnpm run --recursive --filter ./packages/* typecheck" 22 | }, 23 | "author": "Logan McAnsh (https://mcan.sh/)", 24 | "license": "MIT", 25 | "workspaces": [ 26 | "packages/*", 27 | "examples/*" 28 | ], 29 | "dependencies": { 30 | "@changesets/cli": "^2.28.1", 31 | "@manypkg/get-packages": "^2.2.2", 32 | "@types/node": "^22.13.10", 33 | "chalk": "^5.4.1", 34 | "del-cli": "^6.0.0", 35 | "glob": "^11.0.1", 36 | "jsonfile": "^6.1.0", 37 | "npm-run-all": "^4.1.5", 38 | "pkg-pr-new": "^0.0.40", 39 | "prettier": "^3.5.3", 40 | "prompt-confirm": "^2.0.4", 41 | "publint": "^0.3.8", 42 | "semver": "^7.7.1", 43 | "tsup": "^8.4.0", 44 | "type-fest": "^4.37.0", 45 | "typescript": "^5.8.2", 46 | "vitest": "^3.0.8" 47 | }, 48 | "packageManager": "pnpm@10.6.1+sha512.40ee09af407fa9fbb5fbfb8e1cb40fbb74c0af0c3e10e9224d7b53c7658528615b2c92450e74cfad91e3a2dcafe3ce4050d80bda71d757756d2ce2b66213e9a3" 49 | } 50 | -------------------------------------------------------------------------------- /packages/http-helmet/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @mcansh/http-helmet 2 | 3 | ## 0.13.0 4 | 5 | ### Minor Changes 6 | 7 | - 0908d16: update react and react-dom peerDependencies to support react 18 and react 19 8 | 9 | ### Patch Changes 10 | 11 | - 6e43ad1: allows shorthand for Strict-Transport-Policy header using `createStrictTransportSecurity` function and `createSecureHeaders` functions 12 | 13 | ```js 14 | import { createStrictTransportSecurity } from "@mcansh/http-helmet"; 15 | 16 | let hsts = createStrictTransportSecurity({ 17 | maxAge: 31536000, 18 | includeSubDomains: true, 19 | preload: true, 20 | }); 21 | // => "max-age=31536000; includeSubDomains; preload" 22 | ``` 23 | 24 | - 2664239: bumped typefest to latest release 25 | - 83767f3: add prequoted keyword exports (SELF, NONE, UNSAFE_EVAL, etc) 26 | 27 | ## 0.12.2 28 | 29 | ### Patch Changes 30 | 31 | - 4105e69: add support for interest-cohort to permissions policy 32 | 33 | ## 0.12.1 34 | 35 | ### Patch Changes 36 | 37 | - 81a7d9e: remove dependency on node:crypto using the crypto global instead 38 | 39 | ## 0.12.0 40 | 41 | ### Minor Changes 42 | 43 | - af61382: move `createNonce` helper function to main import 44 | add `type` to imports where missing 45 | 46 | ### Patch Changes 47 | 48 | - 4597846: dont allow mixing kebab-case and camelCase csp keys and make it so csp isnt required 49 | 50 | ## 0.11.1 51 | 52 | ### Patch Changes 53 | 54 | - f0a2ee3: feat: only allow using kebab or camel case, not both 55 | 56 | ## 0.11.0 57 | 58 | ### Minor Changes 59 | 60 | - 9b7cc24: feat: filter out falsy values from csp 61 | 62 | ```js 63 | createContentSecurityPolicy({ 64 | "connect-src": [undefined, "'self'", undefined], 65 | }); 66 | 67 | // => `"connect-src 'self'"` 68 | ``` 69 | 70 | ### Patch Changes 71 | 72 | - 9b7cc24: apply `upgrade-insecure-requests` when using kebab case to set it 73 | 74 | previously was only applying the `upgrade-insecure-requests` directive when using camelCase (upgradeInsecureRequests) 75 | 76 | ## 0.10.3 77 | 78 | ### Patch Changes 79 | 80 | - c4b0b6a: allow using kebab case keys for csp 81 | 82 | ```js 83 | let secureHeaders = createSecureHeaders({ 84 | "Content-Security-Policy": { 85 | "default-src": ["'self'"], 86 | "img-src": ["'self'", "data:"], 87 | }, 88 | }); 89 | ``` 90 | 91 | - 1cee380: allow setting Content-Security-Policy-Report-Only 92 | 93 | ```js 94 | let secureHeaders = createSecureHeaders({ 95 | "Content-Security-Policy-Report-Only": { 96 | "default-src": ["'self'"], 97 | "img-src": ["'self'", "data:"], 98 | }, 99 | }); 100 | ``` 101 | 102 | ## 0.10.2 103 | 104 | ### Patch Changes 105 | 106 | - 8e1c380: bump dependencies to latest versions 107 | - 6919888: add nonce generation, context provider, and hook for React and Remix apps 108 | 109 | ## 0.10.1 110 | 111 | ### Patch Changes 112 | 113 | - ba87f33: add funding to package.json 114 | 115 | ## 0.10.0 116 | 117 | ### Minor Changes 118 | 119 | - 7b0c887: re-export types/functions remove deprecated `strictTransportSecurity` in favor of renamed `createStrictTransportSecurity` 120 | - 7d1d570: use Headers global instead of the implementation from `@remix-run/web-fetch` 121 | 122 | ### Patch Changes 123 | 124 | - d439533: add mergeHeaders utility to merge your exisiting headers with the ones created by createdSecureHeaders 125 | - 12329f8: bump dependencies to latest versions 126 | 127 | ## 0.9.0 128 | 129 | ### Minor Changes 130 | 131 | - 0d92a95: stop publishing `@mcansh/remix-secure-headers` 132 | 133 | ## 0.8.2 134 | 135 | ### Patch Changes 136 | 137 | - b9372b6: chore: add support for more headers, add check to ensure we set them 138 | 139 | may or may not have not actually been setting COEP, COOP, CORP, X-Content-Type-Options, X-DNS-Prefetch-Control headers 😬 140 | 141 | ## 0.8.1 142 | 143 | ### Patch Changes 144 | 145 | - 7d28c52: rename repo, publish with provenance 146 | 147 | rename github repo, add repository property to package's package.json 148 | 149 | publish with npm provenance 150 | 151 | update example in README 152 | 153 | ## 0.8.0 154 | 155 | ### Minor Changes 156 | 157 | - 095ff81: rename package as it's for more than just remix 158 | 159 | ### Patch Changes 160 | 161 | - aea04b9: chore(deps): bump to latest 162 | -------------------------------------------------------------------------------- /packages/http-helmet/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Helmet 2 | 3 | easily add CSP and other security headers to your web application. 4 | 5 | ## Install 6 | 7 | ```sh 8 | # npm 9 | npm i @mcansh/http-helmet 10 | ``` 11 | 12 | ## Usage 13 | 14 | basic example using [`@mjackson/node-fetch-server`](https://github.com/mjackson/remix-the-web/tree/main/packages/node-fetch-server) 15 | 16 | ```js 17 | import * as http from "node:http"; 18 | import { createRequestListener } from "@mjackson/node-fetch-server"; 19 | import { createNonce, createSecureHeaders } from "@mcansh/http-helmet"; 20 | 21 | let html = String.raw; 22 | 23 | let handler = (request) => { 24 | let nonce = createNonce(); 25 | let headers = createSecureHeaders({ 26 | "Content-Security-Policy": { 27 | defaultSrc: ["'self'"], 28 | scriptSrc: ["'self'", `'nonce-${nonce}'`], 29 | }, 30 | }); 31 | 32 | headers.append("content-type", "text/html"); 33 | 34 | return new Response( 35 | html` 36 | 37 | 38 | 39 | 40 | 44 | Hello World 45 | 46 | 47 |

Hello World

48 | 49 | 52 | 53 | 56 | 57 | 58 | `, 59 | { headers }, 60 | ); 61 | }; 62 | 63 | let server = http.createServer(createRequestListener(handler)); 64 | 65 | server.listen(3000); 66 | 67 | console.log("✅ app ready: http://localhost:3000"); 68 | ``` 69 | -------------------------------------------------------------------------------- /packages/http-helmet/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import parseContentSecurityPolicy from "content-security-policy-parser"; 3 | import { 4 | createContentSecurityPolicy, 5 | createSecureHeaders, 6 | HASH, 7 | mergeHeaders, 8 | NONCE, 9 | NONE, 10 | REPORT_SAMPLE, 11 | SELF, 12 | STRICT_DYNAMIC, 13 | UNSAFE_EVAL, 14 | UNSAFE_HASHES, 15 | UNSAFE_INLINE, 16 | WASM_UNSAFE_EVAL, 17 | } from "../src/index.js"; 18 | 19 | describe("createSecureHeaders", () => { 20 | it("generates a config", () => { 21 | let headers = createSecureHeaders({ 22 | "Strict-Transport-Security": { 23 | maxAge: 63072000, 24 | includeSubDomains: true, 25 | preload: true, 26 | }, 27 | "Content-Security-Policy": { 28 | defaultSrc: ["'self'"], 29 | upgradeInsecureRequests: true, 30 | scriptSrc: ["'sha512-sdhgsgh'"], 31 | imgSrc: ["'none'"], 32 | }, 33 | "Permissions-Policy": { 34 | battery: [], 35 | accelerometer: ["self"], 36 | autoplay: ["https://example.com"], 37 | camera: ["*"], 38 | fullscreen: ["self", "https://example.com", "https://example.org"], 39 | interestCohort: [], 40 | }, 41 | "X-XSS-Protection": "1; report=https://google.com", 42 | "Cross-Origin-Embedder-Policy": "require-corp", 43 | "Cross-Origin-Opener-Policy": "same-origin", 44 | "Cross-Origin-Resource-Policy": "same-origin", 45 | "X-Content-Type-Options": "nosniff", 46 | "X-DNS-Prefetch-Control": "on", 47 | "Referrer-Policy": "strict-origin-when-cross-origin", 48 | "X-Frame-Options": "DENY", 49 | }); 50 | 51 | expect(headers.get("Strict-Transport-Security")).toBe( 52 | "max-age=63072000; includeSubDomains; preload", 53 | ); 54 | expect(headers.get("Content-Security-Policy")).toBe( 55 | "upgrade-insecure-requests; default-src 'self'; script-src 'sha512-sdhgsgh'; img-src 'none'", 56 | ); 57 | expect(headers.get("Permissions-Policy")).toBe( 58 | `battery=(), accelerometer=(self), autoplay=("https://example.com"), camera=*, fullscreen=(self "https://example.com" "https://example.org"), interest-cohort=()`, 59 | ); 60 | expect(headers.get("X-XSS-Protection")).toBe( 61 | "1; report=https://google.com", 62 | ); 63 | expect(headers.get("Cross-Origin-Embedder-Policy")).toBe("require-corp"); 64 | expect(headers.get("Cross-Origin-Opener-Policy")).toBe("same-origin"); 65 | expect(headers.get("Cross-Origin-Resource-Policy")).toBe("same-origin"); 66 | expect(headers.get("X-Content-Type-Options")).toBe("nosniff"); 67 | expect(headers.get("X-DNS-Prefetch-Control")).toBe("on"); 68 | expect(headers.get("Referrer-Policy")).toBe( 69 | "strict-origin-when-cross-origin", 70 | ); 71 | expect(headers.get("X-Frame-Options")).toBe("DENY"); 72 | }); 73 | 74 | it("allows using exported quoted values", () => { 75 | let headers = createSecureHeaders({ 76 | "Content-Security-Policy": { 77 | defaultSrc: [NONE], 78 | scriptSrc: [ 79 | SELF, 80 | NONCE("foo"), 81 | HASH("sha256", "bar"), 82 | UNSAFE_EVAL, 83 | UNSAFE_HASHES, 84 | WASM_UNSAFE_EVAL, 85 | STRICT_DYNAMIC, 86 | ], 87 | imgSrc: [SELF, "https://example.com"], 88 | styleSrc: [UNSAFE_EVAL, UNSAFE_INLINE], 89 | fontSrc: [REPORT_SAMPLE], 90 | }, 91 | }); 92 | 93 | let csp = headers.get("Content-Security-Policy"); 94 | if (!csp) throw new Error("Expected CSP header"); 95 | let parsed = parseContentSecurityPolicy(csp); 96 | 97 | let defaultSrc = parsed.get("default-src"); 98 | if (!defaultSrc) throw new Error("Expected default-src"); 99 | let scriptSrc = parsed.get("script-src"); 100 | if (!scriptSrc) throw new Error("Expected script-src"); 101 | let imgSrc = parsed.get("img-src"); 102 | if (!imgSrc) throw new Error("Expected img-src"); 103 | let styleSrc = parsed.get("style-src"); 104 | if (!styleSrc) throw new Error("Expected style-src"); 105 | let fontSrc = parsed.get("font-src"); 106 | if (!fontSrc) throw new Error("Expected font-src"); 107 | 108 | expect(defaultSrc).toEqual([NONE]); 109 | expect(scriptSrc).toEqual([ 110 | SELF, 111 | `'nonce-foo'`, 112 | `'sha256-bar'`, 113 | UNSAFE_EVAL, 114 | UNSAFE_HASHES, 115 | WASM_UNSAFE_EVAL, 116 | STRICT_DYNAMIC, 117 | ]); 118 | expect(imgSrc).toEqual([SELF, "https://example.com"]); 119 | expect(styleSrc).toEqual([UNSAFE_EVAL, UNSAFE_INLINE]); 120 | expect(fontSrc).toEqual([REPORT_SAMPLE]); 121 | }); 122 | 123 | it('allows shorthand for "Strict-Transport-Security"', () => { 124 | let headers = createSecureHeaders({ "Strict-Transport-Security": true }); 125 | 126 | expect(headers.get("Strict-Transport-Security")).toBe( 127 | "max-age=15552000; includeSubDomains; preload", 128 | ); 129 | }); 130 | }); 131 | 132 | it("throws an error if the value is reserved", () => { 133 | expect(() => 134 | createSecureHeaders({ 135 | "Content-Security-Policy": { 136 | defaultSrc: ["'self'", "https://example.com"], 137 | }, 138 | "Permissions-Policy": { 139 | battery: ["'self'"], 140 | }, 141 | }), 142 | ).toThrowErrorMatchingInlineSnapshot( 143 | `[Error: [createPermissionsPolicy]: self must not be quoted for "battery".]`, 144 | ); 145 | }); 146 | 147 | describe("mergeHeaders", () => { 148 | it("merges headers", () => { 149 | let secureHeaders = createSecureHeaders({ 150 | "Content-Security-Policy": { "default-src": ["'self'"] }, 151 | }); 152 | 153 | let responseHeaders = new Headers({ 154 | "Content-Type": "text/html", 155 | "x-foo": "bar", 156 | }); 157 | 158 | let merged = mergeHeaders(responseHeaders, secureHeaders); 159 | 160 | expect(merged.get("Content-Type")).toBe("text/html"); 161 | expect(merged.get("x-foo")).toBe("bar"); 162 | expect(merged.get("Content-Security-Policy")).toBe("default-src 'self'"); 163 | }); 164 | 165 | it("throws if the argument is not an object", () => { 166 | // @ts-expect-error 167 | expect(() => mergeHeaders("foo")).toThrowErrorMatchingInlineSnapshot( 168 | `[TypeError: All arguments must be of type object]`, 169 | ); 170 | }); 171 | 172 | it("overrides existing headers", () => { 173 | let secureHeaders = createSecureHeaders({ 174 | "Content-Security-Policy": { "default-src": ["'self'"] }, 175 | }); 176 | 177 | let responseHeaders = new Headers({ 178 | "Content-Security-Policy": "default-src 'none'", 179 | }); 180 | 181 | let merged1 = mergeHeaders(responseHeaders, secureHeaders); 182 | let merged2 = mergeHeaders(secureHeaders, responseHeaders); 183 | 184 | expect(merged1.get("Content-Security-Policy")).toBe("default-src 'self'"); 185 | expect(merged2.get("Content-Security-Policy")).toBe("default-src 'none'"); 186 | }); 187 | 188 | it('keeps all "Set-Cookie" headers', () => { 189 | let headers1 = new Headers({ "Set-Cookie": "foo=bar" }); 190 | let headers2 = new Headers({ "Set-Cookie": "baz=qux" }); 191 | 192 | let merged = mergeHeaders(headers1, headers2); 193 | 194 | expect(merged.get("Set-Cookie")).toBe("foo=bar, baz=qux"); 195 | expect(merged.getSetCookie()).toStrictEqual(["foo=bar", "baz=qux"]); 196 | }); 197 | }); 198 | 199 | it("allows mixing camel and kebab case for CSP keys", () => { 200 | let secureHeaders = createSecureHeaders({ 201 | "Content-Security-Policy": { 202 | "default-src": ["'self'"], 203 | imgSrc: ["'none'"], 204 | "frame-src": ["https://example.com"], 205 | }, 206 | }); 207 | 208 | expect(secureHeaders.get("Content-Security-Policy")).toBe( 209 | "default-src 'self'; img-src 'none'; frame-src https://example.com", 210 | ); 211 | }); 212 | 213 | it("throws an error on duplicate CSP keys", () => { 214 | expect(() => 215 | createSecureHeaders({ 216 | "Content-Security-Policy": { 217 | defaultSrc: ["'self'"], 218 | "default-src": ["'self'"], 219 | }, 220 | }), 221 | ).toThrowErrorMatchingInlineSnapshot( 222 | `[Error: [createContentSecurityPolicy]: The key "default-src" was specified in camelCase and kebab-case.]`, 223 | ); 224 | }); 225 | 226 | it('throws an error when "Content-Security-Policy" and "Content-Security-Policy-Report-Only" are set at the same time', () => { 227 | expect(() => 228 | // @ts-expect-error - this is intentional, we want to test the error 229 | createSecureHeaders({ 230 | "Content-Security-Policy": { 231 | defaultSrc: ["'self'"], 232 | }, 233 | "Content-Security-Policy-Report-Only": { 234 | defaultSrc: ["'self'"], 235 | }, 236 | }), 237 | ).toThrowErrorMatchingInlineSnapshot( 238 | `[Error: createSecureHeaders: Content-Security-Policy and Content-Security-Policy-Report-Only cannot be set at the same time]`, 239 | ); 240 | }); 241 | 242 | it("allows and filters out `undefined` values", () => { 243 | let csp = createContentSecurityPolicy({ 244 | "connect-src": [undefined, "'self'", undefined], 245 | }); 246 | 247 | expect(csp).toMatchInlineSnapshot(`"connect-src 'self'"`); 248 | }); 249 | 250 | it("throws an error when there's no define values for a csp key", () => { 251 | expect(() => 252 | createContentSecurityPolicy({ 253 | "base-uri": [undefined], 254 | "default-src": ["'none'"], 255 | }), 256 | ).toThrowErrorMatchingInlineSnapshot( 257 | `[Error: [createContentSecurityPolicy]: key "base-uri" has no defined options]`, 258 | ); 259 | }); 260 | 261 | describe("checks for both upgradeInsecureRequests and upgrade-insecure-requests", () => { 262 | it("upgradeInsecureRequests", () => { 263 | expect( 264 | createContentSecurityPolicy({ 265 | upgradeInsecureRequests: true, 266 | }), 267 | ).toMatchInlineSnapshot(`"upgrade-insecure-requests"`); 268 | }); 269 | 270 | it("upgrade-insecure-requests", () => { 271 | expect( 272 | createContentSecurityPolicy({ 273 | "upgrade-insecure-requests": true, 274 | }), 275 | ).toMatchInlineSnapshot(`"upgrade-insecure-requests"`); 276 | }); 277 | }); 278 | -------------------------------------------------------------------------------- /packages/http-helmet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mcansh/http-helmet", 3 | "version": "0.13.0", 4 | "description": "", 5 | "license": "MIT", 6 | "author": "Logan McAnsh (https://mcan.sh)", 7 | "type": "module", 8 | "repository": { 9 | "type": "git", 10 | "url": "https:/github.com/mcansh/http-helmet", 11 | "directory": "./packages/http-helmet" 12 | }, 13 | "funding": [ 14 | { 15 | "type": "github", 16 | "url": "https://github.com/sponsors/mcansh" 17 | } 18 | ], 19 | "exports": { 20 | "./package.json": "./package.json", 21 | ".": { 22 | "require": "./dist/index.cjs", 23 | "import": "./dist/index.js" 24 | }, 25 | "./react": { 26 | "require": "./dist/react.cjs", 27 | "import": "./dist/react.js" 28 | } 29 | }, 30 | "main": "./dist/index.cjs", 31 | "module": "./dist/index.js", 32 | "source": "./src/index.ts", 33 | "types": "./dist/index.d.ts", 34 | "files": [ 35 | "dist", 36 | "README.md", 37 | "package.json" 38 | ], 39 | "scripts": { 40 | "prepublishOnly": "npm run build", 41 | "build": "tsup", 42 | "dev": "tsup --watch", 43 | "test": "vitest", 44 | "typecheck": "tsc" 45 | }, 46 | "dependencies": { 47 | "change-case": "^5.4.4", 48 | "type-fest": "^4.37.0" 49 | }, 50 | "devDependencies": { 51 | "@types/react": "^19.0.10", 52 | "@types/react-dom": "^19.0.4", 53 | "content-security-policy-parser": "^0.6.0", 54 | "react": "^19.0.0", 55 | "react-dom": "^19.0.0" 56 | }, 57 | "peerDependencies": { 58 | "react": ">=18.0.0 || >=19.0.0", 59 | "react-dom": ">=18.0.0 || >=19.0.0" 60 | }, 61 | "peerDependenciesMeta": { 62 | "react": { 63 | "optional": true 64 | }, 65 | "react-dom": { 66 | "optional": true 67 | } 68 | }, 69 | "publishConfig": { 70 | "access": "public", 71 | "provenance": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/http-helmet/src/helmet.ts: -------------------------------------------------------------------------------- 1 | import type { RequireOneOrNone } from "type-fest"; 2 | import { createContentSecurityPolicy } from "./rules/content-security-policy.js"; 3 | import type { PublicContentSecurityPolicy } from "./rules/content-security-policy.js"; 4 | import { 5 | createPermissionsPolicy, 6 | PermissionsPolicy, 7 | } from "./rules/permissions.js"; 8 | import { 9 | createStrictTransportSecurity, 10 | StrictTransportSecurity, 11 | } from "./rules/strict-transport-security.js"; 12 | 13 | export type { PublicContentSecurityPolicy as ContentSecurityPolicy }; 14 | export { createContentSecurityPolicy } from "./rules/content-security-policy.js"; 15 | export { createPermissionsPolicy } from "./rules/permissions.js"; 16 | export type { PermissionsPolicy } from "./rules/permissions.js"; 17 | export { createStrictTransportSecurity } from "./rules/strict-transport-security.js"; 18 | export type { StrictTransportSecurity } from "./rules/strict-transport-security.js"; 19 | 20 | export type FrameOptions = "DENY" | "SAMEORIGIN"; 21 | export type ReferrerPolicy = 22 | | "no-referrer" 23 | | "no-referrer-when-downgrade" 24 | | "origin" 25 | | "origin-when-cross-origin" 26 | | "same-origin" 27 | | "strict-origin" 28 | | "strict-origin-when-cross-origin" 29 | | "unsafe-url"; 30 | export type DNSPrefetchControl = "on" | "off"; 31 | export type ContentTypeOptions = "nosniff"; 32 | export type CrossOriginOpenerPolicy = 33 | | "unsafe-none" 34 | | "same-origin-allow-popups" 35 | | "same-origin"; 36 | export type XSSProtection = "0" | "1" | "1; mode=block" | `1; report=${string}`; 37 | 38 | type BaseSecureHeaders = { 39 | /** 40 | * @description The X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a ``, `