From 7654233cbc23fd7748840b3391edd1e138a2842b Mon Sep 17 00:00:00 2001 From: Dallas Hogan Date: Tue, 18 Nov 2025 19:34:06 -0500 Subject: [PATCH 1/4] Create automation for easier to read CSP diffs --- README.md | 32 ++ build.sh | 3 + git/hooks/pre-commit | 16 + package-lock.json | 583 ++++++++++++++++++++++++++++++++++-- package.json | 3 + scripts/_headers.config.ts | 113 +++++++ scripts/generate-headers.ts | 93 ++++++ static/_headers | 13 +- tsconfig.json | 6 +- 9 files changed, 839 insertions(+), 23 deletions(-) create mode 100644 scripts/_headers.config.ts create mode 100644 scripts/generate-headers.ts diff --git a/README.md b/README.md index 52e1bd302..00e6aa99e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,38 @@ hugo server npm run lint ``` +#### HTTP Headers Configuration + +The `static/_headers` file is automatically generated from +`scripts/_headers.config.ts`. **Do not edit `static/_headers` directly** - your +changes will be overwritten. + +##### Making Changes to Headers + +1. Edit `scripts/_headers.config.ts` (the source of truth with readable format + and TypeScript type safety) +2. Generate the headers file: + ```sh + npm run build:headers + ``` +3. Commit both files - a git pre-commit hook will automatically regenerate + `static/_headers` when you modify `scripts/_headers.config.ts` + +##### Automatic Synchronization + +The files stay in sync automatically through: + +- **Pre-commit hook** - Regenerates `_headers` when `_headers.config.ts` changes +- **Build process** - `build.sh` regenerates before deployment +- **Manual generation** - Run `npm run build:headers` anytime + +The TypeScript config format provides: + +- Readable multi-line arrays for CSP directives +- Native TypeScript type safety and IDE support +- Easy-to-review diffs in pull requests +- Zero dependencies for parsing + ## Writing Blog Posts Blog posts are written using diff --git a/build.sh b/build.sh index a49cb708a..9d576953f 100755 --- a/build.sh +++ b/build.sh @@ -8,4 +8,7 @@ if [[ "${BUILD_ENV:-}" == "preview" ]]; then extra_args+=("--buildFuture") fi +# Generate _headers file from TypeScript configuration +npx tsx scripts/generate-headers.ts + hugo --gc --minify -b "$CF_PAGES_URL" "${extra_args[@]}" diff --git a/git/hooks/pre-commit b/git/hooks/pre-commit index a5b5d55b0..77d2e4ff5 100755 --- a/git/hooks/pre-commit +++ b/git/hooks/pre-commit @@ -24,4 +24,20 @@ function timed_run() { fi } +# Regenerate static/_headers from _headers.config.ts if it changed +if git diff --cached --name-only | grep -q "scripts/_headers.config.ts"; then + echo "Detected changes to scripts/_headers.config.ts, regenerating static/_headers..." + npx tsx scripts/generate-headers.ts + git add static/_headers + echo "✅ Updated static/_headers and added to commit" +fi + +# Also regenerate if the generator script itself changed +if git diff --cached --name-only | grep -q "scripts/generate-headers.ts"; then + echo "Detected changes to generator script, regenerating static/_headers..." + npx tsx scripts/generate-headers.ts + git add static/_headers + echo "✅ Updated static/_headers and added to commit" +fi + timed_run "local/precious lint" "local/precious lint --staged" diff --git a/package-lock.json b/package-lock.json index 6ae722f44..c9de9c755 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", "cspell": "^9.3.0", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", @@ -27,6 +28,7 @@ "stylelint-config-sass-guidelines": "^12.1.0", "stylelint-no-unsupported-browser-features": "^8.0.5", "stylelint-order": "^7.0.0", + "tsx": "^4.19.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.3" }, @@ -246,8 +248,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-dart": { "version": "2.3.1", @@ -387,16 +388,14 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -594,8 +593,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -664,7 +662,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -688,7 +685,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -749,6 +745,448 @@ "url": "https://github.com/sponsors/JounQin" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1128,6 +1566,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", @@ -1181,7 +1629,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -1400,7 +1847,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1611,7 +2057,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2671,6 +3116,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "dev": true, @@ -2696,7 +3183,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3157,6 +3643,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "dev": true, @@ -3261,6 +3762,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", @@ -5492,7 +6006,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5566,7 +6079,6 @@ "version": "7.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5769,6 +6281,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "dev": true, @@ -6388,7 +6910,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -6748,6 +7269,26 @@ "typescript": ">=4.8.4" } }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -6835,7 +7376,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6892,6 +7432,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", diff --git a/package.json b/package.json index 94aee1be4..7662379df 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@eslint/compat": "^1.4.1", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", "cspell": "^9.3.0", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", @@ -23,6 +24,7 @@ "stylelint-config-sass-guidelines": "^12.1.0", "stylelint-no-unsupported-browser-features": "^8.0.5", "stylelint-order": "^7.0.0", + "tsx": "^4.19.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.3" }, @@ -33,6 +35,7 @@ "license": "MIT", "name": "blog-site", "scripts": { + "build:headers": "tsx scripts/generate-headers.ts", "fix": "run-p fix:*", "fix:content": "prettier --write '**/*.md' --config './content/.prettierrc.js'", "fix:scripts": "npm run lint:scripts --fix", diff --git a/scripts/_headers.config.ts b/scripts/_headers.config.ts new file mode 100644 index 000000000..b1970c9f4 --- /dev/null +++ b/scripts/_headers.config.ts @@ -0,0 +1,113 @@ +/** + * HTTP Headers Configuration for Cloudflare Pages + * This file is the source of truth for static/_headers generation + * Run: npm run build:headers + * + * Note: This file lives in scripts/ (not static/) to avoid deploying + * TypeScript source files to production. + */ + +interface HeadersConfig { + paths: Array<{ + pattern: string; + headers: Record>; + }>; +} + +const config: HeadersConfig = { + paths: [ + { + pattern: '/*', + headers: { + 'Content-Security-Policy': { + 'connect-src': [ + '\'self\'', + 'https://status.maxmind.com', + 'https://www.maxmind.com', + 'https://api.hubspot.com', + 'https://*.googleapis.com', + 'https://*.google-analytics.com', + 'https://*.analytics.google.com', + 'https://*.googletagmanager.com', + 'https://*.g.doubleclick.net', + 'https://*.google.com', + 'https://forms.hsforms.com', + ], + 'default-src': [ + '\'self\'', + ], + 'font-src': [ + '\'self\'', + 'https://fonts.gstatic.com', + ], + 'form-action': [ + '\'self\'', + ], + 'frame-ancestors': [ + '\'self\'', + ], + 'frame-src': [ + '\'self\'', + 'https://app.hubspot.com', + 'https://www.google.com', + 'https://www.googletagmanager.com', + ], + 'img-src': [ + '\'self\'', + 'data:', + 'https:', + ], + 'object-src': [ + '\'none\'', + ], + 'script-src': [ + '\'self\'', + '\'report-sample\'', + '\'unsafe-inline\'', + 'https://js.hs-scripts.com', + 'https://js.hs-analytics.net', + 'https://js.hs-banner.com', + 'https://js.usemessages.com', + 'https://www.maxmind.com', + 'https://cloud.google.com', + 'https://www.gstatic.com', + 'https://www.googleadservices.com', + 'https://www.google.com', + 'https://*.googletagmanager.com', + 'https://js.hsforms.net', + ], + 'style-src': [ + '\'self\'', + '\'unsafe-inline\'', + 'https://fonts.googleapis.com', + 'https://www.gstatic.com', + ], + }, + 'Feature-Policy': + 'accelerometer \'none\'; autoplay \'none\'; camera \'none\'; ' + + 'encrypted-media \'none\'; fullscreen \'none\'; geolocation \'none\'; ' + + 'gyroscope \'none\'; magnetometer \'none\'; microphone \'none\'; ' + + 'midi \'none\'; payment \'none\'; picture-in-picture \'none\'; ' + + 'usb \'none\'; sync-xhr \'none\'', + 'Permissions-Policy': + 'accelerometer=(), ambient-light-sensor=(), autoplay=(), ' + + 'battery=(), camera=(), display-capture=(), document-domain=(), ' + + 'encrypted-media=(), execution-while-not-rendered=(), ' + + 'execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), ' + + 'geolocation=(), gyroscope=(), hid=(), idle-detection=(), ' + + 'magnetometer=(), microphone=(), midi=(), payment=(), ' + + 'picture-in-picture=(), publickey-credentials-get=(), ' + + 'screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), ' + + 'web-share=(), xr-spatial-tracking=()', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Strict-Transport-Security': + 'max-age=63072000; includeSubDomains; preload', + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + }, + }, + ], +}; + +export default config; diff --git a/scripts/generate-headers.ts b/scripts/generate-headers.ts new file mode 100644 index 000000000..aec9b4309 --- /dev/null +++ b/scripts/generate-headers.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +/** + * Generate static/_headers file from scripts/_headers.config.ts + * + * This script converts a structured TypeScript configuration into the + * Cloudflare Pages _headers format. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import config from './_headers.config.js'; + +interface PathConfig { + pattern: string; + headers: Record>; +} + +/** + * Generate _headers file content from config + */ +function generateHeaders( + config: { paths: PathConfig[] } +): string { + let output = ''; + + // Add warning comment at the top + output += '# ⚠️ DO NOT EDIT THIS FILE DIRECTLY!\n'; + output += '# This file is automatically generated from scripts/_headers.config.ts\n'; + output += '# To make changes, edit scripts/_headers.config.ts and run: npm run build:headers\n'; + output += '#\n'; + output += '# See README.md for more information\n'; + output += '\n'; + + for (const pathConfig of config.paths) { + // Write path pattern + output += pathConfig.pattern + '\n'; + + // Process CSP first if it exists + if (pathConfig.headers['Content-Security-Policy']) { + const csp = pathConfig.headers['Content-Security-Policy'] as Record< + string, + string | string[] + >; + const directives: string[] = []; + + for (const [ + directive, + sources, + ] of Object.entries(csp)) { + if (Array.isArray(sources)) { + directives.push(`${directive} ${sources.join(' ')}`); + } else { + directives.push(`${directive} ${sources}`); + } + } + + output += ` Content-Security-Policy: ${directives.join('; ')}\n`; + } + + // Process other headers + for (const [ + header, + value, + ] of Object.entries(pathConfig.headers)) { + if (header === 'Content-Security-Policy') continue; + + output += ` ${header}: ${value}\n`; + } + } + + return output; +} + +// Main execution +try { + const outputPath = path.join(process.cwd(), 'static', '_headers'); + + // Generate headers file + const headersContent = generateHeaders(config); + + // Write output file + fs.writeFileSync(outputPath, headersContent); + + console.log('✅ Generated static/_headers from scripts/_headers.config.ts'); +} catch (error) { + console.error( + 'Error generating headers file:', + error instanceof Error ? error.message : String(error) + ); + process.exit(1); +} diff --git a/static/_headers b/static/_headers index 6368230ad..19f4e1be9 100644 --- a/static/_headers +++ b/static/_headers @@ -1,10 +1,15 @@ +# ⚠️ DO NOT EDIT THIS FILE DIRECTLY! +# This file is automatically generated from scripts/_headers.config.ts +# To make changes, edit scripts/_headers.config.ts and run: npm run build:headers +# +# See README.md for more information + /* - Content-Security-Policy: connect-src 'self' https://status.maxmind.com https://www.maxmind.com https://api.hubspot.com https://*.googleapis.com https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.com https://forms.hsforms.com; default-src 'self'; font-src 'self' https://fonts.gstatic.com; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://app.hubspot.com https://www.google.com https://www.googletagmanager.com; img-src 'self' data: https:; object-src 'none'; script-src 'self' 'report-sample' 'unsafe-inline' https://js.hs-scripts.com https://js.hs-analytics.net https://js.hs-banner.com https://js.usemessages.com https://www.maxmind.com https://cloud.google.com https://www.gstatic.com https://www.googleadservices.com https://www.google.com https://*.googletagmanager.com https://js.hsforms.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.gstatic.com + Content-Security-Policy: connect-src 'self' https://status.maxmind.com https://www.maxmind.com https://api.hubspot.com https://*.googleapis.com https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.com https://forms.hsforms.com; default-src 'self'; font-src 'self' https://fonts.gstatic.com; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://app.hubspot.com https://www.google.com https://www.googletagmanager.com; img-src 'self' data: https:; object-src 'none'; script-src 'self' 'report-sample' 'unsafe-inline' https://js.hs-scripts.com https://js.hs-analytics.net https://js.hs-banner.com https://js.usemessages.com https://www.maxmind.com https://cloud.google.com https://www.gstatic.com https://www.googleadservices.com https://www.google.com https://*.googletagmanager.com https://js.hsforms.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.gstatic.com Feature-Policy: accelerometer 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; usb 'none'; sync-xhr 'none' Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), web-share=(), xr-spatial-tracking=() Referrer-Policy: strict-origin-when-cross-origin Strict-Transport-Security: max-age=63072000; includeSubDomains; preload X-Content-Type-Options: nosniff - X-Frame-Options: DENY - X-XSS-Protection: 1; mode=block - */ + X-Frame-Options: DENY + X-XSS-Protection: 1; mode=block diff --git a/tsconfig.json b/tsconfig.json index 855611cb5..d2dac5a6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,9 @@ "public", ".cache" ], - "include": ["./assets/js/**/*"] + "include": [ + "./assets/js/**/*", + "./scripts/**/*.ts", + "./static/**/*.ts" + ] } From 5bc3a3a44eba4c9b887d66a2eaba050d037333a4 Mon Sep 17 00:00:00 2001 From: Dallas Hogan Date: Fri, 21 Nov 2025 14:59:04 -0500 Subject: [PATCH 2/4] Clean up automated header generation --- .github/workflows/hugo.yml | 7 + .github/workflows/lint.yml | 2 +- .gitignore | 3 + .nvmrc | 2 +- .stylelintrc.js => .stylelintrc.cjs | 0 README.md | 32 +- bin/_headers.config.ts | 182 ++++++++++ bin/generate-headers.ts | 78 ++++ build.sh | 2 +- eslint.config.mjs | 3 + git/hooks/pre-commit | 16 - package-lock.json | 543 ---------------------------- package.json | 5 +- scripts/_headers.config.ts | 113 ------ scripts/generate-headers.ts | 93 ----- static/_headers | 15 - tsconfig.json | 6 +- 17 files changed, 293 insertions(+), 809 deletions(-) rename .stylelintrc.js => .stylelintrc.cjs (100%) create mode 100644 bin/_headers.config.ts create mode 100644 bin/generate-headers.ts delete mode 100644 scripts/_headers.config.ts delete mode 100644 scripts/generate-headers.ts delete mode 100644 static/_headers diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml index 8a04b9416..ac8567086 100644 --- a/.github/workflows/hugo.yml +++ b/.github/workflows/hugo.yml @@ -19,6 +19,13 @@ jobs: fetch-depth: 0 persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: '22.6' + + - name: Install dependencies + run: npm ci + - name: Setup Hugo uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # 3.0.0 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 27230e3de..cdb1a1d19 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '22' + node-version: '22.6' - name: Get cached dependencies uses: actions/cache@v4 diff --git a/.gitignore b/.gitignore index 0458ffdb8..231cd3649 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ resources/ # Precious install local/ + +# Cloudflare Pages headers +static/_headers diff --git a/.nvmrc b/.nvmrc index 53d1c14db..535e1cca7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22 +v22.6.0 diff --git a/.stylelintrc.js b/.stylelintrc.cjs similarity index 100% rename from .stylelintrc.js rename to .stylelintrc.cjs diff --git a/README.md b/README.md index 00e6aa99e..8654191a8 100644 --- a/README.md +++ b/README.md @@ -101,37 +101,27 @@ hugo server npm run lint ``` -#### HTTP Headers Configuration +#### Cloudflare Pages HTTP Headers Configuration The `static/_headers` file is automatically generated from -`scripts/_headers.config.ts`. **Do not edit `static/_headers` directly** - your -changes will be overwritten. +`bin/_headers.config.ts`. **Do not edit `static/_headers` directly**. ##### Making Changes to Headers -1. Edit `scripts/_headers.config.ts` (the source of truth with readable format - and TypeScript type safety) -2. Generate the headers file: +1. Edit `bin/_headers.config.ts` (the source of truth with readable format and + TypeScript type safety) +2. Test your changes locally: ```sh npm run build:headers ``` -3. Commit both files - a git pre-commit hook will automatically regenerate - `static/_headers` when you modify `scripts/_headers.config.ts` +3. Commit only `bin/_headers.config.ts` - the headers file is regenerated during + deployment -##### Automatic Synchronization +##### Build-Time Generation -The files stay in sync automatically through: - -- **Pre-commit hook** - Regenerates `_headers` when `_headers.config.ts` changes -- **Build process** - `build.sh` regenerates before deployment -- **Manual generation** - Run `npm run build:headers` anytime - -The TypeScript config format provides: - -- Readable multi-line arrays for CSP directives -- Native TypeScript type safety and IDE support -- Easy-to-review diffs in pull requests -- Zero dependencies for parsing +The `static/_headers` file is generated during the build process via `build.sh` +and is not committed to git. For local testing, you can manually generate it +with `npm run build:headers`. ## Writing Blog Posts diff --git a/bin/_headers.config.ts b/bin/_headers.config.ts new file mode 100644 index 000000000..660d0a9fa --- /dev/null +++ b/bin/_headers.config.ts @@ -0,0 +1,182 @@ +/** + * HTTP Headers Configuration for Cloudflare Pages + * This file is the source of truth for static/_headers generation + * Run: npm run build:headers + */ + +interface HeadersConfig { + paths: Array<{ + pattern: string; + headers: Record>; + }>; +} + +const config: HeadersConfig = { + paths: [ + { + pattern: '/*', + headers: { + 'Content-Security-Policy': { + // Allow AJAX/fetch requests to status page, marketing site, HubSpot, and Google services + 'connect-src': [ + '\'self\'', + 'https://status.maxmind.com', + 'https://www.maxmind.com', + // HubSpot API endpoint + // https://legacydocs.hubspot.com/docs/faq/how-do-i-create-a-custom-domain-for-my-forms + 'https://api.hubspot.com', + // HubSpot static assets used by conversations embed + // eslint-disable-next-line max-len + // https://developers.hubspot.com/beta-docs/guides/apps/authentication/working-with-oauth#frequently-asked-questions + 'https://forms.hsforms.com', + 'https://*.googleapis.com', + 'https://*.google-analytics.com', + 'https://*.analytics.google.com', + 'https://*.googletagmanager.com', + 'https://*.g.doubleclick.net', + // Google + // eslint-disable-next-line max-len + // https://developers.google.com/tag-platform/tag-manager/csp#google_analytics_4_google_analytics + 'https://*.google.com', + ], + 'default-src': [ + '\'self\'', + ], + // Google Fonts and Vertex search (indirectly loaded when setting up the searchbox) + 'font-src': [ + '\'self\'', + 'https://fonts.gstatic.com', + ], + 'form-action': [ + '\'self\'', + ], + 'frame-ancestors': [ + '\'self\'', + ], + // HubSpot calls-to-action (pop-ups) and chatflows + // eslint-disable-next-line max-len + // https://knowledge.hubspot.com/website-pages/use-hubspot-content-on-external-sites#calls-to-action + // Google Vertex search + 'frame-src': [ + '\'self\'', + 'https://app.hubspot.com', + 'https://www.google.com', + 'https://www.googletagmanager.com', + ], + 'img-src': [ + '\'self\'', + 'data:', + 'https:', + ], + 'object-src': [ + '\'none\'', + ], + 'script-src': [ + '\'self\'', + '\'report-sample\'', + '\'unsafe-inline\'', + // HubSpot tracking code + // eslint-disable-next-line max-len + // https://developers.hubspot.com/beta-docs/guides/api/tracking-code-api/tracking-code-quickstart-guide#frequently-asked-questions + 'https://js.hs-scripts.com', + // HubSpot analytics + // https://knowledge.hubspot.com/reports/install-the-hubspot-tracking-code + 'https://js.hs-analytics.net', + // HubSpot cookie banner + // https://knowledge.hubspot.com/privacy-and-consent/add-a-cookie-banner-to-your-website + 'https://js.hs-banner.com', + // HubSpot conversations (live chat widget, chat flow) + // https://knowledge.hubspot.com/chatflows/install-the-hubspot-tracking-code-for-chat + 'https://js.usemessages.com', + // HubSpot form widgets + // https://legacydocs.hubspot.com/docs/methods/forms/advanced_form_options + 'https://js.hsforms.net', + 'https://www.maxmind.com', + // Google + // eslint-disable-next-line max-len + // https://developers.google.com/tag-platform/tag-manager/csp#google_analytics_4_google_analytics + 'https://cloud.google.com', + 'https://www.gstatic.com', + 'https://www.googleadservices.com', + 'https://www.google.com', + 'https://*.googletagmanager.com', + ], + // Google Fonts API and Vertex search + // Google static assets + 'style-src': [ + '\'self\'', + '\'unsafe-inline\'', + 'https://fonts.googleapis.com', + 'https://www.gstatic.com', + ], + }, + 'Feature-Policy': [ + 'accelerometer \'none\'', + 'autoplay \'none\'', + 'camera \'none\'', + 'encrypted-media \'none\'', + 'fullscreen \'none\'', + 'geolocation \'none\'', + 'gyroscope \'none\'', + 'magnetometer \'none\'', + 'microphone \'none\'', + 'midi \'none\'', + 'payment \'none\'', + 'picture-in-picture \'none\'', + 'usb \'none\'', + 'sync-xhr \'none\'', + ], + 'Permissions-Policy': [ + 'accelerometer=()', + 'ambient-light-sensor=()', + 'autoplay=()', + 'battery=()', + 'camera=()', + 'display-capture=()', + 'document-domain=()', + 'encrypted-media=()', + 'execution-while-not-rendered=()', + 'execution-while-out-of-viewport=()', + 'fullscreen=()', + 'gamepad=()', + 'geolocation=()', + 'gyroscope=()', + 'hid=()', + 'idle-detection=()', + 'magnetometer=()', + 'microphone=()', + 'midi=()', + 'payment=()', + 'picture-in-picture=()', + 'publickey-credentials-get=()', + 'screen-wake-lock=()', + 'serial=()', + 'speaker-selection=()', + 'usb=()', + 'web-share=()', + 'xr-spatial-tracking=()', + ], + 'Referrer-Policy': [ +'strict-origin-when-cross-origin', +], + 'Strict-Transport-Security': [ + 'max-age=63072000', + 'includeSubDomains', + 'preload', + ], + 'X-Content-Type-Options': [ +'nosniff', +], + 'X-Frame-Options': [ +'DENY', +], + 'X-XSS-Protection': [ +'1', +'mode=block', +], + }, + }, + ], +}; + +export default config; diff --git a/bin/generate-headers.ts b/bin/generate-headers.ts new file mode 100644 index 000000000..da2df0f50 --- /dev/null +++ b/bin/generate-headers.ts @@ -0,0 +1,78 @@ +/** + * Generate static/_headers file from bin/_headers.config.ts + * + * This script converts a structured TypeScript configuration into the + * Cloudflare Pages _headers format. + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import config from './_headers.config.ts'; + +interface PathConfig { + pattern: string; + headers: Record>; +} + +/** + * Generate _headers file content from config + */ +function generateHeaders(config: { paths: PathConfig[] }): string { + let output = ''; + + // Add warning comment at the top + output += '# ⚠️ DO NOT EDIT THIS FILE DIRECTLY!\n'; + output += '# This file is automatically generated from bin/_headers.config.ts\n'; + output += '# To make changes, edit bin/_headers.config.ts and run: npm run build:headers\n'; + output += '#\n'; + output += '# See README.md for more information\n'; + output += '\n'; + + for (const pathConfig of config.paths) { + // Write path pattern + output += pathConfig.pattern + '\n'; + + // Process all headers + for (const [ +header, +value, +] of Object.entries(pathConfig.headers)) { + if (typeof value === 'object' && !Array.isArray(value)) { + // CSP-style header with directives + const directives: string[] = []; + for (const [ +directive, +sources, +] of Object.entries(value)) { + directives.push(`${directive} ${sources.join(' ')}`); + } + output += ` ${header}: ${directives.join('; ')}\n`; + } else { + // Simple array-based header + output += ` ${header}: ${value.join(' ')}\n`; + } + } + } + + return output; +} + +// Main execution +try { + const outputPath = path.join(process.cwd(), 'static', '_headers'); + + // Generate headers file + const headersContent = generateHeaders(config); + + // Write output file + fs.writeFileSync(outputPath, headersContent); + + console.log('✅ Generated static/_headers from bin/_headers.config.ts'); +} catch (error) { + console.error( + 'Error generating headers file:', + error instanceof Error ? error.message : String(error) + ); + process.exit(1); +} diff --git a/build.sh b/build.sh index 9d576953f..8e57bb7b4 100755 --- a/build.sh +++ b/build.sh @@ -9,6 +9,6 @@ if [[ "${BUILD_ENV:-}" == "preview" ]]; then fi # Generate _headers file from TypeScript configuration -npx tsx scripts/generate-headers.ts +npm run build:headers hugo --gc --minify -b "$CF_PAGES_URL" "${extra_args[@]}" diff --git a/eslint.config.mjs b/eslint.config.mjs index b22c5fd8d..dd3f48f07 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -144,6 +144,9 @@ export default tseslint.config( quotes: [ 'warn', 'single', + { + avoidEscape: true, + }, ], semi: [ 1, diff --git a/git/hooks/pre-commit b/git/hooks/pre-commit index 77d2e4ff5..a5b5d55b0 100755 --- a/git/hooks/pre-commit +++ b/git/hooks/pre-commit @@ -24,20 +24,4 @@ function timed_run() { fi } -# Regenerate static/_headers from _headers.config.ts if it changed -if git diff --cached --name-only | grep -q "scripts/_headers.config.ts"; then - echo "Detected changes to scripts/_headers.config.ts, regenerating static/_headers..." - npx tsx scripts/generate-headers.ts - git add static/_headers - echo "✅ Updated static/_headers and added to commit" -fi - -# Also regenerate if the generator script itself changed -if git diff --cached --name-only | grep -q "scripts/generate-headers.ts"; then - echo "Detected changes to generator script, regenerating static/_headers..." - npx tsx scripts/generate-headers.ts - git add static/_headers - echo "✅ Updated static/_headers and added to commit" -fi - timed_run "local/precious lint" "local/precious lint --staged" diff --git a/package-lock.json b/package-lock.json index c9de9c755..9156e0e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "stylelint-config-sass-guidelines": "^12.1.0", "stylelint-no-unsupported-browser-features": "^8.0.5", "stylelint-order": "^7.0.0", - "tsx": "^4.19.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.3" }, @@ -745,448 +744,6 @@ "url": "https://github.com/sponsors/JounQin" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -3116,48 +2673,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/escalade": { "version": "3.2.0", "dev": true, @@ -3643,21 +3158,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "dev": true, @@ -3762,19 +3262,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", @@ -6281,16 +5768,6 @@ "node": ">=8" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/reusify": { "version": "1.1.0", "dev": true, @@ -7269,26 +6746,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, "node_modules/type-check": { "version": "0.4.0", "dev": true, diff --git a/package.json b/package.json index 7662379df..337083f07 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "stylelint-config-sass-guidelines": "^12.1.0", "stylelint-no-unsupported-browser-features": "^8.0.5", "stylelint-order": "^7.0.0", - "tsx": "^4.19.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.3" }, @@ -35,13 +34,13 @@ "license": "MIT", "name": "blog-site", "scripts": { - "build:headers": "tsx scripts/generate-headers.ts", + "build:headers": "node --experimental-strip-types bin/generate-headers.ts", "fix": "run-p fix:*", "fix:content": "prettier --write '**/*.md' --config './content/.prettierrc.js'", "fix:scripts": "npm run lint:scripts --fix", "fix:styles": "npm run lint:styles --fix", "lint": "run-p lint:*", - "lint:configs": "npx eslint . .*rc.js --max-warnings=0", + "lint:configs": "npx eslint . .*rc.cjs --max-warnings=0", "lint:content": "prettier --check '**/*.md' --config './content/.prettierrc.js'", "lint:cspell": "cspell '**/*.md' --no-summary --no-progress --color", "lint:scripts": "tsc && npx eslint . --max-warnings=0", diff --git a/scripts/_headers.config.ts b/scripts/_headers.config.ts deleted file mode 100644 index b1970c9f4..000000000 --- a/scripts/_headers.config.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * HTTP Headers Configuration for Cloudflare Pages - * This file is the source of truth for static/_headers generation - * Run: npm run build:headers - * - * Note: This file lives in scripts/ (not static/) to avoid deploying - * TypeScript source files to production. - */ - -interface HeadersConfig { - paths: Array<{ - pattern: string; - headers: Record>; - }>; -} - -const config: HeadersConfig = { - paths: [ - { - pattern: '/*', - headers: { - 'Content-Security-Policy': { - 'connect-src': [ - '\'self\'', - 'https://status.maxmind.com', - 'https://www.maxmind.com', - 'https://api.hubspot.com', - 'https://*.googleapis.com', - 'https://*.google-analytics.com', - 'https://*.analytics.google.com', - 'https://*.googletagmanager.com', - 'https://*.g.doubleclick.net', - 'https://*.google.com', - 'https://forms.hsforms.com', - ], - 'default-src': [ - '\'self\'', - ], - 'font-src': [ - '\'self\'', - 'https://fonts.gstatic.com', - ], - 'form-action': [ - '\'self\'', - ], - 'frame-ancestors': [ - '\'self\'', - ], - 'frame-src': [ - '\'self\'', - 'https://app.hubspot.com', - 'https://www.google.com', - 'https://www.googletagmanager.com', - ], - 'img-src': [ - '\'self\'', - 'data:', - 'https:', - ], - 'object-src': [ - '\'none\'', - ], - 'script-src': [ - '\'self\'', - '\'report-sample\'', - '\'unsafe-inline\'', - 'https://js.hs-scripts.com', - 'https://js.hs-analytics.net', - 'https://js.hs-banner.com', - 'https://js.usemessages.com', - 'https://www.maxmind.com', - 'https://cloud.google.com', - 'https://www.gstatic.com', - 'https://www.googleadservices.com', - 'https://www.google.com', - 'https://*.googletagmanager.com', - 'https://js.hsforms.net', - ], - 'style-src': [ - '\'self\'', - '\'unsafe-inline\'', - 'https://fonts.googleapis.com', - 'https://www.gstatic.com', - ], - }, - 'Feature-Policy': - 'accelerometer \'none\'; autoplay \'none\'; camera \'none\'; ' + - 'encrypted-media \'none\'; fullscreen \'none\'; geolocation \'none\'; ' + - 'gyroscope \'none\'; magnetometer \'none\'; microphone \'none\'; ' + - 'midi \'none\'; payment \'none\'; picture-in-picture \'none\'; ' + - 'usb \'none\'; sync-xhr \'none\'', - 'Permissions-Policy': - 'accelerometer=(), ambient-light-sensor=(), autoplay=(), ' + - 'battery=(), camera=(), display-capture=(), document-domain=(), ' + - 'encrypted-media=(), execution-while-not-rendered=(), ' + - 'execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), ' + - 'geolocation=(), gyroscope=(), hid=(), idle-detection=(), ' + - 'magnetometer=(), microphone=(), midi=(), payment=(), ' + - 'picture-in-picture=(), publickey-credentials-get=(), ' + - 'screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), ' + - 'web-share=(), xr-spatial-tracking=()', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Strict-Transport-Security': - 'max-age=63072000; includeSubDomains; preload', - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - }, - }, - ], -}; - -export default config; diff --git a/scripts/generate-headers.ts b/scripts/generate-headers.ts deleted file mode 100644 index aec9b4309..000000000 --- a/scripts/generate-headers.ts +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env node - -/** - * Generate static/_headers file from scripts/_headers.config.ts - * - * This script converts a structured TypeScript configuration into the - * Cloudflare Pages _headers format. - */ - -import * as fs from 'fs'; -import * as path from 'path'; - -import config from './_headers.config.js'; - -interface PathConfig { - pattern: string; - headers: Record>; -} - -/** - * Generate _headers file content from config - */ -function generateHeaders( - config: { paths: PathConfig[] } -): string { - let output = ''; - - // Add warning comment at the top - output += '# ⚠️ DO NOT EDIT THIS FILE DIRECTLY!\n'; - output += '# This file is automatically generated from scripts/_headers.config.ts\n'; - output += '# To make changes, edit scripts/_headers.config.ts and run: npm run build:headers\n'; - output += '#\n'; - output += '# See README.md for more information\n'; - output += '\n'; - - for (const pathConfig of config.paths) { - // Write path pattern - output += pathConfig.pattern + '\n'; - - // Process CSP first if it exists - if (pathConfig.headers['Content-Security-Policy']) { - const csp = pathConfig.headers['Content-Security-Policy'] as Record< - string, - string | string[] - >; - const directives: string[] = []; - - for (const [ - directive, - sources, - ] of Object.entries(csp)) { - if (Array.isArray(sources)) { - directives.push(`${directive} ${sources.join(' ')}`); - } else { - directives.push(`${directive} ${sources}`); - } - } - - output += ` Content-Security-Policy: ${directives.join('; ')}\n`; - } - - // Process other headers - for (const [ - header, - value, - ] of Object.entries(pathConfig.headers)) { - if (header === 'Content-Security-Policy') continue; - - output += ` ${header}: ${value}\n`; - } - } - - return output; -} - -// Main execution -try { - const outputPath = path.join(process.cwd(), 'static', '_headers'); - - // Generate headers file - const headersContent = generateHeaders(config); - - // Write output file - fs.writeFileSync(outputPath, headersContent); - - console.log('✅ Generated static/_headers from scripts/_headers.config.ts'); -} catch (error) { - console.error( - 'Error generating headers file:', - error instanceof Error ? error.message : String(error) - ); - process.exit(1); -} diff --git a/static/_headers b/static/_headers deleted file mode 100644 index 19f4e1be9..000000000 --- a/static/_headers +++ /dev/null @@ -1,15 +0,0 @@ -# ⚠️ DO NOT EDIT THIS FILE DIRECTLY! -# This file is automatically generated from scripts/_headers.config.ts -# To make changes, edit scripts/_headers.config.ts and run: npm run build:headers -# -# See README.md for more information - -/* - Content-Security-Policy: connect-src 'self' https://status.maxmind.com https://www.maxmind.com https://api.hubspot.com https://*.googleapis.com https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.com https://forms.hsforms.com; default-src 'self'; font-src 'self' https://fonts.gstatic.com; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://app.hubspot.com https://www.google.com https://www.googletagmanager.com; img-src 'self' data: https:; object-src 'none'; script-src 'self' 'report-sample' 'unsafe-inline' https://js.hs-scripts.com https://js.hs-analytics.net https://js.hs-banner.com https://js.usemessages.com https://www.maxmind.com https://cloud.google.com https://www.gstatic.com https://www.googleadservices.com https://www.google.com https://*.googletagmanager.com https://js.hsforms.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.gstatic.com - Feature-Policy: accelerometer 'none'; autoplay 'none'; camera 'none'; encrypted-media 'none'; fullscreen 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; midi 'none'; payment 'none'; picture-in-picture 'none'; usb 'none'; sync-xhr 'none' - Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), web-share=(), xr-spatial-tracking=() - Referrer-Policy: strict-origin-when-cross-origin - Strict-Transport-Security: max-age=63072000; includeSubDomains; preload - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - X-XSS-Protection: 1; mode=block diff --git a/tsconfig.json b/tsconfig.json index d2dac5a6f..d0d2d75cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,9 @@ { "compilerOptions": { + "allowImportingTsExtensions": true, "baseUrl": ".", - "module": "commonjs", + "module": "ESNext", + "moduleResolution": "bundler", "noEmit": true, "skipLibCheck": true, "strict": true, @@ -14,7 +16,7 @@ ], "include": [ "./assets/js/**/*", - "./scripts/**/*.ts", + "./bin/**/*.ts", "./static/**/*.ts" ] } From 43fca3567ec2232e9f62edc6bbe92e101bac8680 Mon Sep 17 00:00:00 2001 From: Dallas Hogan Date: Sun, 30 Nov 2025 18:59:53 -0500 Subject: [PATCH 3/4] Clean up scripts --- bin/_headers.config.ts | 151 +++++++++++++++++++++++----------------- bin/generate-headers.ts | 12 ++-- 2 files changed, 92 insertions(+), 71 deletions(-) diff --git a/bin/_headers.config.ts b/bin/_headers.config.ts index 660d0a9fa..7c6dba72a 100644 --- a/bin/_headers.config.ts +++ b/bin/_headers.config.ts @@ -17,114 +17,135 @@ const config: HeadersConfig = { pattern: '/*', headers: { 'Content-Security-Policy': { - // Allow AJAX/fetch requests to status page, marketing site, HubSpot, and Google services 'connect-src': [ - '\'self\'', + "'self'", 'https://status.maxmind.com', 'https://www.maxmind.com', - // HubSpot API endpoint - // https://legacydocs.hubspot.com/docs/faq/how-do-i-create-a-custom-domain-for-my-forms - 'https://api.hubspot.com', - // HubSpot static assets used by conversations embed + // eslint-disable-next-line max-len - // https://developers.hubspot.com/beta-docs/guides/apps/authentication/working-with-oauth#frequently-asked-questions + // https://knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot#content-security-policy + + // HubSpot API + 'https://api.hubspot.com', + + // HubSpot static assets (conversations embed) 'https://forms.hsforms.com', + 'https://*.googleapis.com', + + // eslint-disable-next-line max-len + // https://developers.google.com/tag-platform/security/guides/csp#google_analytics_4_google_analytics 'https://*.google-analytics.com', 'https://*.analytics.google.com', 'https://*.googletagmanager.com', + + // https://developers.google.com/tag-platform/security/guides/csp#google_ads 'https://*.g.doubleclick.net', - // Google - // eslint-disable-next-line max-len - // https://developers.google.com/tag-platform/tag-manager/csp#google_analytics_4_google_analytics + + // Google domains (various TLDs for international support) 'https://*.google.com', ], 'default-src': [ - '\'self\'', + "'self'", ], - // Google Fonts and Vertex search (indirectly loaded when setting up the searchbox) 'font-src': [ - '\'self\'', + "'self'", + + // Loaded indirectly by Google Vertex search 'https://fonts.gstatic.com', ], 'form-action': [ - '\'self\'', + "'self'", ], 'frame-ancestors': [ - '\'self\'', + "'self'", ], - // HubSpot calls-to-action (pop-ups) and chatflows - // eslint-disable-next-line max-len - // https://knowledge.hubspot.com/website-pages/use-hubspot-content-on-external-sites#calls-to-action - // Google Vertex search 'frame-src': [ - '\'self\'', + "'self'", + + // eslint-disable-next-line max-len + // https://knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot#content-security-policy + + // HubSpot calls-to-action (pop-ups) and chatflows 'https://app.hubspot.com', - 'https://www.google.com', + + // https://developers.google.com/tag-platform/security/guides/csp#google_ads 'https://www.googletagmanager.com', + + // Google Vertex search + 'https://www.google.com', ], 'img-src': [ - '\'self\'', + "'self'", 'data:', 'https:', ], 'object-src': [ - '\'none\'', + "'none'", ], 'script-src': [ - '\'self\'', - '\'report-sample\'', - '\'unsafe-inline\'', - // HubSpot tracking code + "'self'", + "'report-sample'", + "'unsafe-inline'", + // eslint-disable-next-line max-len - // https://developers.hubspot.com/beta-docs/guides/api/tracking-code-api/tracking-code-quickstart-guide#frequently-asked-questions + // https://knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot#content-security-policy + + // HubSpot tracking code 'https://js.hs-scripts.com', - // HubSpot analytics - // https://knowledge.hubspot.com/reports/install-the-hubspot-tracking-code + + // HubSpot Analytics 'https://js.hs-analytics.net', + // HubSpot cookie banner - // https://knowledge.hubspot.com/privacy-and-consent/add-a-cookie-banner-to-your-website 'https://js.hs-banner.com', - // HubSpot conversations (live chat widget, chat flow) - // https://knowledge.hubspot.com/chatflows/install-the-hubspot-tracking-code-for-chat + + // HubSpot Conversations and Chatflows 'https://js.usemessages.com', - // HubSpot form widgets - // https://legacydocs.hubspot.com/docs/methods/forms/advanced_form_options + + // HubSpot forms 'https://js.hsforms.net', + + // MaxMind marketing site 'https://www.maxmind.com', - // Google - // eslint-disable-next-line max-len - // https://developers.google.com/tag-platform/tag-manager/csp#google_analytics_4_google_analytics + + // Google Vertex search 'https://cloud.google.com', 'https://www.gstatic.com', + + // https://developers.google.com/tag-platform/security/guides/csp#google_ads_conversions 'https://www.googleadservices.com', 'https://www.google.com', + + // Google Tag Manager 'https://*.googletagmanager.com', ], - // Google Fonts API and Vertex search - // Google static assets 'style-src': [ - '\'self\'', - '\'unsafe-inline\'', + "'self'", + "'unsafe-inline'", + + // Google Fonts API and Vertex search default styles 'https://fonts.googleapis.com', + + // Google static assets 'https://www.gstatic.com', ], }, 'Feature-Policy': [ - 'accelerometer \'none\'', - 'autoplay \'none\'', - 'camera \'none\'', - 'encrypted-media \'none\'', - 'fullscreen \'none\'', - 'geolocation \'none\'', - 'gyroscope \'none\'', - 'magnetometer \'none\'', - 'microphone \'none\'', - 'midi \'none\'', - 'payment \'none\'', - 'picture-in-picture \'none\'', - 'usb \'none\'', - 'sync-xhr \'none\'', + "accelerometer 'none'", + "autoplay 'none'", + "camera 'none'", + "encrypted-media 'none'", + "fullscreen 'none'", + "geolocation 'none'", + "gyroscope 'none'", + "magnetometer 'none'", + "microphone 'none'", + "midi 'none'", + "payment 'none'", + "picture-in-picture 'none'", + "usb 'none'", + "sync-xhr 'none'", ], 'Permissions-Policy': [ 'accelerometer=()', @@ -157,23 +178,23 @@ const config: HeadersConfig = { 'xr-spatial-tracking=()', ], 'Referrer-Policy': [ -'strict-origin-when-cross-origin', -], + 'strict-origin-when-cross-origin', + ], 'Strict-Transport-Security': [ 'max-age=63072000', 'includeSubDomains', 'preload', ], 'X-Content-Type-Options': [ -'nosniff', -], + 'nosniff', + ], 'X-Frame-Options': [ -'DENY', -], + 'DENY', + ], 'X-XSS-Protection': [ -'1', -'mode=block', -], + '1', + 'mode=block', + ], }, }, ], diff --git a/bin/generate-headers.ts b/bin/generate-headers.ts index da2df0f50..4a5d1047a 100644 --- a/bin/generate-headers.ts +++ b/bin/generate-headers.ts @@ -35,16 +35,16 @@ function generateHeaders(config: { paths: PathConfig[] }): string { // Process all headers for (const [ -header, -value, -] of Object.entries(pathConfig.headers)) { + header, + value, + ] of Object.entries(pathConfig.headers)) { if (typeof value === 'object' && !Array.isArray(value)) { // CSP-style header with directives const directives: string[] = []; for (const [ -directive, -sources, -] of Object.entries(value)) { + directive, + sources, + ] of Object.entries(value)) { directives.push(`${directive} ${sources.join(' ')}`); } output += ` ${header}: ${directives.join('; ')}\n`; From 1408ea1b1db47c9bd495ecafd9617b8d8515919a Mon Sep 17 00:00:00 2001 From: Dallas Hogan Date: Sun, 30 Nov 2025 19:22:33 -0500 Subject: [PATCH 4/4] Clean up headers automation and fix delimiter formatting --- bin/generate-headers.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/bin/generate-headers.ts b/bin/generate-headers.ts index 4a5d1047a..21c56087a 100644 --- a/bin/generate-headers.ts +++ b/bin/generate-headers.ts @@ -38,20 +38,26 @@ function generateHeaders(config: { paths: PathConfig[] }): string { header, value, ] of Object.entries(pathConfig.headers)) { - if (typeof value === 'object' && !Array.isArray(value)) { - // CSP-style header with directives + // Handle Content-Security-Policy (nested object) + if (header === 'Content-Security-Policy') { + const csp = value as Record; const directives: string[] = []; + for (const [ directive, sources, - ] of Object.entries(value)) { + ] of Object.entries(csp)) { directives.push(`${directive} ${sources.join(' ')}`); } - output += ` ${header}: ${directives.join('; ')}\n`; - } else { - // Simple array-based header - output += ` ${header}: ${value.join(' ')}\n`; + + output += ` Content-Security-Policy: ${directives.join('; ')}\n`; + continue; } + + // Handle all other headers (arrays) + const values = value as string[]; + const separator = header === 'Permissions-Policy' ? ', ' : '; '; + output += ` ${header}: ${values.join(separator)}\n`; } }