Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/BASELINE_COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
62 changes: 62 additions & 0 deletions scripts/check-cloudflare-facts-freshness.mjs
Original file line number Diff line number Diff line change
@@ -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 ?? '<dpf source>'} ` +
`and ${data.cwa?.sourceUrl ?? '<cwa source>'}, 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`);
7 changes: 4 additions & 3 deletions src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
---

<footer class="site-footer">
Expand All @@ -31,6 +31,7 @@ const legalHref = locale === 'de' ? '/impressum' : '/en/legal';
<li><a href={aboutHref}>{t.about}</a></li>
<li><a href={contactHref}>{t.contact}</a></li>
<li><a href={legalHref}>{t.legal}</a></li>
<li><a href={privacyHref}>{t.privacy}</a></li>
<li>
<a
href="https://github.com/blackbrowed-labs"
Expand Down
13 changes: 13 additions & 0 deletions src/data/cloudflare-facts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"verifiedDate": "2026-04-28",
"dpf": {
"status": "active",
"sourceUrl": "https://www.dataprivacyframework.gov/participant/5666",
"originalCertificationDate": "2016-11-09",
"nextCertificationDue": "2026-09-23"
},
"cwa": {
"retentionAggregatesMonths": 6,
"sourceUrl": "https://developers.cloudflare.com/web-analytics/faq/"
}
}
39 changes: 39 additions & 0 deletions src/lib/cloudflare-facts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Read-only access to src/data/cloudflare-facts.json.
*
* Astro imports JSON statically at build time — the verifier in Phase
* D writes the same shape, so this typed view stays valid across the
* manual-data → automated-data transition.
*/

import facts from '../data/cloudflare-facts.json';

export interface CloudflareFacts {
readonly verifiedDate: string;
readonly dpf: {
readonly status: 'active' | 'pending' | 'withdrawn' | 'inactive';
readonly sourceUrl: string;
readonly originalCertificationDate: string;
readonly nextCertificationDue: string;
};
readonly cwa: {
readonly retentionAggregatesMonths: number;
readonly sourceUrl: string;
};
}

export const cloudflareFacts: CloudflareFacts = facts as CloudflareFacts;

/**
* Format an ISO 8601 date (YYYY-MM-DD) as a long-form locale-specific
* string for legal-prose rendering. German uses "28. April 2026"; English
* uses "April 28, 2026". Run at build time only.
*/
export function formatVerifiedDate(isoDate: string, locale: 'de' | 'en'): string {
const [year, month, day] = isoDate.split('-').map(Number);
return new Intl.DateTimeFormat(locale === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(new Date(Date.UTC(year, month - 1, day)));
}
Loading
Loading