@hubpav/hwio-web-worker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/headers.d.ts +36 -0
- package/dist/headers.js +74 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +42 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# @hubpav/hwio-web-worker
|
|
2
|
+
|
|
3
|
+
`hwioWorker(options)` is the fetch handler for a HARDWARIO static site on Cloudflare Workers:
|
|
4
|
+
CSP report collector (`POST /csp-report` → 204 + one JSON log line), optional canonical-host
|
|
5
|
+
301, optional Early Hints for fonts. `@hubpav/hwio-web-worker/headers` builds the security
|
|
6
|
+
headers and renders the `_headers` file the `hwioUi()` Astro integration writes at build time.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Options for the security headers a site ships (written into `_headers` at build time). */
|
|
2
|
+
export interface HWioHeadersOptions {
|
|
3
|
+
/** Consent-gated Google Tag Manager on this site (adds GTM/GA/Ads origins). */
|
|
4
|
+
gtm?: boolean;
|
|
5
|
+
/** Cloudflare Turnstile on this site's forms. */
|
|
6
|
+
turnstile?: boolean;
|
|
7
|
+
/** The forms Worker endpoint origin, e.g. https://submit.hardwario.com */
|
|
8
|
+
submitEndpoint?: string;
|
|
9
|
+
/** Extra frame-src origins (YouTube, HubSpot meetings). */
|
|
10
|
+
frames?: string[];
|
|
11
|
+
/** Extra script-src origins. */
|
|
12
|
+
scripts?: string[];
|
|
13
|
+
/** Extra connect-src origins. */
|
|
14
|
+
connect?: string[];
|
|
15
|
+
/** SHA-256 hashes ('sha256-…') of the inline bootstrap scripts. */
|
|
16
|
+
scriptHashes?: string[];
|
|
17
|
+
/** Keep 'unsafe-inline' in script-src (needed until every inline script is hashed). */
|
|
18
|
+
unsafeInlineScripts?: boolean;
|
|
19
|
+
/** Ship the CSP as Content-Security-Policy-Report-Only instead of enforcing. */
|
|
20
|
+
reportOnly?: boolean;
|
|
21
|
+
/** First-party collector path for CSP reports (the Worker answers it with 204). */
|
|
22
|
+
reportUri?: string | null;
|
|
23
|
+
/** HSTS: 'basic' = max-age only (www), 'strict' = includeSubDomains + preload (enerooo). */
|
|
24
|
+
hsts?: 'basic' | 'strict' | 'none';
|
|
25
|
+
/** Font files to announce as Early Hints on HTML responses (Cloudflare replays Link headers as 103). */
|
|
26
|
+
earlyHints?: string[];
|
|
27
|
+
/** Additional cache rules, path glob -> Cache-Control value. */
|
|
28
|
+
cache?: Record<string, string>;
|
|
29
|
+
/** Extra headers on every response (e.g. X-Robots-Tag: noindex). */
|
|
30
|
+
extra?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
export declare function hwioBuildCsp(o: HWioHeadersOptions): string;
|
|
33
|
+
export declare function hwioSecurityHeaders(o: HWioHeadersOptions): Record<string, string>;
|
|
34
|
+
export declare function hwioEarlyHintLinks(files: string[]): string[];
|
|
35
|
+
/** Renders a Cloudflare `_headers` file: immutable build assets, fonts, media, then the security block on `/*`. */
|
|
36
|
+
export declare function hwioRenderHeadersFile(o: HWioHeadersOptions): string;
|
package/dist/headers.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const uniq = (list) => Array.from(new Set(list));
|
|
2
|
+
export function hwioBuildCsp(o) {
|
|
3
|
+
const script = ["'self'", ...(o.unsafeInlineScripts !== false ? ["'unsafe-inline'"] : []), ...(o.scriptHashes ?? []).map((h) => `'${h}'`)];
|
|
4
|
+
const connect = ["'self'"];
|
|
5
|
+
const img = ["'self'", 'data:', 'https:'];
|
|
6
|
+
const frame = [];
|
|
7
|
+
if (o.turnstile) {
|
|
8
|
+
script.push('https://challenges.cloudflare.com');
|
|
9
|
+
connect.push('https://challenges.cloudflare.com');
|
|
10
|
+
frame.push('https://challenges.cloudflare.com');
|
|
11
|
+
}
|
|
12
|
+
if (o.gtm) {
|
|
13
|
+
script.push('https://www.googletagmanager.com', 'https://www.google-analytics.com', 'https://*.google-analytics.com');
|
|
14
|
+
connect.push('https://www.googletagmanager.com', 'https://www.google-analytics.com', 'https://*.google-analytics.com', 'https://*.analytics.google.com', 'https://stats.g.doubleclick.net');
|
|
15
|
+
frame.push('https://www.googletagmanager.com', 'https://td.doubleclick.net');
|
|
16
|
+
}
|
|
17
|
+
if (o.submitEndpoint)
|
|
18
|
+
connect.push(o.submitEndpoint);
|
|
19
|
+
script.push(...(o.scripts ?? []));
|
|
20
|
+
connect.push(...(o.connect ?? []));
|
|
21
|
+
frame.push(...(o.frames ?? []));
|
|
22
|
+
const directives = [
|
|
23
|
+
"default-src 'self'",
|
|
24
|
+
"base-uri 'self'",
|
|
25
|
+
"object-src 'none'",
|
|
26
|
+
"form-action 'self'",
|
|
27
|
+
"frame-ancestors 'self'",
|
|
28
|
+
`script-src ${uniq(script).join(' ')}`,
|
|
29
|
+
"style-src 'self' 'unsafe-inline'",
|
|
30
|
+
`img-src ${uniq(img).join(' ')}`,
|
|
31
|
+
"font-src 'self' data:",
|
|
32
|
+
`connect-src ${uniq(connect).join(' ')}`,
|
|
33
|
+
...(frame.length ? [`frame-src ${uniq(frame).join(' ')}`] : ["frame-src 'none'"]),
|
|
34
|
+
'upgrade-insecure-requests',
|
|
35
|
+
];
|
|
36
|
+
if (o.reportUri)
|
|
37
|
+
directives.push(`report-uri ${o.reportUri}`, 'report-to csp-endpoint');
|
|
38
|
+
return directives.join('; ');
|
|
39
|
+
}
|
|
40
|
+
export function hwioSecurityHeaders(o) {
|
|
41
|
+
const headers = {
|
|
42
|
+
'X-Content-Type-Options': 'nosniff',
|
|
43
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
44
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
45
|
+
'Permissions-Policy': 'geolocation=(), microphone=(), camera=(), payment=()',
|
|
46
|
+
};
|
|
47
|
+
const hsts = o.hsts ?? 'basic';
|
|
48
|
+
if (hsts === 'basic')
|
|
49
|
+
headers['Strict-Transport-Security'] = 'max-age=31536000';
|
|
50
|
+
if (hsts === 'strict')
|
|
51
|
+
headers['Strict-Transport-Security'] = 'max-age=63072000; includeSubDomains; preload';
|
|
52
|
+
const reportUri = o.reportUri === undefined ? '/csp-report' : o.reportUri;
|
|
53
|
+
if (reportUri)
|
|
54
|
+
headers['Reporting-Endpoints'] = `csp-endpoint="${reportUri}"`;
|
|
55
|
+
headers[o.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'] = hwioBuildCsp({ ...o, reportUri });
|
|
56
|
+
return { ...headers, ...(o.extra ?? {}) };
|
|
57
|
+
}
|
|
58
|
+
export function hwioEarlyHintLinks(files) {
|
|
59
|
+
return files.map((f) => `<${f}>; rel=preload; as=font; type=font/woff2; crossorigin`);
|
|
60
|
+
}
|
|
61
|
+
/** Renders a Cloudflare `_headers` file: immutable build assets, fonts, media, then the security block on `/*`. */
|
|
62
|
+
export function hwioRenderHeadersFile(o) {
|
|
63
|
+
const cache = {
|
|
64
|
+
'/_astro/*': 'public, max-age=31536000, immutable',
|
|
65
|
+
'/fonts/*': 'public, max-age=31536000, immutable',
|
|
66
|
+
'/images/*': 'public, max-age=2592000, must-revalidate',
|
|
67
|
+
...(o.cache ?? {}),
|
|
68
|
+
};
|
|
69
|
+
const blocks = Object.entries(cache).map(([path, value]) => `${path}\n Cache-Control: ${value}`);
|
|
70
|
+
const security = Object.entries(hwioSecurityHeaders(o)).map(([k, v]) => ` ${k}: ${v}`);
|
|
71
|
+
const hints = (o.earlyHints ?? []).length ? hwioEarlyHintLinks(o.earlyHints).map((l) => ` Link: ${l}`) : [];
|
|
72
|
+
blocks.push(`/*\n${[...security, ...hints].join('\n')}`);
|
|
73
|
+
return `# Generated by @hubpav/hwio-web-worker (hwioRenderHeadersFile). Edit the site's hwioUi() options, not this file.\n${blocks.join('\n\n')}\n`;
|
|
74
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './headers.js';
|
|
2
|
+
export interface HWioWorkerOptions {
|
|
3
|
+
/** Redirect anything that is not https://<canonicalHost> with one 301 (Astro hub sites). Null on micro-sites. */
|
|
4
|
+
canonicalHost?: string | null;
|
|
5
|
+
/** Answer POST /csp-report with 204 after logging one JSON line (Workers Logs). */
|
|
6
|
+
cspReport?: boolean;
|
|
7
|
+
/** Font files to announce as Early Hints on HTML responses served through the Worker. */
|
|
8
|
+
earlyHints?: string[];
|
|
9
|
+
/** Name of the static assets binding. */
|
|
10
|
+
assetsBinding?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fetch handler for an Astro static site on Cloudflare Workers with static assets.
|
|
14
|
+
* Sites with `run_worker_first: ["/csp-report"]` only route the collector here; hub sites with
|
|
15
|
+
* `run_worker_first: true` also get the canonical redirect and Early Hints.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hwioWorker(options?: HWioWorkerOptions): {
|
|
18
|
+
fetch(request: Request, env: Record<string, unknown>): Promise<Response>;
|
|
19
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { hwioEarlyHintLinks } from './headers.js';
|
|
2
|
+
export * from './headers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fetch handler for an Astro static site on Cloudflare Workers with static assets.
|
|
5
|
+
* Sites with `run_worker_first: ["/csp-report"]` only route the collector here; hub sites with
|
|
6
|
+
* `run_worker_first: true` also get the canonical redirect and Early Hints.
|
|
7
|
+
*/
|
|
8
|
+
export function hwioWorker(options = {}) {
|
|
9
|
+
const { canonicalHost = null, cspReport = true, earlyHints = [], assetsBinding = 'ASSETS' } = options;
|
|
10
|
+
return {
|
|
11
|
+
async fetch(request, env) {
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
if (cspReport && url.pathname === '/csp-report') {
|
|
14
|
+
if (request.method === 'POST') {
|
|
15
|
+
try {
|
|
16
|
+
const text = (await request.text()).slice(0, 8192);
|
|
17
|
+
console.log(JSON.stringify({ kind: 'csp-report', ua: request.headers.get('user-agent') ?? '', report: JSON.parse(text) }));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
console.log(JSON.stringify({ kind: 'csp-report', parse: 'failed' }));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return new Response(null, { status: 204 });
|
|
24
|
+
}
|
|
25
|
+
if (canonicalHost && (url.protocol !== 'https:' || url.hostname !== canonicalHost)) {
|
|
26
|
+
url.protocol = 'https:';
|
|
27
|
+
url.hostname = canonicalHost;
|
|
28
|
+
return Response.redirect(url.toString(), 301);
|
|
29
|
+
}
|
|
30
|
+
const assets = env[assetsBinding];
|
|
31
|
+
if (!assets)
|
|
32
|
+
return new Response('assets binding missing', { status: 500 });
|
|
33
|
+
const res = await assets.fetch(request);
|
|
34
|
+
if (earlyHints.length && res.status === 200 && (res.headers.get('content-type') ?? '').includes('text/html')) {
|
|
35
|
+
const withHints = new Response(res.body, res);
|
|
36
|
+
hwioEarlyHintLinks(earlyHints).forEach((l) => withHints.headers.append('Link', l));
|
|
37
|
+
return withHints;
|
|
38
|
+
}
|
|
39
|
+
return res;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hubpav/hwio-web-worker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Cloudflare Worker handler and security-header builder for the HARDWARIO static sites: canonical-host redirect, CSP report collector, Early Hints, _headers generation.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hardwario/website-ui.git",
|
|
10
|
+
"directory": "packages/web-worker"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./headers": {
|
|
22
|
+
"types": "./dist/headers.d.ts",
|
|
23
|
+
"default": "./dist/headers.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"test": "node --test \"test/**/*.test.mjs\""
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false
|
|
35
|
+
}
|