Skip to content

Commit db74a6e

Browse files
committed
refactor: rearrange some dependencies, preparing for adapter(s)
1 parent edf676c commit db74a6e

File tree

4 files changed

+109
-98
lines changed

4 files changed

+109
-98
lines changed

src/index.ts

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,52 @@ import { type SecHeaders } from "./types.js";
33
import crypto from "node:crypto";
44
import { appendHeader } from "vinxi/http";
55
import { DEFAULT_HEADERS, HEADER_NAMES } from "./defaults.js";
6-
import { keyIsHeader } from "./utils.js";
7-
import { generateCSP } from "./lib/csp.js";
6+
import { deepFallbackMerge, keyIsHeader } from "./utils.js";
7+
import { chooseCSP, generateCSP } from "./lib/csp.js";
88

9-
export const secureRequest = (options?: SecHeaders) => (event: FetchEvent) => {
10-
const settings: SecHeaders = { ...DEFAULT_HEADERS, ...options };
11-
12-
const chooseCSP = () => {
13-
if (!settings.csp) {
14-
return;
15-
}
16-
if (process.env.NODE_ENV === "development") {
17-
return settings.csp.dev || settings.csp.prod;
18-
} else {
19-
return settings.csp.prod;
20-
}
9+
const h3Attacher =
10+
(event: FetchEvent["nativeEvent"]) => (key: string, headerValue: string) => {
11+
appendHeader(event, key, headerValue);
2112
};
2213

23-
const nonce = crypto.randomBytes(16).toString("base64");
24-
event.locals.nonce = nonce;
25-
14+
function attachSecHeaders(
15+
settings: SecHeaders,
16+
attacher: ReturnType<typeof h3Attacher>,
17+
) {
2618
const entries = Object.entries(settings) as Array<
2719
[keyof SecHeaders, string | null]
2820
>;
29-
30-
entries.forEach(([configKey, headerValue]) => {
21+
for (const [configKey, headerValue] of entries) {
3122
if (headerValue === null) {
3223
return;
3324
}
3425

3526
if (keyIsHeader(configKey)) {
36-
const key = HEADER_NAMES[configKey];
37-
38-
appendHeader(event.nativeEvent, key, headerValue);
27+
attacher(HEADER_NAMES[configKey], headerValue);
3928
}
40-
});
41-
42-
const csp = chooseCSP();
43-
44-
if (csp) {
45-
appendHeader(
46-
event.nativeEvent,
47-
csp.cspBlock || !csp.cspReportOnly
48-
? "Content-Security-Policy"
49-
: "Content-Security-Policy-Report-Only",
50-
generateCSP(csp.value, nonce),
51-
);
5229
}
53-
};
30+
}
31+
32+
// SolidStart FetchEvent is H3Event["context"]
33+
export const secureRequest =
34+
(options?: Partial<SecHeaders>) => (event: FetchEvent) => {
35+
const settings = options
36+
? deepFallbackMerge<SecHeaders>(options, DEFAULT_HEADERS)
37+
: DEFAULT_HEADERS;
38+
const addHeader = h3Attacher(event.nativeEvent);
39+
const csp = chooseCSP(settings);
40+
41+
const nonce = crypto.randomBytes(16).toString("base64");
42+
event.locals.nonce = nonce;
43+
44+
attachSecHeaders(settings, addHeader);
45+
46+
if (csp) {
47+
addHeader(
48+
csp.cspBlock || !csp.cspReportOnly
49+
? "Content-Security-Policy"
50+
: "Content-Security-Policy-Report-Only",
51+
generateCSP(csp.value, nonce),
52+
);
53+
}
54+
};

src/lib/csp.ts

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,76 @@
1-
import {
2-
getCSP,
3-
nonce,
4-
CSPDirectives,
5-
} from "csp-header";
6-
import { type CSP, type CSPHeaderConfig } from "../types.js";
1+
import { getCSP, nonce, CSPDirectives } from "csp-header";
2+
import { SecHeaders, type CSP, type CSPHeaderConfig } from "../types.js";
73
import { DEV_DEFAULT_CSP, PROD_DEFAULT_CSP } from "../defaults.js";
84

95
const cspNonceDirectives = [
10-
"script-src",
11-
"style-src",
12-
"img-src",
13-
"font-src",
14-
"media-src",
15-
"object-src",
16-
"default-src",
6+
"script-src",
7+
"style-src",
8+
"img-src",
9+
"font-src",
10+
"media-src",
11+
"object-src",
12+
"default-src",
1713
] as const;
1814

1915
const DEFAULT_CSP: CSPHeaderConfig = {
20-
prod: {
21-
withNonce: true,
22-
value: PROD_DEFAULT_CSP,
23-
cspBlock: false,
24-
cspReportOnly: true,
25-
},
26-
dev: {
27-
withNonce: true,
28-
value: DEV_DEFAULT_CSP,
29-
cspBlock: true,
30-
cspReportOnly: false,
31-
},
16+
prod: {
17+
withNonce: true,
18+
value: PROD_DEFAULT_CSP,
19+
cspBlock: false,
20+
cspReportOnly: true,
21+
},
22+
dev: {
23+
withNonce: true,
24+
value: DEV_DEFAULT_CSP,
25+
cspBlock: true,
26+
cspReportOnly: false,
27+
},
3228
};
3329

3430
export const addNonceToDirectives = (
35-
userDefinedCSP: CSP["value"],
36-
nonceString: string
31+
userDefinedCSP: CSP["value"],
32+
nonceString: string,
3733
): CSP["value"] => {
38-
const csp: Partial<CSPDirectives> = {
39-
...DEFAULT_CSP.prod.value,
40-
...userDefinedCSP,
41-
};
34+
const csp: Partial<CSPDirectives> = {
35+
...DEFAULT_CSP.prod.value,
36+
...userDefinedCSP,
37+
};
4238

43-
cspNonceDirectives.forEach((directive) => {
44-
if (csp[directive] && Array.isArray(csp[directive])) {
45-
csp[directive].push(nonce(nonceString));
46-
}
47-
});
39+
cspNonceDirectives.forEach((directive) => {
40+
if (csp[directive] && Array.isArray(csp[directive])) {
41+
csp[directive].push(nonce(nonceString));
42+
}
43+
});
4844

49-
return csp;
45+
return csp;
5046
};
5147

5248
export function generateCSP(cspOptions: CSP["value"], nonceString?: string) {
53-
const isDev = process.env.NODE_ENV === "development";
54-
const directives = nonceString && !isDev ? addNonceToDirectives(cspOptions, nonceString) : cspOptions;
49+
const isDev = process.env.NODE_ENV === "development";
50+
const directives =
51+
nonceString && !isDev
52+
? addNonceToDirectives(cspOptions, nonceString)
53+
: cspOptions;
5554

56-
if (Object.prototype.hasOwnProperty.call(directives,"report-uri")) {
57-
const reportUri = directives["report-uri"];
58-
delete directives["report-uri"];
55+
if (Object.prototype.hasOwnProperty.call(directives, "report-uri")) {
56+
const reportUri = directives["report-uri"];
57+
delete directives["report-uri"];
5958

60-
return getCSP({ directives, reportUri }) as string;
61-
} else {
62-
return getCSP({
63-
directives,
64-
}) as string;
65-
}
59+
return getCSP({ directives, reportUri }) as string;
60+
} else {
61+
return getCSP({
62+
directives,
63+
}) as string;
64+
}
6665
}
66+
67+
export const chooseCSP = (settings: SecHeaders) => {
68+
if (!settings.csp) {
69+
return;
70+
}
71+
if (process.env.NODE_ENV === "development") {
72+
return settings.csp.dev || settings.csp.prod;
73+
} else {
74+
return settings.csp.prod;
75+
}
76+
};

src/lib/hsts.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
interface Params {
2-
includeSubDomains?: boolean;
3-
preload?: boolean;
4-
maxAge: number;
2+
includeSubDomains?: boolean;
3+
preload?: boolean;
4+
maxAge: number;
55
}
66

77
export function hsts({
8-
includeSubDomains = true,
9-
preload = true,
10-
maxAge = 31536000 /* 1year */,
8+
includeSubDomains = true,
9+
preload = true,
10+
maxAge = 31536000 /* 1year */,
1111
}: Params) {
12-
return `max-age=${String(maxAge)};${includeSubDomains ? " includeSubDomains;" : ""}${
13-
preload ? " preload" : ""
14-
}`;
12+
return `max-age=${String(maxAge)};${includeSubDomains ? " includeSubDomains;" : ""}${
13+
preload ? " preload" : ""
14+
}`;
1515
}

src/utils.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { HEADER_NAMES } from "./defaults.js";
22
import { type HeaderNames } from "./types.js";
33

4-
export function deepMerge(
5-
target: Record<string, unknown>,
6-
source: Record<string, unknown>,
7-
): Record<string, unknown> {
4+
export function deepFallbackMerge<TargetShape = Record<string, unknown>>(
5+
target: Partial<TargetShape>,
6+
source: TargetShape,
7+
): TargetShape {
88
for (const key in source) {
99
if (source[key] instanceof Object) {
1010
if (!target[key]) {
1111
Object.assign(target, { [key]: {} });
1212
}
1313

14-
deepMerge(
14+
deepFallbackMerge(
1515
target[key] as Record<string, unknown>,
1616
source[key] as Record<string, unknown>,
1717
);
1818
} else {
1919
Object.assign(target, { [key]: source[key] });
2020
}
2121
}
22-
return target;
22+
return target as TargetShape;
2323
}
2424

2525
export const keyIsHeader = (key: string): key is keyof HeaderNames => {

0 commit comments

Comments
 (0)