├── .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 |
24 |
--------------------------------------------------------------------------------
/examples/react-router-v7/app/welcome/logo-light.svg:
--------------------------------------------------------------------------------
1 |
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 |

15 |

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 |
68 | ),
69 | },
70 | {
71 | href: "https://rmx.as/discord",
72 | text: "Join Discord",
73 | icon: (
74 |
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 ``, `