@hubpav/hwio-web-worker 0.7.1 → 0.7.2
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 +4 -2
- package/dist/headers.d.ts +14 -0
- package/dist/headers.js +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +91 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @hubpav/hwio-web-worker
|
|
2
2
|
|
|
3
3
|
`hwioWorker(options)` is the fetch handler for a HARDWARIO static site on Cloudflare Workers:
|
|
4
|
-
CSP report collector (`POST /csp-report` → 204
|
|
5
|
-
|
|
4
|
+
CSP report collector (`POST /csp-report` → 204; Reporting API batches and legacy `report-uri`
|
|
5
|
+
bodies up to 256 KB, one JSON log line per distinct violation, URLs without query strings; parse
|
|
6
|
+
them yourself with `hwioParseCspReports`), optional canonical-host 301, optional Early Hints for
|
|
7
|
+
fonts. Query the collected reports in Workers Logs with the filter `kind = csp-report`. `@hubpav/hwio-web-worker/headers` builds the security
|
|
6
8
|
headers and renders the `_headers` file the `hwioUi()` Astro integration writes at build time.
|
package/dist/headers.d.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
export interface HWioHeadersOptions {
|
|
3
3
|
/** Consent-gated Google Tag Manager on this site (adds GTM/GA/Ads origins). */
|
|
4
4
|
gtm?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Country Google domains GA4 with Ads features calls (`www.google.<tld>/ads/ga-audiences`); a CSP
|
|
7
|
+
* source cannot wildcard the TLD, so each is listed. Only used with `gtm`. Default: the markets
|
|
8
|
+
* the sites are written for.
|
|
9
|
+
*/
|
|
10
|
+
googleTlds?: string[];
|
|
5
11
|
/** Cloudflare Turnstile on this site's forms. */
|
|
6
12
|
turnstile?: boolean;
|
|
7
13
|
/** The forms Worker endpoint origin, e.g. https://submit.hardwario.com */
|
|
@@ -35,6 +41,14 @@ export interface HWioHeadersOptions {
|
|
|
35
41
|
*/
|
|
36
42
|
previewNoindex?: boolean;
|
|
37
43
|
}
|
|
44
|
+
/** Default `googleTlds`: the countries of the sites' languages (cs, sk, de, pl) plus DACH neighbours. */
|
|
45
|
+
export declare const HWIO_GOOGLE_TLDS: string[];
|
|
46
|
+
/**
|
|
47
|
+
* connect-src origins for GA4 with Ads features via GTM, after Google's tag CSP guide
|
|
48
|
+
* (developers.google.com/tag-platform/security/guides/csp). `*.google.com` also covers
|
|
49
|
+
* `analytics.google.com/g/collect`, which `*.analytics.google.com` does not match.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hwioGtmConnectOrigins(tlds?: string[]): string[];
|
|
38
52
|
export declare function hwioBuildCsp(o: HWioHeadersOptions): string;
|
|
39
53
|
export declare function hwioSecurityHeaders(o: HWioHeadersOptions): Record<string, string>;
|
|
40
54
|
export declare function hwioEarlyHintLinks(files: string[]): string[];
|
package/dist/headers.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
const uniq = (list) => Array.from(new Set(list));
|
|
2
|
+
/** Default `googleTlds`: the countries of the sites' languages (cs, sk, de, pl) plus DACH neighbours. */
|
|
3
|
+
export const HWIO_GOOGLE_TLDS = ['cz', 'sk', 'de', 'at', 'ch', 'pl'];
|
|
4
|
+
/**
|
|
5
|
+
* connect-src origins for GA4 with Ads features via GTM, after Google's tag CSP guide
|
|
6
|
+
* (developers.google.com/tag-platform/security/guides/csp). `*.google.com` also covers
|
|
7
|
+
* `analytics.google.com/g/collect`, which `*.analytics.google.com` does not match.
|
|
8
|
+
*/
|
|
9
|
+
export function hwioGtmConnectOrigins(tlds = HWIO_GOOGLE_TLDS) {
|
|
10
|
+
return [
|
|
11
|
+
'https://www.googletagmanager.com',
|
|
12
|
+
'https://www.google-analytics.com',
|
|
13
|
+
'https://*.google-analytics.com',
|
|
14
|
+
'https://*.analytics.google.com',
|
|
15
|
+
'https://*.google.com',
|
|
16
|
+
...tlds.map((tld) => `https://*.google.${tld}`),
|
|
17
|
+
'https://*.g.doubleclick.net',
|
|
18
|
+
'https://pagead2.googlesyndication.com',
|
|
19
|
+
];
|
|
20
|
+
}
|
|
2
21
|
export function hwioBuildCsp(o) {
|
|
3
22
|
const script = ["'self'", ...(o.unsafeInlineScripts !== false ? ["'unsafe-inline'"] : []), ...(o.scriptHashes ?? []).map((h) => `'${h}'`)];
|
|
4
23
|
const connect = ["'self'"];
|
|
@@ -11,7 +30,7 @@ export function hwioBuildCsp(o) {
|
|
|
11
30
|
}
|
|
12
31
|
if (o.gtm) {
|
|
13
32
|
script.push('https://www.googletagmanager.com', 'https://www.google-analytics.com', 'https://*.google-analytics.com');
|
|
14
|
-
connect.push(
|
|
33
|
+
connect.push(...hwioGtmConnectOrigins(o.googleTlds));
|
|
15
34
|
frame.push('https://www.googletagmanager.com', 'https://td.doubleclick.net');
|
|
16
35
|
}
|
|
17
36
|
if (o.submitEndpoint)
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from './headers.js';
|
|
|
2
2
|
export interface HWioWorkerOptions {
|
|
3
3
|
/** Redirect anything that is not https://<canonicalHost> with one 301 (Astro hub sites). Null on micro-sites. */
|
|
4
4
|
canonicalHost?: string | null;
|
|
5
|
-
/** Answer POST /csp-report with 204 after logging one JSON line (Workers Logs). */
|
|
5
|
+
/** Answer POST /csp-report with 204 after logging one JSON line per distinct violation (Workers Logs). */
|
|
6
6
|
cspReport?: boolean;
|
|
7
7
|
/** Font files to announce as Early Hints on HTML responses served through the Worker. */
|
|
8
8
|
earlyHints?: string[];
|
|
@@ -15,6 +15,25 @@ export interface HWioWorkerOptions {
|
|
|
15
15
|
*/
|
|
16
16
|
previewHosts?: string[];
|
|
17
17
|
}
|
|
18
|
+
/** One CSP violation, normalized from either report format; URLs keep origin and path only. */
|
|
19
|
+
export interface HWioCspViolation {
|
|
20
|
+
directive: string;
|
|
21
|
+
blocked: string;
|
|
22
|
+
document: string;
|
|
23
|
+
source: string;
|
|
24
|
+
line: number | null;
|
|
25
|
+
column: number | null;
|
|
26
|
+
disposition: string;
|
|
27
|
+
sample: string;
|
|
28
|
+
count: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parses a CSP report body: the Reporting API batch (`application/reports+json`, an array of
|
|
32
|
+
* `{ type: 'csp-violation', body }`, sent for `report-to`) or the legacy `report-uri` object
|
|
33
|
+
* (`application/csp-report`, `{ 'csp-report': { … } }`). Identical violations are merged into one
|
|
34
|
+
* entry with a count. Throws on malformed JSON.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hwioParseCspReports(text: string): HWioCspViolation[];
|
|
18
37
|
/**
|
|
19
38
|
* Fetch handler for an Astro static site on Cloudflare Workers with static assets.
|
|
20
39
|
* Sites with `run_worker_first: ["/csp-report"]` only route the collector here; hub sites with
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
import { hwioEarlyHintLinks } from './headers.js';
|
|
2
2
|
export * from './headers.js';
|
|
3
|
+
/** Report bodies above this size are not parsed (Chrome batches reach ~100 KB). */
|
|
4
|
+
const CSP_MAX_BODY = 256 * 1024;
|
|
5
|
+
/** Distinct violations logged per request; the rest is summarized as `dropped`. */
|
|
6
|
+
const CSP_MAX_LINES = 50;
|
|
7
|
+
/** Origin + path of a URL, so query strings (form fields, click ids) never reach the logs. */
|
|
8
|
+
function cspUrl(value) {
|
|
9
|
+
if (typeof value !== 'string' || !value)
|
|
10
|
+
return '';
|
|
11
|
+
try {
|
|
12
|
+
const u = new URL(value);
|
|
13
|
+
return u.protocol === 'http:' || u.protocol === 'https:' ? `${u.origin}${u.pathname}` : u.protocol;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return value.slice(0, 200);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const cspText = (value, max = 200) => (typeof value === 'string' ? value.slice(0, max) : '');
|
|
20
|
+
const cspNumber = (value) => (Number.isFinite(Number(value)) && value !== null && value !== '' ? Number(value) : null);
|
|
21
|
+
/**
|
|
22
|
+
* Parses a CSP report body: the Reporting API batch (`application/reports+json`, an array of
|
|
23
|
+
* `{ type: 'csp-violation', body }`, sent for `report-to`) or the legacy `report-uri` object
|
|
24
|
+
* (`application/csp-report`, `{ 'csp-report': { … } }`). Identical violations are merged into one
|
|
25
|
+
* entry with a count. Throws on malformed JSON.
|
|
26
|
+
*/
|
|
27
|
+
export function hwioParseCspReports(text) {
|
|
28
|
+
const data = JSON.parse(text);
|
|
29
|
+
const raw = [];
|
|
30
|
+
if (Array.isArray(data)) {
|
|
31
|
+
for (const item of data) {
|
|
32
|
+
if (item && typeof item === 'object' && item.type === 'csp-violation') {
|
|
33
|
+
const body = item.body;
|
|
34
|
+
if (body && typeof body === 'object')
|
|
35
|
+
raw.push(body);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (data && typeof data === 'object') {
|
|
40
|
+
const legacy = data['csp-report'];
|
|
41
|
+
if (legacy && typeof legacy === 'object')
|
|
42
|
+
raw.push(legacy);
|
|
43
|
+
}
|
|
44
|
+
const merged = new Map();
|
|
45
|
+
for (const r of raw) {
|
|
46
|
+
const v = {
|
|
47
|
+
directive: cspText(r.effectiveDirective ?? r['effective-directive'] ?? r['violated-directive'], 80),
|
|
48
|
+
blocked: cspUrl(r.blockedURL ?? r['blocked-uri']),
|
|
49
|
+
document: cspUrl(r.documentURL ?? r['document-uri']),
|
|
50
|
+
source: cspUrl(r.sourceFile ?? r['source-file']),
|
|
51
|
+
line: cspNumber(r.lineNumber ?? r['line-number']),
|
|
52
|
+
column: cspNumber(r.columnNumber ?? r['column-number']),
|
|
53
|
+
disposition: cspText(r.disposition, 20),
|
|
54
|
+
sample: cspText(r.sample ?? r['script-sample'], 80),
|
|
55
|
+
count: 1,
|
|
56
|
+
};
|
|
57
|
+
const key = [v.directive, v.blocked, v.document, v.source, v.line, v.column, v.disposition].join('|');
|
|
58
|
+
const seen = merged.get(key);
|
|
59
|
+
if (seen)
|
|
60
|
+
seen.count += 1;
|
|
61
|
+
else
|
|
62
|
+
merged.set(key, v);
|
|
63
|
+
}
|
|
64
|
+
return [...merged.values()];
|
|
65
|
+
}
|
|
66
|
+
async function logCspReport(request) {
|
|
67
|
+
const ua = cspText(request.headers.get('user-agent'), 160);
|
|
68
|
+
const declared = Number(request.headers.get('content-length') ?? 0);
|
|
69
|
+
if (declared > CSP_MAX_BODY) {
|
|
70
|
+
console.log(JSON.stringify({ kind: 'csp-report', parse: 'too-large', bytes: declared, ua }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let violations;
|
|
74
|
+
try {
|
|
75
|
+
const text = await request.text();
|
|
76
|
+
if (text.length > CSP_MAX_BODY) {
|
|
77
|
+
console.log(JSON.stringify({ kind: 'csp-report', parse: 'too-large', bytes: text.length, ua }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
violations = hwioParseCspReports(text);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
console.log(JSON.stringify({ kind: 'csp-report', parse: 'failed', ua }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const v of violations.slice(0, CSP_MAX_LINES))
|
|
87
|
+
console.log(JSON.stringify({ kind: 'csp-report', ...v, ua }));
|
|
88
|
+
if (violations.length > CSP_MAX_LINES) {
|
|
89
|
+
console.log(JSON.stringify({ kind: 'csp-report', dropped: violations.length - CSP_MAX_LINES, ua }));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
3
92
|
/**
|
|
4
93
|
* Fetch handler for an Astro static site on Cloudflare Workers with static assets.
|
|
5
94
|
* Sites with `run_worker_first: ["/csp-report"]` only route the collector here; hub sites with
|
|
@@ -12,15 +101,8 @@ export function hwioWorker(options = {}) {
|
|
|
12
101
|
const url = new URL(request.url);
|
|
13
102
|
const preview = previewHosts.some((suffix) => url.hostname.endsWith(suffix));
|
|
14
103
|
if (cspReport && url.pathname === '/csp-report') {
|
|
15
|
-
if (request.method === 'POST')
|
|
16
|
-
|
|
17
|
-
const text = (await request.text()).slice(0, 8192);
|
|
18
|
-
console.log(JSON.stringify({ kind: 'csp-report', ua: request.headers.get('user-agent') ?? '', report: JSON.parse(text) }));
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
console.log(JSON.stringify({ kind: 'csp-report', parse: 'failed' }));
|
|
22
|
-
}
|
|
23
|
-
}
|
|
104
|
+
if (request.method === 'POST')
|
|
105
|
+
await logCspReport(request);
|
|
24
106
|
return new Response(null, { status: 204 });
|
|
25
107
|
}
|
|
26
108
|
if (canonicalHost && !preview && (url.protocol !== 'https:' || url.hostname !== canonicalHost)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubpav/hwio-web-worker",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|