├── .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 |
15 |
20 |
21 |
22 |
23 |
24 |
25 | What's next?
26 |
27 |
42 |
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 ` `, `