forked from blackjackshellac/kitchenTimer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity_headers_middleware.mjs
More file actions
89 lines (81 loc) · 3.09 KB
/
Copy pathsecurity_headers_middleware.mjs
File metadata and controls
89 lines (81 loc) · 3.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* Reference security headers + CSP for a future static web UI or API gateway.
*
* taskTimer ships as GJS/GTK; this module is not wired into `main.js`. Use it when
* serving `dist/` (or `frontend/dist/`) behind Node, or mirror the same values in
* nginx/Caddy (see docs/dev/deployment.md — Task 66).
*/
/** @typedef {{ apiOrigin?: string, staticOrigin?: string, allowInlineStyles?: boolean }} SecurityHeaderOptions */
const DEFAULT_API_ORIGIN = 'http://localhost/mock';
const DEFAULT_STATIC_ORIGIN = "'self'";
/**
* Build Content-Security-Policy tuned for a same-origin SPA bundle plus API calls.
*
* Asset URLs: scripts/styles load from the app origin (`'self'` / `staticOrigin`).
* API: `connect-src` includes `apiOrigin` (Playwright MSW uses `http://localhost/mock` today).
*
* @param {SecurityHeaderOptions} [options]
* @returns {string}
*/
export function buildContentSecurityPolicy(options = {}) {
const apiOrigin = options.apiOrigin || DEFAULT_API_ORIGIN;
const staticOrigin = options.staticOrigin || DEFAULT_STATIC_ORIGIN;
const styleSrc = options.allowInlineStyles === false
? staticOrigin
: `${staticOrigin} 'unsafe-inline'`;
const directives = [
`default-src ${staticOrigin}`,
`base-uri ${staticOrigin}`,
`form-action ${staticOrigin}`,
`frame-ancestors 'none'`,
`object-src 'none'`,
`script-src ${staticOrigin}`,
`style-src ${styleSrc}`,
`img-src ${staticOrigin} data:`,
`font-src ${staticOrigin}`,
`connect-src ${staticOrigin} ${apiOrigin}`,
'upgrade-insecure-requests',
];
return directives.join('; ');
}
/**
* Header map applied by {@link securityHeadersMiddleware} and documented nginx snippet.
* @param {SecurityHeaderOptions} [options]
* @returns {Record<string, string>}
*/
export function securityHeaders(options = {}) {
return {
'Content-Security-Policy': buildContentSecurityPolicy(options),
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
};
}
/**
* Express-style `(req, res, next)` middleware, or call without `next` to set headers only.
*
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
* @param {(() => void)|undefined} next
* @param {SecurityHeaderOptions} [options]
*/
export function securityHeadersMiddleware(req, res, next, options = {}) {
const headers = securityHeaders(options);
for (const [name, value] of Object.entries(headers)) {
res.setHeader(name, value);
}
if (typeof next === 'function') {
next();
}
}
/**
* Apply headers to a ServerResponse (non-middleware call sites).
* @param {import('http').ServerResponse} res
* @param {SecurityHeaderOptions} [options]
*/
export function applySecurityHeaders(res, options = {}) {
securityHeadersMiddleware(null, res, undefined, options);
}