Skip to content

Commit 441be06

Browse files
committed
Use backticks and arrays
1 parent bcd85ed commit 441be06

File tree

3 files changed

+106
-94
lines changed

3 files changed

+106
-94
lines changed

bin/_headers.config.ts

Lines changed: 83 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
interface HeadersConfig {
88
paths: Array<{
99
pattern: string;
10-
headers: Record<string, string | Record<string, string[]>>;
10+
headers: Record<string, string[] | Record<string, string[]>>;
1111
}>;
1212
}
1313

@@ -17,8 +17,10 @@ const config: HeadersConfig = {
1717
pattern: '/*',
1818
headers: {
1919
'Content-Security-Policy': {
20+
// Allow AJAX/fetch requests to status page, marketing site, HubSpot,
21+
// and Google services for analytics and tag management
2022
'connect-src': [
21-
'\'self\'',
23+
"'self'",
2224
'https://status.maxmind.com',
2325
'https://www.maxmind.com',
2426
'https://api.hubspot.com',
@@ -30,37 +32,32 @@ const config: HeadersConfig = {
3032
'https://*.g.doubleclick.net',
3133
'https://*.google.com',
3234
],
33-
'default-src': [
34-
'\'self\'',
35-
],
36-
'font-src': [
37-
'\'self\'',
38-
'https://fonts.gstatic.com',
39-
],
40-
'form-action': [
41-
'\'self\'',
42-
],
43-
'frame-ancestors': [
44-
'\'self\'',
45-
],
35+
// Fallback for resources not covered by other directives
36+
'default-src': ["'self'"],
37+
// Allow fonts from our site and Google Fonts
38+
'font-src': ["'self'", 'https://fonts.gstatic.com'],
39+
// Only allow form submissions to our own domain
40+
'form-action': ["'self'"],
41+
// Prevent this site from being embedded in iframes on other domains
42+
'frame-ancestors': ["'self'"],
43+
// Allow embedding content from HubSpot and Google services
4644
'frame-src': [
47-
'\'self\'',
45+
"'self'",
4846
'https://app.hubspot.com',
4947
'https://www.google.com',
5048
'https://www.googletagmanager.com',
5149
],
52-
'img-src': [
53-
'\'self\'',
54-
'data:',
55-
'https:',
56-
],
57-
'object-src': [
58-
'\'none\'',
59-
],
50+
// Allow images from our site, data URIs, and any HTTPS source
51+
'img-src': ["'self'", 'data:', 'https:'],
52+
// Block all plugins (Flash, Java, etc.)
53+
'object-src': ["'none'"],
54+
// Allow scripts from our site, HubSpot, Google services, and inline scripts
55+
// 'unsafe-inline' needed for HubSpot and Google Tag Manager
56+
// 'report-sample' includes script sample in violation reports
6057
'script-src': [
61-
'\'self\'',
62-
'\'report-sample\'',
63-
'\'unsafe-inline\'',
58+
"'self'",
59+
"'report-sample'",
60+
"'unsafe-inline'",
6461
'https://js.hs-scripts.com',
6562
'https://js.hs-analytics.net',
6663
'https://js.hs-banner.com',
@@ -72,35 +69,70 @@ const config: HeadersConfig = {
7269
'https://www.google.com',
7370
'https://*.googletagmanager.com',
7471
],
72+
// Allow styles from our site, Google Fonts, and inline styles
73+
// 'unsafe-inline' needed for dynamic styling
7574
'style-src': [
76-
'\'self\'',
77-
'\'unsafe-inline\'',
75+
"'self'",
76+
"'unsafe-inline'",
7877
'https://fonts.googleapis.com',
7978
'https://www.gstatic.com',
8079
],
8180
},
82-
'Feature-Policy':
83-
'accelerometer \'none\'; autoplay \'none\'; camera \'none\'; ' +
84-
'encrypted-media \'none\'; fullscreen \'none\'; geolocation \'none\'; ' +
85-
'gyroscope \'none\'; magnetometer \'none\'; microphone \'none\'; ' +
86-
'midi \'none\'; payment \'none\'; picture-in-picture \'none\'; ' +
87-
'usb \'none\'; sync-xhr \'none\'',
88-
'Permissions-Policy':
89-
'accelerometer=(), ambient-light-sensor=(), autoplay=(), ' +
90-
'battery=(), camera=(), display-capture=(), document-domain=(), ' +
91-
'encrypted-media=(), execution-while-not-rendered=(), ' +
92-
'execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), ' +
93-
'geolocation=(), gyroscope=(), hid=(), idle-detection=(), ' +
94-
'magnetometer=(), microphone=(), midi=(), payment=(), ' +
95-
'picture-in-picture=(), publickey-credentials-get=(), ' +
96-
'screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), ' +
97-
'web-share=(), xr-spatial-tracking=()',
98-
'Referrer-Policy': 'strict-origin-when-cross-origin',
99-
'Strict-Transport-Security':
100-
'max-age=63072000; includeSubDomains; preload',
101-
'X-Content-Type-Options': 'nosniff',
102-
'X-Frame-Options': 'DENY',
103-
'X-XSS-Protection': '1; mode=block',
81+
'Feature-Policy': [
82+
"accelerometer 'none'",
83+
"autoplay 'none'",
84+
"camera 'none'",
85+
"encrypted-media 'none'",
86+
"fullscreen 'none'",
87+
"geolocation 'none'",
88+
"gyroscope 'none'",
89+
"magnetometer 'none'",
90+
"microphone 'none'",
91+
"midi 'none'",
92+
"payment 'none'",
93+
"picture-in-picture 'none'",
94+
"usb 'none'",
95+
"sync-xhr 'none'",
96+
],
97+
'Permissions-Policy': [
98+
'accelerometer=()',
99+
'ambient-light-sensor=()',
100+
'autoplay=()',
101+
'battery=()',
102+
'camera=()',
103+
'display-capture=()',
104+
'document-domain=()',
105+
'encrypted-media=()',
106+
'execution-while-not-rendered=()',
107+
'execution-while-out-of-viewport=()',
108+
'fullscreen=()',
109+
'gamepad=()',
110+
'geolocation=()',
111+
'gyroscope=()',
112+
'hid=()',
113+
'idle-detection=()',
114+
'magnetometer=()',
115+
'microphone=()',
116+
'midi=()',
117+
'payment=()',
118+
'picture-in-picture=()',
119+
'publickey-credentials-get=()',
120+
'screen-wake-lock=()',
121+
'serial=()',
122+
'speaker-selection=()',
123+
'usb=()',
124+
'web-share=()',
125+
'xr-spatial-tracking=()',
126+
],
127+
'Referrer-Policy': ['strict-origin-when-cross-origin'],
128+
'Strict-Transport-Security': [
129+
'max-age=63072000',
130+
'includeSubDomains',
131+
'preload',
132+
],
133+
'X-Content-Type-Options': ['nosniff'],
134+
'X-Frame-Options': ['DENY'],
135+
'X-XSS-Protection': ['1', 'mode=block'],
104136
},
105137
},
106138
],

bin/generate-headers.ts

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,21 @@ import config from './_headers.config.ts';
1212

1313
interface PathConfig {
1414
pattern: string;
15-
headers: Record<string, string | Record<string, string | string[]>>;
15+
headers: Record<string, string[] | Record<string, string[]>>;
1616
}
1717

1818
/**
1919
* Generate _headers file content from config
2020
*/
21-
function generateHeaders(
22-
config: { paths: PathConfig[] }
23-
): string {
21+
function generateHeaders(config: { paths: PathConfig[] }): string {
2422
let output = '';
2523

2624
// Add warning comment at the top
2725
output += '# ⚠️ DO NOT EDIT THIS FILE DIRECTLY!\n';
28-
output += '# This file is automatically generated from bin/_headers.config.ts\n';
29-
output += '# To make changes, edit bin/_headers.config.ts and run: npm run build:headers\n';
26+
output +=
27+
'# This file is automatically generated from bin/_headers.config.ts\n';
28+
output +=
29+
'# To make changes, edit bin/_headers.config.ts and run: npm run build:headers\n';
3030
output += '#\n';
3131
output += '# See README.md for more information\n';
3232
output += '\n';
@@ -35,36 +35,25 @@ function generateHeaders(
3535
// Write path pattern
3636
output += pathConfig.pattern + '\n';
3737

38-
// Process CSP first if it exists
39-
if (pathConfig.headers['Content-Security-Policy']) {
40-
const csp = pathConfig.headers['Content-Security-Policy'] as Record<
41-
string,
42-
string | string[]
43-
>;
44-
const directives: string[] = [];
38+
// Process all headers
39+
for (const [header, value] of Object.entries(pathConfig.headers)) {
40+
// Handle Content-Security-Policy (nested object)
41+
if (header === 'Content-Security-Policy') {
42+
const csp = value as Record<string, string[]>;
43+
const directives: string[] = [];
4544

46-
for (const [
47-
directive,
48-
sources,
49-
] of Object.entries(csp)) {
50-
if (Array.isArray(sources)) {
45+
for (const [directive, sources] of Object.entries(csp)) {
5146
directives.push(`${directive} ${sources.join(' ')}`);
52-
} else {
53-
directives.push(`${directive} ${sources}`);
5447
}
55-
}
56-
57-
output += ` Content-Security-Policy: ${directives.join('; ')}\n`;
58-
}
5948

60-
// Process other headers
61-
for (const [
62-
header,
63-
value,
64-
] of Object.entries(pathConfig.headers)) {
65-
if (header === 'Content-Security-Policy') continue;
49+
output += ` Content-Security-Policy: ${directives.join('; ')}\n`;
50+
continue;
51+
}
6652

67-
output += ` ${header}: ${value}\n`;
53+
// Handle all other headers (arrays)
54+
const values = value as string[];
55+
const separator = header === 'Permissions-Policy' ? ', ' : '; ';
56+
output += ` ${header}: ${values.join(separator)}\n`;
6857
}
6958
}
7059

eslint.config.mjs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,6 @@ export default tseslint.config(
7979
},
8080

8181
rules: {
82-
'array-bracket-newline': [
83-
'warn',
84-
{
85-
minItems: 1,
86-
multiline: true,
87-
},
88-
],
89-
90-
'array-element-newline': [
91-
'warn',
92-
'always',
93-
],
9482
'comma-dangle': [
9583
'warn',
9684
'always-multiline',
@@ -145,6 +133,9 @@ export default tseslint.config(
145133
quotes: [
146134
'warn',
147135
'single',
136+
{
137+
avoidEscape: true,
138+
},
148139
],
149140
semi: [
150141
1,

0 commit comments

Comments
 (0)