├── .nvmrc ├── .prettierignore ├── README.md ├── .gitignore ├── examples ├── react-router-v7 │ ├── app │ │ ├── app.css │ │ ├── routes.ts │ │ ├── entry.client.tsx │ │ ├── routes │ │ │ └── home.tsx │ │ ├── root.tsx │ │ ├── entry.server.tsx │ │ └── welcome │ │ │ ├── welcome.tsx │ │ │ ├── logo-dark.svg │ │ │ └── logo-light.svg │ ├── .gitignore │ ├── prettier.config.js │ ├── public │ │ └── favicon.ico │ ├── react-router.config.ts │ ├── vite.config.ts │ ├── Dockerfile │ ├── tsconfig.json │ ├── Dockerfile.bun │ ├── Dockerfile.pnpm │ ├── package.json │ └── README.md ├── vite │ ├── .gitignore │ ├── app │ │ ├── root.css │ │ ├── routes │ │ │ ├── page.$id.tsx │ │ │ └── _index.tsx │ │ ├── entry.client.tsx │ │ ├── root.tsx │ │ └── entry.server.tsx │ ├── public │ │ └── favicon.ico │ ├── vite.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── .eslintrc.cjs └── basic │ ├── .gitignore │ ├── public │ └── favicon.ico │ ├── remix.env.d.ts │ ├── .eslintrc.cjs │ ├── app │ ├── styles │ │ └── app.css │ ├── root.tsx │ ├── routes │ │ └── _index.tsx │ └── entry.server.tsx │ ├── remix.config.js │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── pnpm-workspace.yaml ├── prettier.config.js ├── .changeset ├── config.json └── README.md ├── packages └── http-helmet │ ├── tsup.config.ts │ ├── src │ ├── react.tsx │ ├── index.ts │ ├── rules │ │ ├── strict-transport-security.ts │ │ ├── permissions.ts │ │ └── content-security-policy.ts │ ├── utils.ts │ └── helmet.ts │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ ├── CHANGELOG.md │ └── __tests__ │ └── index.test.ts ├── .github ├── dependabot.yml └── workflows │ ├── release-preview.yml │ ├── test.yml │ ├── release-experimental.yml │ └── release.yml ├── scripts ├── publish.js ├── remove-prerelease-changelogs.js └── version.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/http-helmet/README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .ds_store 3 | node_modules 4 | -------------------------------------------------------------------------------- /examples/react-router-v7/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /examples/vite/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | export default {}; 3 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /examples/react-router-v7/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.react-router 3 | /.cache 4 | /build 5 | .env 6 | -------------------------------------------------------------------------------- /examples/vite/app/root.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: system-ui, sans-serif; 3 | line-height: 1.8; 4 | } 5 | -------------------------------------------------------------------------------- /examples/vite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcansh/http-helmet/HEAD/examples/vite/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcansh/http-helmet/HEAD/examples/basic/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /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/HEAD/examples/react-router-v7/public/favicon.ico -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /packages/http-helmet/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | import pkgJson from "./package.json"; 3 | 4 | let external = Object.keys(pkgJson.dependencies || {}); 5 | 6 | export default defineConfig(() => { 7 | return { 8 | shims: true, 9 | entry: ["src/index.ts", "src/react.tsx"], 10 | sourcemap: true, 11 | external, 12 | tsconfig: "./tsconfig.json", 13 | dts: true, 14 | format: ["cjs", "esm"], 15 | platform: "neutral", 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/http-helmet/src/react.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | let NonceContext = React.createContext(undefined); 4 | 5 | type NonceProviderProps = { 6 | nonce: string; 7 | children: React.ReactNode; 8 | }; 9 | 10 | export function NonceProvider({ nonce, children }: NonceProviderProps) { 11 | return ( 12 | {children} 13 | ); 14 | } 15 | 16 | export function useNonce(): string | undefined { 17 | return React.useContext(NonceContext); 18 | } 19 | -------------------------------------------------------------------------------- /packages/http-helmet/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "ES2022", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | 11 | "isolatedModules": true, 12 | "moduleResolution": "Bundler", 13 | "moduleDetection": "force", 14 | "resolveJsonModule": true, 15 | "jsx": "react", 16 | 17 | "outDir": "dist", 18 | "emitDeclarationOnly": true, 19 | "declaration": true, 20 | "declarationMap": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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/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/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"] -------------------------------------------------------------------------------- /packages/http-helmet/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | HASH, 3 | NONCE, 4 | NONE, 5 | REPORT_SAMPLE, 6 | SELF, 7 | STRICT_DYNAMIC, 8 | UNSAFE_EVAL, 9 | UNSAFE_HASHES, 10 | UNSAFE_INLINE, 11 | WASM_UNSAFE_EVAL, 12 | mergeHeaders, 13 | createNonce, 14 | } from "./utils"; 15 | 16 | export { 17 | createContentSecurityPolicy, 18 | createPermissionsPolicy, 19 | createSecureHeaders, 20 | createStrictTransportSecurity, 21 | } from "./helmet"; 22 | 23 | export type { 24 | ContentSecurityPolicy, 25 | ContentTypeOptions, 26 | CreateSecureHeaders, 27 | CrossOriginOpenerPolicy, 28 | DNSPrefetchControl, 29 | FrameOptions, 30 | PermissionsPolicy, 31 | ReferrerPolicy, 32 | StrictTransportSecurity, 33 | XSSProtection, 34 | } from "./helmet"; 35 | -------------------------------------------------------------------------------- /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/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/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/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/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.8", 15 | "@remix-run/react": "^2.16.8", 16 | "@remix-run/serve": "^2.16.8", 17 | "isbot": "^5.1.28", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0" 20 | }, 21 | "devDependencies": { 22 | "@remix-run/dev": "^2.16.8", 23 | "@remix-run/eslint-config": "^2.16.8", 24 | "@types/react": "^19.1.6", 25 | "@types/react-dom": "^19.1.5", 26 | "eslint": "^8.57.0", 27 | "typescript": "^5.8.2" 28 | }, 29 | "engines": { 30 | "node": ">=18" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/http-helmet/src/rules/strict-transport-security.ts: -------------------------------------------------------------------------------- 1 | export type StrictTransportSecurity = { 2 | maxAge: number; 3 | includeSubDomains?: boolean; 4 | preload?: boolean; 5 | }; 6 | 7 | export function createStrictTransportSecurity( 8 | options: StrictTransportSecurity, 9 | ): string; 10 | export function createStrictTransportSecurity(options: true): string; 11 | export function createStrictTransportSecurity( 12 | options: StrictTransportSecurity | true, 13 | ): string; 14 | export function createStrictTransportSecurity( 15 | options: StrictTransportSecurity | true, 16 | ): string { 17 | if (options === true) { 18 | options = { maxAge: 15552000, includeSubDomains: true, preload: true }; 19 | } 20 | 21 | let header = `max-age=${options.maxAge}`; 22 | 23 | if (options.includeSubDomains) { 24 | header += "; includeSubDomains"; 25 | } 26 | 27 | if (options.preload) { 28 | header += "; preload"; 29 | } 30 | 31 | return header; 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.6.1", 14 | "@react-router/serve": "^7.6.1", 15 | "isbot": "^5.1.28", 16 | "react": "^19.1.0", 17 | "react-dom": "^19.1.0", 18 | "react-router": "^7.6.1" 19 | }, 20 | "devDependencies": { 21 | "@react-router/dev": "^7.6.1", 22 | "@tailwindcss/vite": "^4.0.12", 23 | "@types/node": "^22", 24 | "@types/react": "^19.1.6", 25 | "@types/react-dom": "^19.1.5", 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.3.5", 31 | "vite-tsconfig-paths": "^5.1.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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/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/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/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/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.8", 16 | "@remix-run/react": "2.16.8", 17 | "@remix-run/serve": "2.16.8", 18 | "isbot": "^5.1.28", 19 | "react": "^19.1.0", 20 | "react-dom": "^19.1.0" 21 | }, 22 | "devDependencies": { 23 | "@remix-run/dev": "2.16.8", 24 | "@types/react": "^19.1.6", 25 | "@types/react-dom": "^19.1.5", 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.3.5", 36 | "vite-tsconfig-paths": "^5.1.4" 37 | }, 38 | "engines": { 39 | "node": ">=18.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from "node:child_process"; 4 | import semver from "semver"; 5 | import { globSync } from "glob"; 6 | 7 | let packages = globSync("packages/*", { absolute: true }); 8 | 9 | function getTaggedVersion() { 10 | let output = execSync("git tag --list --points-at HEAD").toString().trim(); 11 | return output.replace(/^v/g, ""); 12 | } 13 | 14 | /** 15 | * @param {string} dir 16 | * @param {string} tag 17 | */ 18 | function publish(dir, tag) { 19 | execSync(`npm publish --access public --tag ${tag} ${dir}`, { 20 | stdio: "inherit", 21 | }); 22 | } 23 | 24 | async function run() { 25 | // Make sure there's a current tag 26 | let taggedVersion = getTaggedVersion(); 27 | if (taggedVersion === "") { 28 | console.error("Missing release version. Run the version script first."); 29 | process.exit(1); 30 | } 31 | 32 | let prerelease = semver.prerelease(taggedVersion); 33 | let prereleaseTag = prerelease ? String(prerelease[0]) : undefined; 34 | let tag = prereleaseTag 35 | ? prereleaseTag.includes("nightly") 36 | ? "nightly" 37 | : prereleaseTag.includes("experimental") 38 | ? "experimental" 39 | : prereleaseTag 40 | : "latest"; 41 | 42 | for (let name of packages) { 43 | publish(name, tag); 44 | } 45 | } 46 | 47 | run().then( 48 | () => { 49 | process.exit(0); 50 | }, 51 | (error) => { 52 | console.error(error); 53 | process.exit(1); 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.41.0" 49 | }, 50 | "devDependencies": { 51 | "@types/react": "^19.1.6", 52 | "@types/react-dom": "^19.1.5", 53 | "content-security-policy-parser": "^0.6.0", 54 | "react": "^19.1.0", 55 | "react-dom": "^19.1.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/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Helmet 2 | 3 | > [!NOTE] 4 | > This repo is now part of [mcansh/packages](https://github.com/mcansh/packages/tree/main/packages/http-helmet) 5 | 6 | easily add CSP and other security headers to your web application. 7 | 8 | ## Install 9 | 10 | ```sh 11 | # npm 12 | npm i @mcansh/http-helmet 13 | ``` 14 | 15 | ## Usage 16 | 17 | basic example using [`@mjackson/node-fetch-server`](https://github.com/mjackson/remix-the-web/tree/main/packages/node-fetch-server) 18 | 19 | ```js 20 | import * as http from "node:http"; 21 | import { createRequestListener } from "@mjackson/node-fetch-server"; 22 | import { createNonce, createSecureHeaders } from "@mcansh/http-helmet"; 23 | 24 | let html = String.raw; 25 | 26 | let handler = (request) => { 27 | let nonce = createNonce(); 28 | let headers = createSecureHeaders({ 29 | "Content-Security-Policy": { 30 | defaultSrc: ["'self'"], 31 | scriptSrc: ["'self'", `'nonce-${nonce}'`], 32 | }, 33 | }); 34 | 35 | headers.append("content-type", "text/html"); 36 | 37 | return new Response( 38 | html` 39 | 40 | 41 | 42 | 43 | 47 | Hello World 48 | 49 | 50 |

Hello World

51 | 52 | 55 | 56 | 59 | 60 | 61 | `, 62 | { headers }, 63 | ); 64 | }; 65 | 66 | let server = http.createServer(createRequestListener(handler)); 67 | 68 | server.listen(3000); 69 | 70 | console.log("✅ app ready: http://localhost:3000"); 71 | ``` 72 | -------------------------------------------------------------------------------- /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.15.21", 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.12", 42 | "semver": "^7.7.1", 43 | "tsup": "^8.4.0", 44 | "type-fest": "^4.41.0", 45 | "typescript": "^5.8.2", 46 | "vitest": "^3.1.4" 47 | }, 48 | "packageManager": "pnpm@10.6.1+sha512.40ee09af407fa9fbb5fbfb8e1cb40fbb74c0af0c3e10e9224d7b53c7658528615b2c92450e74cfad91e3a2dcafe3ce4050d80bda71d757756d2ce2b66213e9a3" 49 | } 50 | -------------------------------------------------------------------------------- /packages/http-helmet/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function isQuoted(value: string): boolean { 2 | return /^".*"$/.test(value); 3 | } 4 | 5 | type Algorithm = "sha256" | "sha384" | "sha512"; 6 | 7 | type HashSource = `'${Algorithm}-${string}'`; 8 | 9 | export type QuotedSource = 10 | | "'self'" 11 | | "'none'" 12 | | "'unsafe-inline'" 13 | | "'unsafe-eval'" 14 | | "'wasm-unsafe-eval'" 15 | | "'unsafe-hashes'" 16 | | `'nonce-${string}'` 17 | | "'strict-dynamic'" 18 | | "'report-sample'" 19 | | HashSource; 20 | 21 | export let SELF = "'self'" as const; 22 | export let NONE = "'none'" as const; 23 | export let UNSAFE_INLINE = "'unsafe-inline'" as const; 24 | export let UNSAFE_EVAL = "'unsafe-eval'" as const; 25 | export let WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'" as const; 26 | export let UNSAFE_HASHES = "'unsafe-hashes'" as const; 27 | export let STRICT_DYNAMIC = "'strict-dynamic'" as const; 28 | export let REPORT_SAMPLE = "'report-sample'" as const; 29 | export function NONCE(nonce: string): `'nonce-${string}'` { 30 | return `'nonce-${nonce}'`; 31 | } 32 | export function HASH(algorithm: Algorithm, hash: string): HashSource { 33 | return `'${algorithm}-${hash}'`; 34 | } 35 | 36 | function isObject(value: unknown) { 37 | return value !== null && typeof value === "object"; 38 | } 39 | 40 | export function mergeHeaders(...sources: HeadersInit[]): Headers { 41 | let result = new Headers(); 42 | 43 | for (let source of sources) { 44 | if (!isObject(source)) { 45 | throw new TypeError("All arguments must be of type object"); 46 | } 47 | 48 | let headers = new Headers(source); 49 | 50 | for (let [key, value] of headers.entries()) { 51 | if (value === undefined || value === "undefined") { 52 | result.delete(key); 53 | } else if (key === "set-cookie") { 54 | result.append(key, value); 55 | } else { 56 | result.set(key, value); 57 | } 58 | } 59 | } 60 | 61 | return new Headers(result); 62 | } 63 | 64 | export function createNonce(): string { 65 | return Buffer.from(crypto.randomUUID()).toString("base64"); 66 | } 67 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/http-helmet/src/rules/permissions.ts: -------------------------------------------------------------------------------- 1 | import type { LiteralUnion } from "type-fest"; 2 | import { kebabCase } from "change-case"; 3 | 4 | type KnownPermissions = LiteralUnion< 5 | | "accelerometer" 6 | | "ambientLightSensor" 7 | | "autoplay" 8 | | "battery" 9 | | "camera" 10 | | "displayCapture" 11 | | "documentDomain" 12 | | "encryptedMedia" 13 | | "executionWhileNotRendered" 14 | | "executionWhileOutOfViewport" 15 | | "fullscreen" 16 | | "gamepad" 17 | | "geolocation" 18 | | "gyroscope" 19 | | "interestCohort" 20 | | "layoutAnimations" 21 | | "legacyImageFormats" 22 | | "magnetometer" 23 | | "microphone" 24 | | "midi" 25 | | "navigationOverride" 26 | | "oversizedImages" 27 | | "payment" 28 | | "pictureInPicture" 29 | | "publickeyCredentialsGet" 30 | | "speakerSelection" 31 | | "syncXhr" 32 | | "unoptimizedImages" 33 | | "unsizedMedia" 34 | | "usb" 35 | | "screenWakeLock" 36 | | "webShare" 37 | | "xrSpatialTracking", 38 | string 39 | >; 40 | 41 | export type PermissionsPolicy = { 42 | [key in KnownPermissions]?: Array; 43 | }; 44 | 45 | const reservedPermissionKeywords = new Set(["self", "*"]); 46 | 47 | export function createPermissionsPolicy(features: PermissionsPolicy): string { 48 | return Object.entries(features) 49 | .map(([key, featureValues]) => { 50 | if (!Array.isArray(featureValues)) { 51 | throw new Error( 52 | `The value of the "${key}" feature must be array of strings.`, 53 | ); 54 | } 55 | 56 | const allowedValuesSeen: Set = new Set(); 57 | 58 | featureValues.forEach((allowedValue) => { 59 | if (typeof allowedValue !== "string") { 60 | throw new Error( 61 | `[createPermissionsPolicy]: The value of "${key}" contains a non-string, which is not supported.`, 62 | ); 63 | } 64 | 65 | if (allowedValuesSeen.has(allowedValue)) { 66 | throw new Error( 67 | `[createPermissionsPolicy]: The value of "${key}" contains duplicates, which it shouldn't.`, 68 | ); 69 | } 70 | 71 | if (allowedValue === "'self'") { 72 | throw new Error( 73 | `[createPermissionsPolicy]: self must not be quoted for "${key}".`, 74 | ); 75 | } 76 | 77 | allowedValuesSeen.add(allowedValue); 78 | }); 79 | 80 | if (featureValues.length > 1 && allowedValuesSeen.has("*")) { 81 | throw new Error( 82 | `[createPermissionsPolicy]: The value of the "${key}" feature cannot contain * and other values.`, 83 | ); 84 | } 85 | 86 | const featureKeyDashed = kebabCase(key); 87 | const featureValuesUnion = featureValues 88 | .map((value) => { 89 | if (reservedPermissionKeywords.has(value)) { 90 | return value; 91 | } 92 | 93 | return `"${value}"`; 94 | }) 95 | .join(" "); 96 | 97 | if (featureValuesUnion === "*") { 98 | return `${featureKeyDashed}=${featureValuesUnion}`; 99 | } 100 | 101 | return `${featureKeyDashed}=(${featureValuesUnion})`; 102 | }) 103 | .join(", "); 104 | } 105 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/remove-prerelease-changelogs.js: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | import path from "node:path"; 3 | import * as url from "node:url"; 4 | import { getPackagesSync } from "@manypkg/get-packages"; 5 | 6 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); 7 | const rootDir = path.join(__dirname, ".."); 8 | 9 | const DRY_RUN = false; 10 | // pre-release headings look like: "1.15.0-pre.2" 11 | const PRE_RELEASE_HEADING_REGEXP = /^\d+\.\d+\.\d+-pre\.\d+$/i; 12 | // stable headings look like: "1.15.0" 13 | const STABLE_HEADING_REGEXP = /^\d+\.\d+\.\d+$/i; 14 | 15 | main(); 16 | 17 | async function main() { 18 | if (isPrereleaseMode()) { 19 | console.log("🚫 Skipping changelog removal in prerelease mode"); 20 | return; 21 | } 22 | await removePreReleaseChangelogs(); 23 | console.log("✅ Removed pre-release changelogs"); 24 | } 25 | 26 | async function removePreReleaseChangelogs() { 27 | let allPackages = getPackagesSync(rootDir).packages; 28 | 29 | /** @type {Promise[]} */ 30 | let processes = []; 31 | for (let pkg of allPackages) { 32 | let changelogPath = path.join(pkg.dir, "CHANGELOG.md"); 33 | if (!fs.existsSync(changelogPath)) { 34 | continue; 35 | } 36 | let changelogFileContents = fs.readFileSync(changelogPath, "utf-8"); 37 | processes.push( 38 | (async () => { 39 | let preReleaseHeadingIndex = findHeadingLineIndex( 40 | changelogFileContents, 41 | { 42 | level: 2, 43 | startAtIndex: 0, 44 | matcher: PRE_RELEASE_HEADING_REGEXP, 45 | }, 46 | ); 47 | 48 | while (preReleaseHeadingIndex !== -1) { 49 | let nextStableHeadingIndex = findHeadingLineIndex( 50 | changelogFileContents, 51 | { 52 | level: 2, 53 | startAtIndex: preReleaseHeadingIndex + 1, 54 | matcher: STABLE_HEADING_REGEXP, 55 | }, 56 | ); 57 | 58 | // remove all lines between the pre-release heading and the next stable 59 | // heading 60 | changelogFileContents = removeLines(changelogFileContents, { 61 | start: preReleaseHeadingIndex, 62 | end: nextStableHeadingIndex === -1 ? "max" : nextStableHeadingIndex, 63 | }); 64 | 65 | // find the next pre-release heading 66 | preReleaseHeadingIndex = findHeadingLineIndex(changelogFileContents, { 67 | level: 2, 68 | startAtIndex: 0, 69 | matcher: PRE_RELEASE_HEADING_REGEXP, 70 | }); 71 | } 72 | 73 | if (DRY_RUN) { 74 | console.log("FILE CONTENTS:\n\n" + changelogFileContents); 75 | } else { 76 | await fs.promises.writeFile( 77 | changelogPath, 78 | changelogFileContents, 79 | "utf-8", 80 | ); 81 | } 82 | })(), 83 | ); 84 | } 85 | return Promise.all(processes); 86 | } 87 | 88 | function isPrereleaseMode() { 89 | try { 90 | let prereleaseFilePath = path.join(rootDir, ".changeset", "pre.json"); 91 | return fs.existsSync(prereleaseFilePath); 92 | } catch (err) { 93 | return false; 94 | } 95 | } 96 | 97 | /** 98 | * @param {string} markdownContents 99 | * @param {{ level: number; startAtIndex: number; matcher: RegExp }} opts 100 | */ 101 | function findHeadingLineIndex( 102 | markdownContents, 103 | { level, startAtIndex, matcher }, 104 | ) { 105 | let index = markdownContents.split("\n").findIndex((line, i) => { 106 | if (i < startAtIndex || !line.startsWith(`${"#".repeat(level)} `)) 107 | return false; 108 | let headingContents = line.slice(level + 1).trim(); 109 | return matcher.test(headingContents); 110 | }); 111 | return index; 112 | } 113 | 114 | /** 115 | * @param {string} markdownContents 116 | * @param {{ start: number; end: number | 'max' }} param1 117 | */ 118 | function removeLines(markdownContents, { start, end }) { 119 | let lines = markdownContents.split("\n"); 120 | lines.splice(start, end === "max" ? lines.length - start : end - start); 121 | return lines.join("\n"); 122 | } 123 | -------------------------------------------------------------------------------- /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/src/rules/content-security-policy.ts: -------------------------------------------------------------------------------- 1 | import { LiteralUnion, KebabCasedProperties } from "type-fest"; 2 | import { QuotedSource, isQuoted } from "../utils.js"; 3 | import { kebabCase } from "change-case"; 4 | 5 | type CspSetting = Array | undefined>; 6 | 7 | type ContentSecurityPolicyCamel = { 8 | childSrc?: CspSetting; 9 | connectSrc?: CspSetting; 10 | defaultSrc?: CspSetting; 11 | fontSrc?: CspSetting; 12 | frameSrc?: CspSetting; 13 | imgSrc?: CspSetting; 14 | manifestSrc?: CspSetting; 15 | mediaSrc?: CspSetting; 16 | objectSrc?: CspSetting; 17 | prefetchSrc?: CspSetting; 18 | scriptSrc?: CspSetting; 19 | scriptSrcElem?: CspSetting; 20 | scriptSrcAttr?: CspSetting; 21 | styleSrc?: CspSetting; 22 | styleSrcElem?: CspSetting; 23 | styleSrcAttr?: CspSetting; 24 | workerSrc?: CspSetting; 25 | baseUri?: CspSetting; 26 | sandbox?: CspSetting; 27 | formAction?: CspSetting; 28 | frameAncestors?: CspSetting; 29 | navigateTo?: CspSetting; 30 | reportUri?: CspSetting; 31 | reportTo?: CspSetting; 32 | requireSriFor?: CspSetting; 33 | requireTrustedTypesFor?: CspSetting; 34 | trustedTypes?: CspSetting; 35 | upgradeInsecureRequests?: boolean; 36 | }; 37 | 38 | type ContentSecurityPolicyKebab = 39 | KebabCasedProperties; 40 | 41 | type ContentSecurityPolicy = 42 | | ContentSecurityPolicyCamel 43 | | ContentSecurityPolicyKebab; 44 | 45 | export type PublicContentSecurityPolicy = Parameters< 46 | typeof createContentSecurityPolicy 47 | >[0]; 48 | 49 | let reservedCSPKeywords = new Set([ 50 | "self", 51 | "none", 52 | "unsafe-inline", 53 | "unsafe-eval", 54 | ]); 55 | 56 | export function createContentSecurityPolicy( 57 | settings: ContentSecurityPolicyCamel, 58 | ): string; 59 | export function createContentSecurityPolicy( 60 | settings: ContentSecurityPolicyKebab, 61 | ): string; 62 | export function createContentSecurityPolicy( 63 | settings: ContentSecurityPolicy, 64 | ): string; 65 | export function createContentSecurityPolicy( 66 | settings: ContentSecurityPolicy, 67 | ): string { 68 | let { "upgrade-insecure-requests": upgradeInsecureRequests, ...rest } = 69 | Object.entries(settings).reduce( 70 | (acc, [key, value]) => { 71 | let kebab = kebabCase(key) as keyof ContentSecurityPolicyKebab; 72 | if (acc[kebab]) { 73 | throw new Error( 74 | `[createContentSecurityPolicy]: The key "${key}" was specified in camelCase and kebab-case.`, 75 | ); 76 | } 77 | // @ts-expect-error - hush 78 | acc[kebab] = value; 79 | return acc; 80 | }, 81 | {}, 82 | ); 83 | 84 | let policy: Array = []; 85 | 86 | if (upgradeInsecureRequests) { 87 | policy.push("upgrade-insecure-requests"); 88 | } 89 | 90 | for (let [key, values] of Object.entries(rest)) { 91 | let allowedValuesSeen: Set = new Set(); 92 | 93 | if (!Array.isArray(values)) { 94 | throw new Error( 95 | `[createContentSecurityPolicy]: The value of the "${key}" must be array of strings.`, 96 | ); 97 | } 98 | 99 | let definedValues = values.filter( 100 | (v): v is string => typeof v !== "undefined", 101 | ); 102 | 103 | definedValues.forEach((allowedValue) => { 104 | if (typeof allowedValue !== "string") { 105 | throw new Error( 106 | `[createContentSecurityPolicy]: The value of the "${key}" contains a non-string, which is not supported.`, 107 | ); 108 | } 109 | 110 | if (allowedValuesSeen.has(allowedValue)) { 111 | throw new Error( 112 | `[createContentSecurityPolicy]: The value of the "${key}" contains duplicates, which it shouldn't.`, 113 | ); 114 | } 115 | 116 | if (reservedCSPKeywords.has(allowedValue) && !isQuoted(allowedValue)) { 117 | throw new Error( 118 | `[createContentSecurityPolicy]: reserved keyword ${allowedValue} must be quoted.`, 119 | ); 120 | } 121 | 122 | allowedValuesSeen.add(allowedValue); 123 | }); 124 | 125 | if (definedValues.length === 0) { 126 | throw new Error( 127 | `[createContentSecurityPolicy]: key "${key}" has no defined options`, 128 | ); 129 | } 130 | 131 | policy.push(`${key} ${definedValues.join(" ")}`); 132 | } 133 | 134 | return policy.join("; "); 135 | } 136 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { execSync } from "node:child_process"; 3 | import semver from "semver"; 4 | import jsonfile from "jsonfile"; 5 | import chalk from "chalk"; 6 | import Confirm from "prompt-confirm"; 7 | 8 | let packages = ["http-helmet"]; 9 | 10 | let rootDir = path.join(import.meta.dirname, ".."); 11 | 12 | run(process.argv.slice(2)).then( 13 | () => { 14 | process.exit(0); 15 | }, 16 | (error) => { 17 | console.error(error); 18 | process.exit(1); 19 | }, 20 | ); 21 | 22 | /** 23 | * @param {string[]} args 24 | */ 25 | async function run(args) { 26 | let givenVersion = args[0]; 27 | let prereleaseId = args[1]; 28 | 29 | ensureCleanWorkingDirectory(); 30 | 31 | // Get the next version number 32 | let currentVersion = await getPackageVersion("http-helmet"); 33 | let nextVersion = semver.valid(givenVersion); 34 | if (nextVersion == null) { 35 | nextVersion = getNextVersion(currentVersion, givenVersion, prereleaseId); 36 | } 37 | 38 | // Confirm the next version number 39 | if (prereleaseId !== "--skip-prompt") { 40 | let answer = await prompt( 41 | `Are you sure you want to bump version ${currentVersion} to ${nextVersion}? [Yn] `, 42 | ); 43 | if (answer === false) return 0; 44 | } 45 | 46 | await incrementVersion(nextVersion); 47 | } 48 | 49 | /** 50 | * @param {string} nextVersion 51 | */ 52 | async function incrementVersion(nextVersion) { 53 | // Update version numbers in package.json for all packages 54 | for (let name of packages) { 55 | await updateVersion(`${name}`, nextVersion); 56 | } 57 | 58 | // Commit and tag 59 | execSync(`git commit --all --message="Version ${nextVersion}"`); 60 | execSync(`git tag -a -m "Version ${nextVersion}" v${nextVersion}`); 61 | console.log(chalk.green(` Committed and tagged version ${nextVersion}`)); 62 | } 63 | 64 | /** 65 | * @param {string} packageName 66 | * @param {(json: import('type-fest').PackageJson) => any} transform 67 | */ 68 | async function updatePackageConfig(packageName, transform) { 69 | let file = packageJson(packageName, "packages"); 70 | try { 71 | let json = await jsonfile.readFile(file); 72 | if (!json) { 73 | console.log(`No package.json found for ${packageName}; skipping`); 74 | return; 75 | } 76 | transform(json); 77 | await jsonfile.writeFile(file, json, { spaces: 2 }); 78 | } catch { 79 | return; 80 | } 81 | } 82 | 83 | /** 84 | * @param {string} packageName 85 | * @param {string} nextVersion 86 | * @param {string} [successMessage] 87 | */ 88 | async function updateVersion(packageName, nextVersion, successMessage) { 89 | await updatePackageConfig(packageName, (config) => { 90 | config.version = nextVersion; 91 | for (let pkg of packages) { 92 | let fullPackageName = `@mcansh/${pkg}`; 93 | if (config.dependencies?.[fullPackageName]) { 94 | config.dependencies[fullPackageName] = nextVersion; 95 | } 96 | if (config.devDependencies?.[fullPackageName]) { 97 | config.devDependencies[fullPackageName] = nextVersion; 98 | } 99 | if (config.peerDependencies?.[fullPackageName]) { 100 | let isRelaxedPeerDep = 101 | config.peerDependencies[fullPackageName]?.startsWith("^"); 102 | config.peerDependencies[fullPackageName] = `${ 103 | isRelaxedPeerDep ? "^" : "" 104 | }${nextVersion}`; 105 | } 106 | } 107 | }); 108 | let logName = `@mcansh/${packageName.slice(6)}`; 109 | console.log( 110 | chalk.green( 111 | ` ${ 112 | successMessage || 113 | `Updated ${chalk.bold(logName)} to version ${chalk.bold(nextVersion)}` 114 | }`, 115 | ), 116 | ); 117 | } 118 | 119 | /** 120 | * @param {string|undefined} currentVersion 121 | * @param {string} givenVersion 122 | * @param {string} [prereleaseId] 123 | * @returns 124 | */ 125 | function getNextVersion(currentVersion, givenVersion, prereleaseId = "pre") { 126 | if (givenVersion == null) { 127 | console.error("Missing next version. Usage: node version.js [nextVersion]"); 128 | process.exit(1); 129 | } 130 | 131 | let nextVersion; 132 | if (givenVersion === "experimental") { 133 | let hash = execSync(`git rev-parse --short HEAD`).toString().trim(); 134 | nextVersion = `0.0.0-experimental-${hash}`; 135 | } else { 136 | // @ts-ignore 137 | nextVersion = semver.inc(currentVersion, givenVersion, prereleaseId); 138 | } 139 | 140 | if (nextVersion == null) { 141 | console.error(`Invalid version specifier: ${givenVersion}`); 142 | process.exit(1); 143 | } 144 | 145 | return nextVersion; 146 | } 147 | 148 | /** 149 | * @returns {void} 150 | */ 151 | function ensureCleanWorkingDirectory() { 152 | let status = execSync(`git status --porcelain`).toString().trim(); 153 | let lines = status.split("\n"); 154 | if (!lines.every((line) => line === "" || line.startsWith("?"))) { 155 | console.error( 156 | "Working directory is not clean. Please commit or stash your changes.", 157 | ); 158 | process.exit(1); 159 | } 160 | } 161 | 162 | /** 163 | * @param {string} packageName 164 | * @param {string} [directory] 165 | * @returns {string} 166 | */ 167 | function packageJson(packageName, directory = "") { 168 | return path.join(rootDir, directory, packageName, "package.json"); 169 | } 170 | 171 | /** 172 | * @param {string} packageName 173 | * @returns {Promise} 174 | */ 175 | async function getPackageVersion(packageName) { 176 | let file = packageJson(packageName, "packages"); 177 | let json = await jsonfile.readFile(file); 178 | return json.version; 179 | } 180 | 181 | /** 182 | * @param {string} question 183 | * @returns {Promise} 184 | */ 185 | async function prompt(question) { 186 | let confirm = new Confirm(question); 187 | let answer = await confirm.run(); 188 | return answer; 189 | } 190 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ``, `