Skip to content

Commit 0a5c55c

Browse files
committed
Create automation for easier to read CSP diffs
1 parent 8f6d2cf commit 0a5c55c

File tree

8 files changed

+259
-2
lines changed

8 files changed

+259
-2
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,32 @@ when files change.
8585
hugo server
8686
```
8787

88+
### HTTP Headers Configuration
89+
90+
The `static/_headers` file is **automatically generated** from
91+
`scripts/_headers.config.ts`. This automation makes Content Security Policy (CSP)
92+
diffs more readable in pull requests.
93+
94+
#### ⚠️ Important
95+
96+
**DO NOT edit `static/_headers` directly!** Your changes will be overwritten. Instead,
97+
edit `scripts/_headers.config.ts`.
98+
99+
#### Making Changes to HTTP Headers
100+
101+
1. Edit `scripts/_headers.config.ts` with your desired header changes
102+
2. Run `npm run build:headers` to regenerate `static/_headers`
103+
3. Commit both files together
104+
105+
#### Automated Synchronization
106+
107+
Headers are automatically regenerated in three ways:
108+
109+
1. **Pre-commit hook**: When you commit changes to `scripts/_headers.config.ts`,
110+
the hook automatically regenerates `static/_headers` and includes it in your commit
111+
2. **Build process**: The `build.sh` script regenerates headers before deployment
112+
3. **Manual generation**: Run `npm run build:headers` anytime
113+
88114
### Updating Release Notes for the New Year
89115

90116
Whenever you create your first release note for a product category for a new

build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33
set -eu
44

5+
# Generate _headers file from TypeScript configuration
6+
npx tsx scripts/generate-headers.ts
7+
58
hugo --gc --minify -b "$CF_PAGES_URL"

git/hooks/pre-commit

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,20 @@ function timed_run() {
2424
fi
2525
}
2626

27+
# Regenerate static/_headers from _headers.config.ts if it changed
28+
if git diff --cached --name-only | grep -q "scripts/_headers.config.ts"; then
29+
echo "Detected changes to scripts/_headers.config.ts, regenerating static/_headers..."
30+
npx tsx scripts/generate-headers.ts
31+
git add static/_headers
32+
echo "✅ Updated static/_headers and added to commit"
33+
fi
34+
35+
# Also regenerate if the generator script itself changed
36+
if git diff --cached --name-only | grep -q "scripts/generate-headers.ts"; then
37+
echo "Detected changes to generator script, regenerating static/_headers..."
38+
npx tsx scripts/generate-headers.ts
39+
git add static/_headers
40+
echo "✅ Updated static/_headers and added to commit"
41+
fi
42+
2743
timed_run "precious lint" "precious lint --staged"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"url": "https://github.com/maxmind/dev-site.git"
3939
},
4040
"scripts": {
41+
"build:headers": "npx tsx scripts/generate-headers.ts",
4142
"fix": "run-p fix:*",
4243
"fix:scripts": "npm run lint:scripts --fix",
4344
"fix:styles": "npm run lint:styles --fix",

scripts/_headers.config.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* HTTP Headers Configuration for Cloudflare Pages
3+
* This file is the source of truth for static/_headers generation
4+
* Run: npm run build:headers
5+
*
6+
* Note: This file lives in scripts/ (not static/) to avoid deploying
7+
* TypeScript source files to production.
8+
*/
9+
10+
interface HeadersConfig {
11+
paths: Array<{
12+
pattern: string;
13+
headers: Record<string, string | Record<string, string[]>>;
14+
}>;
15+
}
16+
17+
const config: HeadersConfig = {
18+
paths: [
19+
{
20+
pattern: '/*',
21+
headers: {
22+
'Content-Security-Policy': {
23+
'connect-src': [
24+
'\'self\'',
25+
'https://status.maxmind.com',
26+
'https://www.maxmind.com',
27+
'https://api.hubspot.com',
28+
'https://static.hsappstatic.net',
29+
'https://*.googleapis.com',
30+
'https://*.google-analytics.com',
31+
'https://*.analytics.google.com',
32+
'https://*.googletagmanager.com',
33+
'https://*.g.doubleclick.net',
34+
'https://*.google.com',
35+
],
36+
'default-src': [
37+
'\'self\'',
38+
],
39+
'font-src': [
40+
'\'self\'',
41+
'https://fonts.gstatic.com',
42+
],
43+
'form-action': [
44+
'\'self\'',
45+
],
46+
'frame-ancestors': [
47+
'\'self\'',
48+
],
49+
'frame-src': [
50+
'\'self\'',
51+
'https://app.hubspot.com',
52+
'https://www.google.com',
53+
'https://www.googletagmanager.com',
54+
],
55+
'img-src': [
56+
'\'self\'',
57+
'data:',
58+
'https:',
59+
],
60+
'object-src': [
61+
'\'none\'',
62+
],
63+
'script-src': [
64+
'\'self\'',
65+
'\'report-sample\'',
66+
'\'unsafe-inline\'',
67+
'https://js.hs-scripts.com',
68+
'https://js.hs-analytics.net',
69+
'https://js.hs-banner.com',
70+
'https://js.usemessages.com',
71+
'https://www.maxmind.com',
72+
'https://cloud.google.com',
73+
'https://www.gstatic.com',
74+
'https://www.googleadservices.com',
75+
'https://www.google.com',
76+
'https://*.googletagmanager.com',
77+
],
78+
'style-src': [
79+
'\'self\'',
80+
'\'unsafe-inline\'',
81+
'https://fonts.googleapis.com',
82+
'https://www.gstatic.com',
83+
],
84+
},
85+
'Feature-Policy':
86+
'accelerometer \'none\'; autoplay \'none\'; camera \'none\'; ' +
87+
'encrypted-media \'none\'; fullscreen \'none\'; geolocation \'none\'; ' +
88+
'gyroscope \'none\'; magnetometer \'none\'; microphone \'none\'; ' +
89+
'midi \'none\'; payment \'none\'; picture-in-picture \'none\'; ' +
90+
'usb \'none\'; sync-xhr \'none\'',
91+
'Permissions-Policy':
92+
'accelerometer=(), ambient-light-sensor=(), autoplay=(), ' +
93+
'battery=(), camera=(), display-capture=(), document-domain=(), ' +
94+
'encrypted-media=(), execution-while-not-rendered=(), ' +
95+
'execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), ' +
96+
'geolocation=(), gyroscope=(), hid=(), idle-detection=(), ' +
97+
'magnetometer=(), microphone=(), midi=(), payment=(), ' +
98+
'picture-in-picture=(), publickey-credentials-get=(), ' +
99+
'screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), ' +
100+
'web-share=(), xr-spatial-tracking=()',
101+
'Referrer-Policy': 'strict-origin-when-cross-origin',
102+
'Strict-Transport-Security':
103+
'max-age=63072000; includeSubDomains; preload',
104+
'X-Content-Type-Options': 'nosniff',
105+
'X-Frame-Options': 'DENY',
106+
'X-XSS-Protection': '1; mode=block',
107+
},
108+
},
109+
],
110+
};
111+
112+
export default config;

scripts/generate-headers.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Generate static/_headers file from scripts/_headers.config.ts
5+
*
6+
* This script converts a structured TypeScript configuration into the
7+
* Cloudflare Pages _headers format.
8+
*/
9+
10+
import * as fs from 'fs';
11+
import * as path from 'path';
12+
13+
import config from './_headers.config.js';
14+
15+
interface PathConfig {
16+
pattern: string;
17+
headers: Record<string, string | Record<string, string | string[]>>;
18+
}
19+
20+
/**
21+
* Generate _headers file content from config
22+
*/
23+
function generateHeaders(
24+
config: { paths: PathConfig[] }
25+
): string {
26+
let output = '';
27+
28+
// Add warning comment at the top
29+
output += '# ⚠️ DO NOT EDIT THIS FILE DIRECTLY!\n';
30+
output += '# This file is automatically generated from scripts/_headers.config.ts\n';
31+
output += '# To make changes, edit scripts/_headers.config.ts and run: npm run build:headers\n';
32+
output += '#\n';
33+
output += '# See README.md for more information\n';
34+
output += '\n';
35+
36+
for (const pathConfig of config.paths) {
37+
// Write path pattern
38+
output += pathConfig.pattern + '\n';
39+
40+
// Process CSP first if it exists
41+
if (pathConfig.headers['Content-Security-Policy']) {
42+
const csp = pathConfig.headers['Content-Security-Policy'] as Record<
43+
string,
44+
string | string[]
45+
>;
46+
const directives: string[] = [];
47+
48+
for (const [
49+
directive,
50+
sources,
51+
] of Object.entries(csp)) {
52+
if (Array.isArray(sources)) {
53+
directives.push(`${directive} ${sources.join(' ')}`);
54+
} else {
55+
directives.push(`${directive} ${sources}`);
56+
}
57+
}
58+
59+
output += ` Content-Security-Policy: ${directives.join('; ')}\n`;
60+
}
61+
62+
// Process other headers
63+
for (const [
64+
header,
65+
value,
66+
] of Object.entries(pathConfig.headers)) {
67+
if (header === 'Content-Security-Policy') continue;
68+
69+
output += ` ${header}: ${value}\n`;
70+
}
71+
}
72+
73+
return output;
74+
}
75+
76+
// Main execution
77+
try {
78+
const outputPath = path.join(process.cwd(), 'static', '_headers');
79+
80+
// Generate headers file
81+
const headersContent = generateHeaders(config);
82+
83+
// Write output file
84+
fs.writeFileSync(outputPath, headersContent);
85+
86+
console.log('✅ Generated static/_headers from scripts/_headers.config.ts');
87+
} catch (error) {
88+
console.error(
89+
'Error generating headers file:',
90+
error instanceof Error ? error.message : String(error)
91+
);
92+
process.exit(1);
93+
}

static/_headers

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
# ⚠️ DO NOT EDIT THIS FILE DIRECTLY!
2+
# This file is automatically generated from scripts/_headers.config.ts
3+
# To make changes, edit scripts/_headers.config.ts and run: npm run build:headers
4+
#
5+
# See README.md for more information
6+
17
/*
2-
Content-Security-Policy: connect-src 'self' https://status.maxmind.com https://www.maxmind.com https://api.hubspot.com https://static.hsappstatic.net https://*.googleapis.com https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.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; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.gstatic.com
8+
Content-Security-Policy: connect-src 'self' https://status.maxmind.com https://www.maxmind.com https://api.hubspot.com https://static.hsappstatic.net https://*.googleapis.com https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.g.doubleclick.net https://*.google.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; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://www.gstatic.com
39
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'
410
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=()
511
Referrer-Policy: strict-origin-when-cross-origin

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
"public",
1414
".cache"
1515
],
16-
"include": ["./assets/js/**/*", "./bin/**/*.ts"]
16+
"include": ["./assets/js/**/*", "./bin/**/*.ts", "./scripts/**/*.ts"]
1717
}

0 commit comments

Comments
 (0)