diff --git a/docs/BASELINE_COPY.md b/docs/BASELINE_COPY.md index b9235a5..d0e25c5 100644 --- a/docs/BASELINE_COPY.md +++ b/docs/BASELINE_COPY.md @@ -269,7 +269,7 @@ Weitere Informationen zum Datenschutz bei Cloudflare: https://www.cloudflare.com 4. Cloudflare-Dienste: Web Analytics und Turnstile -Zur Reichweitenmessung wird Cloudflare Web Analytics eingesetzt. Hierzu wird beim Aufruf der Website ein Skript (Beacon) vom Cloudflare-Host static.cloudflareinsights.com nachgeladen. Dieser Dienst arbeitet cookiefrei und ohne Fingerprinting. Es werden ausschließlich aggregierte Nutzungsstatistiken erhoben (Seitenaufrufe, Referrer, ungefähre geografische Herkunft auf Länderebene). Eine Identifizierung einzelner Besucher ist weder vorgesehen noch möglich. Aggregierte Auswertungsdaten werden für {{ cwa_retention_aggregates_months }} Monate gespeichert; einzelne Rohereignisse werden innerhalb von {{ cwa_retention_raw_hours }} Stunden aggregiert und anschließend gelöscht. Diese Angaben werden regelmäßig anhand der öffentlichen Dokumentation von Cloudflare geprüft (zuletzt geprüft am {{ cloudflare_facts_verified_date }}). +Zur Reichweitenmessung wird Cloudflare Web Analytics eingesetzt. Hierzu wird beim Aufruf der Website ein Skript (Beacon) vom Cloudflare-Host static.cloudflareinsights.com nachgeladen. Dieser Dienst arbeitet cookiefrei und ohne Fingerprinting. Es werden ausschließlich aggregierte Nutzungsstatistiken erhoben (Seitenaufrufe, Referrer, ungefähre geografische Herkunft auf Länderebene). Eine Identifizierung einzelner Besucher ist weder vorgesehen noch möglich. Aggregierte Auswertungsdaten werden für {{ cwa_retention_aggregates_months }} Monate gespeichert. Einzelne Rohereignisse werden bereits an der Cloudflare-Edge vorverarbeitet; eine dauerhafte Speicherung roher Einzelereignisse durch uns findet nicht statt. Diese Angaben werden regelmäßig anhand der öffentlichen Dokumentation von Cloudflare geprüft (zuletzt geprüft am {{ cloudflare_facts_verified_date }}). Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an einer datenschutzfreundlichen Reichweitenmessung). @@ -376,7 +376,7 @@ Cloudflare's privacy policy: https://www.cloudflare.com/privacypolicy/ 4. Cloudflare Services: Web Analytics and Turnstile -Cloudflare Web Analytics is used to measure site usage. To do this, a script (beacon) is loaded from the Cloudflare host static.cloudflareinsights.com when a page is opened. This service operates without cookies and without fingerprinting. Only aggregate usage statistics are collected (page views, referrer, approximate country-level geolocation). Identification of individual visitors is neither intended nor possible. Aggregated analytics data is retained for {{ cwa_retention_aggregates_months }} months; individual raw events are aggregated and then deleted within {{ cwa_retention_raw_hours }} hours. These figures are verified periodically against Cloudflare's public documentation (last verified on {{ cloudflare_facts_verified_date }}). +Cloudflare Web Analytics is used to measure site usage. To do this, a script (beacon) is loaded from the Cloudflare host static.cloudflareinsights.com when a page is opened. This service operates without cookies and without fingerprinting. Only aggregate usage statistics are collected (page views, referrer, approximate country-level geolocation). Identification of individual visitors is neither intended nor possible. Aggregated analytics data is retained for {{ cwa_retention_aggregates_months }} months. Individual raw events are pre-processed at Cloudflare's edge; no persistent storage of raw individual events takes place under our control. These figures are verified periodically against Cloudflare's public documentation (last verified on {{ cloudflare_facts_verified_date }}). Legal basis: Art. 6 (1) (f) GDPR (legitimate interest in privacy-friendly analytics). diff --git a/package.json b/package.json index 3b7f3a1..2d74fc0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "astro dev", "start": "astro dev", - "build": "node scripts/build-headers.mjs && astro build && node scripts/extract-tokens.mjs", + "build": "node scripts/check-cloudflare-facts-freshness.mjs && node scripts/build-headers.mjs && astro build && node scripts/extract-tokens.mjs", "preview": "astro preview", "astro": "astro", "check": "astro check" diff --git a/scripts/check-cloudflare-facts-freshness.mjs b/scripts/check-cloudflare-facts-freshness.mjs new file mode 100755 index 0000000..dc7121d --- /dev/null +++ b/scripts/check-cloudflare-facts-freshness.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/** + * Build-time freshness guard for src/data/cloudflare-facts.json. + * + * Manual-era "no silent failures" mechanism per backlog item #8. + * Fails the build if `verifiedDate` is older than MAX_AGE_DAYS, forcing + * either a fresh hand-verification or the Phase D automated verifier + * before another deploy can ship. + * + * MAX_AGE_DAYS=90 matches the future verifier's verification cadence. + * Tighter than the post-verifier 120-day threshold (which factors in a + * grace window on top of the cadence) — manual processes need stronger + * forcing functions than automated ones. + * + * Exit codes: + * 0 — verifiedDate is at most MAX_AGE_DAYS old. + * 1 — verifiedDate is missing, malformed, or older than MAX_AGE_DAYS. + */ + +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const MAX_AGE_DAYS = 90; +const DATA_PATH = resolve('src/data/cloudflare-facts.json'); + +function fail(message) { + process.stderr.write(`[cloudflare-facts] ${message}\n`); + process.exit(1); +} + +const raw = await readFile(DATA_PATH, 'utf8').catch((err) => + fail(`cannot read ${DATA_PATH}: ${err.message}`), +); + +let data; +try { + data = JSON.parse(raw); +} catch (err) { + fail(`invalid JSON in ${DATA_PATH}: ${err.message}`); +} + +const verifiedDate = data?.verifiedDate; +if (typeof verifiedDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(verifiedDate)) { + fail(`verifiedDate missing or not ISO YYYY-MM-DD: ${JSON.stringify(verifiedDate)}`); +} + +const verifiedMs = Date.parse(`${verifiedDate}T00:00:00Z`); +if (Number.isNaN(verifiedMs)) { + fail(`verifiedDate not a parseable date: ${verifiedDate}`); +} + +const ageDays = Math.floor((Date.now() - verifiedMs) / (24 * 60 * 60 * 1000)); +if (ageDays > MAX_AGE_DAYS) { + fail( + `verifiedDate ${verifiedDate} is ${ageDays} days old (max ${MAX_AGE_DAYS}). ` + + `Re-verify Cloudflare facts at ${data.dpf?.sourceUrl ?? ''} ` + + `and ${data.cwa?.sourceUrl ?? ''}, then update src/data/cloudflare-facts.json. ` + + `Or land Phase D Cloudflare-fact-verifier (backlog #8).`, + ); +} + +process.stdout.write(`[cloudflare-facts] verifiedDate ${verifiedDate} OK (${ageDays} days old)\n`); diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro index 6c41a2b..1496a7e 100644 --- a/src/components/SiteFooter.astro +++ b/src/components/SiteFooter.astro @@ -2,9 +2,8 @@ /** * Site footer — small brand mark, tiny nav, copyright. * - * Nav lists locale-aware About, Contact, Impressum, and an external - * GitHub link. Products and Datenschutz entries arrive with their - * respective Pass 2 phases. + * Nav lists locale-aware About, Contact, Impressum, Datenschutz, and an + * external GitHub link. Products entry arrives with Phase C. */ import BrandLogo from './BrandLogo.astro'; @@ -18,6 +17,7 @@ const t = getUiStrings(locale).footer; const aboutHref = locale === 'de' ? '/ueber' : '/en/about'; const contactHref = locale === 'de' ? '/kontakt' : '/en/contact'; const legalHref = locale === 'de' ? '/impressum' : '/en/legal'; +const privacyHref = locale === 'de' ? '/datenschutz' : '/en/privacy'; ---