# CloakDrop — Launch-Checkliste

Stand 30. Juli 2026. Erzeugt aus einem Audit über neun Domänen (304 Rohbefunde),
gegen das Repo auf Falsch-Positive geprüft, auf Vollständigkeit und fachliche
Richtigkeit gegengelesen, entdoppelt und in Stufen geordnet.
[B] = Launch-Blocker. Aufwand S/M/L/XL. "Abh." = Abhängigkeit.

---

# CloakDrop — Konsolidierte Launch-Checkliste

**Aufwand:** S ≤ 1 PT · M = 2–4 PT · L = 5–10 PT · XL > 10 PT. **[B] = echter Launch-Blocker** (ohne das darf/kann es nicht live gehen).
Zusammengeführt aus 303 Rohpunkten + 18 Ergänzungen; widerlegte Punkte gestrichen, fachliche Korrekturen wörtlich in die betroffenen Punkte übernommen. Domänen bei Mehrfachherkunft in Klammern.

---

## Stufe 0 — Fundament (ohne das ist alles andere wertlos)

**0.1 · [B] · M · Versionskontrolle, Freeze-Commit, Off-Site-Backup** (Prozess + Betrieb)
warum: Der komplette Bestand existiert genau einmal in `c:\Users\roman\Downloads\CLOAKDROP`, ohne `.git`, ohne Remote, ohne Historie. Er ist nicht regenerierbar: `tasks/build_app.py:806` liest die **ausgelieferte** Datei als eigene Eingabe und holt die Base64-Wortmarke aus ihr heraus (798-802). Ein fehlgeschlagener Regex ist ein Totalverlust — genau das ist laut `tasks/todo.md:1283` schon passiert. `tasks/_baseline` ist kein Backup: zwei Dateien vom 28.07. mit dem bekannten `$1`-Defekt.
tun: Projekt aus `Downloads` an einen dauerhaften Ort. `git init`, erster Commit = 20 .html + `tasks/` im heutigen Byte-Zustand ohne jede Aufräumung, `git tag pre-refactor`. `.gitignore` (`tasks/_shots/` 6,0 MB, `dist/`, `__pycache__/`, `.wrangler/`); `tasks/_baseline` **löschen**, nicht ignorieren. `.gitattributes` mit `* text=auto eol=lf` plus `core.autocrlf=false` — alle Dateien sind rein LF (index.html: 0× CRLF, 3183× LF), `build_app.py:848` schreibt mit `newline=''`, und ein CRLF-Checkout auf Windows bricht die Zusicherung `shared_hash(t) == hash_before` (Zeile 886). Privates Remote plus zweites Remote außerhalb desselben Anbieterkontos. RPO = letzter Commit, RTO = 15 min, Wiederherstellung einmal echt durchspielen.
Abh.: keine — der erste Handgriff überhaupt.

**0.2 · [B] · L · src/ vs. dist/: der Bau darf nicht sein eigenes Ergebnis patchen**
warum: `build_app.py:805-848` liest die Zieldatei, ersetzt vier marker-gefundene Regionen und schreibt zurück. `<head>`, Fonts, geteiltes CSS, Fußzeile, Cookie-Dialog existieren **nur** im Artefakt. Folge: `rm -rf dist && build` ist unmöglich, ein halb gelaufener Bau hinterlässt kaputte Auslieferung, und Minifizierung ist grundsätzlich ausgeschlossen, weil sie die Anker (`/* ===== App-Schale`, `</style>\n</head>`, `<div class="cookie-pop"`) zerstört.
tun: `src/pages`, `src/css/{base,pages}`, `src/js/{base,pages}`, `src/partials`, `src/assets`, `src/data`; `dist/` wegwerfbar und ignoriert. Erster Schritt mechanisch: 20 Dateien nach `src/pages/` verschieben, Bau **kopiert** nur — dieser Zustand muss byte-identisch zum Freeze-Commit sein, bevor irgendetwas extrahiert wird.
Abh.: 0.1

**0.3 · [B] · XL · Alle 20 Seiten aus einem Generator + geteilten Unterbau extrahieren** (Bau + Performance)
warum: Gemessen: erster `<style>`-Block 152.071 B, SHA-Prefix `08d0e9ba`, **identisch auf 20/20**; zweiter Block mit 72.262 B Gemeinprefix = ganzer Block 2 von index.html. Zusammen 224.333 B je Seite, 4,28 MB von 6,35 MB. Dazu 5 identische JS-Blöcke (Theme-Toggle 2.424 B auf 17 Seiten, Cookie-Banner 1.862 B auf 18, Nav-Dropdowns, Scroll-Reveal, Newsletter) und 2,89 MB Font-Base64. `build_app.py:858-867` deckt nur `dashboard/received/expired/settings` ab — **index.html ist Handarbeit** (Docstring Zeile 2: „Generator fuer die vier App-Seiten"). Messbarer Preis der Duplikation: die Zusicherung `assert 'href="#"' not in t` (894) sorgt dafür, dass die 4 gebauten Seiten 0 tote Links haben, die 16 Handseiten tragen 85. Der `$1`-Defekt musste „in allen 18 Dateien byte-identisch" korrigiert werden (`todo.md:1283-1296`) — vier verlorene Selektoren, die die Upload- und Fehlerzustände der Drop-Card mit Browser-Defaults rendern ließen.
tun: Strikte Reihenfolge, jede Stufe eigener Commit mit eigener Zusicherung: (0) Pixel-Baseline aller 20 Seiten × {light,dark} × {320,400,720,1040,1440,1920,2560} nach `tests/baseline/`. (1) Kopier-Bau, byte-identisch. (2) Fonts heraus (`inter-var-latin.woff2` ≈ 73 KB, `jetbrains-mono-var-latin.woff2` ≈ 40 KB aus 97.356 + 53.976 Base64-Zeichen), `font-display: swap`, `unicode-range` unverändert, `preload` nur für Inter. (3) `base.css` = Block 1 ohne Fonts + 72.262-B-Prefix, eingebunden **genau an der Stelle** der Inline-Blöcke — Reihenfolge base vor Seiten-CSS ist tragend. (4) Seiten-CSS ab `\n\n/* ===== <Seitenname>` (maschinell eindeutig: about 7.723 B, api 14.020 B, App-Seiten je 62.418 B identisch, imprint/privacy je 2.379 B). (5) JS analog; der 234-B-Theme-Bootstrap (20/20 identisch) **muss** inline im `<head>` bleiben und braucht dafür einen CSP-Hash. (6) Alle 20 Seiten über den Generator, Zusicherungsliste aus 886-922 auf alle anwenden, erweitert um „jeder interne Link zeigt auf eine existierende Datei" und „Titel/Description je Seite gesetzt und unterschiedlich". Zusicherungen: (a) Round-Trip `base.css + page.css` byte-gleich dem ursprünglichen Inline-CSS, (b) `shared_hash`-Wächter bleibt, (c) `getComputedStyle` über feste Selektorliste in beiden Themes, (d) Pixel-Diff = 0. Erst danach Minifizierung (lightningcss/esbuild/html-minifier-terser) und Inhalts-Hashes im Dateinamen. Alt-Generatoren `build_dashboard_pages.py` (1.700 Zeilen) und `build_received.py` löschen — ihr `SystemExit`-Guard schützt vor dem Ausführen, nicht vor dem Lesen, und `build_received.py` würde das Vorgängerdesign inkl. `$1`-Defekt zurückbauen.
Auf: ~20–25 PT — der größte Einzelposten der Stufe.
Abh.: 0.2

**0.4 · [B] · S · Abhängigkeitsverwaltung und Werkzeug-Pinning**
warum: Keine `package.json`, keine `requirements.txt`, kein Lockfile. Bau braucht Python (lokal 3.10.11), Tests Playwright plus passenden Chromium — beides unversioniert. Der Bau läuft nur auf diesem Rechner; `opensource.html` verspricht „reproducible builds".
tun: Werkzeugkette auf Node vereinheitlichen (Minifizierung, Bildpipeline, Playwright-Runner und Wrangler leben dort), `package.json` + Lockfile + `engines`; Python nur noch für die Datengenerierung oder ganz ablösen. Chromium-Version über Playwright gepinnt. Einziger dokumentierter Einstieg: `npm ci && npm run build && npm test`.
Abh.: 0.2

**0.5 · [B] · L · CI mit blockierenden Gates**
warum: Kein `.github/`, keine Pipeline, kein Hook. `verify_app.py` muss von Hand gestartet werden und öffnet Seiten per `as_uri()` (Zeile 15/57), also `file://` — dort ist die Seite **kein Secure Context**, das Skript kommentiert es selbst (Zeile 257). `crypto.subtle` ist dort nicht verfügbar; index.html nutzt es an 8 Stellen. Der Zustand, der live geht, ist nie automatisch geprüft.
tun: Jobs als eigenständige, blockierende Gates: build (Zusicherungen, `dist/` als Artefakt) · e2e über **echten HTTP-Server** (Playwright `webServer`, damit Secure Context, CSP und Cache-Header greifen), alle 20 Seiten, chromium+firefox+webkit, light/dark, 8 Breiten ab 320 · lint (html-validate/vnu, stylelint, eslint) · links (lychee mit `--include-fragments`, `href="#"` = Fehler) · a11y (axe-core, 0 serious/critical) · budget · Pixel-Diff gegen `tests/baseline`. Pre-Commit: stylelint/eslint, `build --check`, gitleaks, Größenprüfung; commit-msg: commitlint.
Abh.: 0.3, 0.4

**0.6 · hoch · S · Branch-Modell, Commit-Konvention, Review-Gate**
warum: Ohne Branches passiert jeder Versuch am ausgelieferten Stand. index.html enthält 8 `crypto.subtle`-Aufrufe — keine Änderung daran darf ohne zweiten Blick und grünen Testlauf live gehen. Ohne Konvention ist `changelog.html` nie aus der Historie ableitbar.
tun: Trunk-based, `main` geschützt (linear history, required checks, keine Force-Pushes), `feat/fix/chore`-Branches, Conventional Commits, PR-Pflicht auch im Alleingang, CODEOWNERS auf index.html und Krypto-Pfade, `CONTRIBUTING.md`.
Abh.: 0.1

**0.7 · [B] · M · Domain sichern, Marken-Vorprüfung, Typo-/Homoglyph-Schutz** (Recht + Plattform)
warum: `cloakdrop.com` steht **70×** über 15 Dateien im Auslieferungscode, und der erzeugte Link ist wörtlich `https://cloakdrop.com/#t=…&k=…` (index.html:2459). Gehört die Domain jemand anderem, schickt der Empfänger den Schlüssel im Fragment an einen fremden Origin — der schwerste denkbare Fehler für ein E2EE-Produkt. Zweitens ist die Domain der einzige Vertrauensanker: `cloakdr0p.com` mit derselben Oberfläche liest Schlüssel ab, und nirgends steht, wie ein echter Link aussieht.
tun: Verfügbarkeits- und Ähnlichkeitsrecherche (DPMAregister, EUIPO eSearch plus, WIPO, Handelsregister; Klassen 9/38/42), registrieren; bei Fremdregistrar 60-Tage-Sperre einplanen, danach Transfer zu Cloudflare Registrar (At-Cost, WHOIS-Redaction). Registrar-Lock + 2FA, Auth-Code offline. Defensiv: `.de`, `.app`, `.net`, Ziffern- und Bindestrichvarianten, alle per 301 auf den Apex. Certificate-Transparency-Monitoring auf ähnliche Namen. Kurzer verlinkter Abschnitt „Wie ein echter CloakDrop-Link aussieht" (genau ein Hostname, `#t=`/`#k=`-Form, niemals ein Formular, das nach dem Link fragt) — der Ton aus `contact.html` („Never paste a drop link") ist richtig, steht aber nur dort. **Erst danach irgendetwas deployen.**
Abh.: keine

**0.8 · [B] · S · Launch-Zuschnitt: geschlossene Beta, Go/No-Go, Abbruchbedingung**
warum: 0 Treffer für beta/invite/waitlist/early access; `roadmap.html:1747` kennt nur „Cloud relay — the big one". Rund vierzig Blocker greifen zum ersten Mal gleichzeitig ineinander: erster Upload über Mobilfunk, erster 3-GB-Download, erster paralleler Burn-Abruf, erster Missbrauchsversuch, erste Rechnung. Ein offener Launch ohne Rate Limits und ohne Ausgabendeckel setzt das maximale Risiko in den Moment maximaler Aufmerksamkeit. Ohne definierte Abbruchbedingung ist die einzige schnelle Improvisation „DNS abschalten" — und das nimmt die Statusseite mit.
tun: Geschlossene Beta über Einladungs-Token (das Token-Modell braucht Pro ohnehin, also keine Zusatzarbeit), 2–3 Wochen mit absichtlich niedrigen Grenzen (500 MB pro Drop, harte Tagesobergrenze für alle Uploads). Go/No-Go schriftlich und prüfbar: Löschjob nachweislich grün, Upload-/Download-Erfolgsquote über Zielwert, Rollback einmal echt geübt, Impressum/AGB live, Abuse-Postfach beantwortet, Review-Befunde behoben. Abbruch mit Rückweg: Uploads pausieren, bestehende Drops laufen aus, Statusseite erklärt — nie alles gleichzeitig aus.
Abh.: keine

**0.9 · [B] · S · Rückwärtsterminplan mit Fremdlaufzeiten**
warum: Mehrere Blocker haben Laufzeiten, die niemand verkürzt, und hängen voneinander ab: Gewerbe-/HR-Eintrag → USt-IdNr. → Anbieterkonto beim Zahlungsdienst → Checkout → Widerruf/Kündigungsbutton. Dazu 60-Tage-Domainsperre, HSTS-Preload nach Testphase und praktisch unumkehrbar, DMARC-Rampe `p=none` → `p=reject` über ≥ 2 Wochen, EUIPO 4–6 Monate, externer Krypto-Review mit Terminvorlauf plus Nachbesserungsrunde, Versicherung **vor** dem ersten Request, Steuerberater **vor** der Anbieterwahl. Ohne diese Kette ist jeder Termin geraten, und die weichen Punkte fallen weg, weil die harten nicht fertig sind.
tun: Rückwärtsplanung vom Zieltermin in drei Bahnen (Recht/Steuer, Plattform/Technik, Kommunikation), Fremdlaufzeiten als Fixpunkte. Was nicht rechtzeitig fertig wird, entscheidet über den **Umfang** (0.8), nicht über die Qualität. Beauftragungen mit echtem Datum, als Abhängigkeit geführt.
Abh.: 0.8

**Stufe 0 gesamt: 35–50 PT** (davon ~60 % Punkt 0.3).

---

## Stufe 1 — Rechtsträger & Recht

**1.1 · [B] · M · Rechtsträger, Gewerbe, USt-IdNr.**
warum: `imprint.html` sagt selbst: „CloakDrop does not have one yet — no registered entity, no address to publish." Ohne Rechtsträger gibt es keinen Impressumspflichtigen, keinen Verantwortlichen nach DSGVO, keinen AGB-Verwender, keinen Zahlungsempfänger und kein Anbieterkonto bei Paddle/Stripe.
tun: Rechtsform entscheiden (bei E2EE-Filehoster mit Abuse-Risiko spricht Haftungstrennung für UG/GmbH). Gewerbeanmeldung § 14 GewO, Fragebogen zur steuerlichen Erfassung (ELSTER), USt-IdNr. § 27a UStG (für OSS/Reverse-Charge zwingend), IHK, VBG; bei UG/GmbH Notar, HR-Eintrag, Transparenzregister.
Abh.: 0.9 (Termin) — längste Fremdlaufzeit der Liste.

**1.2 · [B] · S · Impressum vollständig, aus jeder Seite erreichbar**
warum: Neun Pflichtfelder nach § 5 DDG stehen ausdrücklich leer (`imprint.html:1748-1757`, nur E-Mail gefüllt); die Seite nennt die Abmahnfolge selbst (1771). Zusätzlich: `dashboard/received/expired/settings` zeigen den Cookie-Banner (je 8 Treffer `cookie-pop`), verlinken aber **0×** auf imprint oder privacy — Einwilligung ohne Informationszugang.
tun: Name, Rechtsform, Postanschrift (kein Postfach), Vertretung, E-Mail, zweiter Schnellkanal (Telefonnummer ist die sichere Lesart), Registergericht + HRB, USt-IdNr., § 18 Abs. 2 MStV-Verantwortlicher (wegen `blog.html`). Feld „Supervisory authority" streichen, wenn kein erlaubnispflichtiges Gewerbe. Legal-Zeile in den Generator der App-Schale.
Abh.: 1.1, 0.3

**1.3 · [B] · M · Datenschutzerklärung: Launch-Fassung mit Rechtsgrundlagen**
warum: Verantwortlicher, DSB, Aufsichtsbehörde und Hoster stehen als „not filled in", Processors als „none today". Und die Seite beschreibt einen serverlosen Zustand („The page itself makes no requests at all") — beim Launch ist das falsch. Korrektur zur Rohfassung: privacy.html **enthält** „Under the GDPR" (1887), „legitimate interest" (1895) und „controller" (1888/1910/1914/1916) — was fehlt, ist die Zuordnung **je Verarbeitung**.
tun: Zweite Fassung, die zum Launchtag live geht: Verantwortlicher mit Anschrift, Aufsichtsbehörde mit Beschwerdelink, Hoster, Prozessorenliste. Je Verarbeitung Art. 6 zuordnen: Auslieferung/Logs = lit. f mit Interessenabwägung; Annahme/Speicherung/Auslieferung der Chunks = lit. b; Löschautomatik = lit. c + b; Missbrauchsabwehr/Sperrlisten = lit. f, DSA-Pflichten = lit. c; Newsletter = lit. a; Abrechnung = lit. b, Rechnungsaufbewahrung = lit. c. Je Punkt Speicherdauer und Empfänger. R2-Objektzugriffe benennen (Bucket, Objektschlüssel = Drop-ID + Chunk-Index, Bytes, Zeit, IP) und das Rückschlussrisiko: Chunkzahl, -größen und Uploadzeit machen Absender und Empfänger verknüpfbar.
Abh.: 1.1, 1.5, 2.7

**1.4 · [B] · M · Verzeichnis von Verarbeitungstätigkeiten + DSFA-Schwellenwertprüfung**
warum: Art. 30 DSGVO; die Ausnahme unter 250 Beschäftigten greift nicht, weil die Verarbeitung nicht nur gelegentlich erfolgt und ein Drittlandtransfer stattfindet. Anonyme Massenübertragung großer Dateien mit IP-Paar-Metadaten berührt mehrere Kriterien der DSK-Positivliste.
tun: VVT intern (nicht veröffentlichen): Website/Logs, Relay-Speicherung, Newsletter, Kontakt, Pro-Vertrag/Zahlung, Abuse-Meldungen, Betroffenenanfragen — je Zweck, Betroffene, Datenkategorien, Empfänger inkl. Cloudflare und Zahlungsdienstleister, Transfergrundlage, Löschfristen, TOM-Referenz. Schwellenwertanalyse gegen DSK-Liste und WP248 schreiben; auch die Negativentscheidung datiert ablegen und mit dem VVT verknüpfen.
Abh.: 1.3

**1.5 · [B] · M · Cloudflare als Auftragsverarbeiter, Drittlandtransfer, TIA, EU-Jurisdiction**
warum: `grep "Cloudflare"` über alle .html = 0 Treffer, privacy.html sagt „Processors — none today". Ab dem ersten öffentlichen Request verarbeitet Cloudflare IP-Adressen. Korrektur zur Rohfassung: **bei den Self-Serve-Plänen ist das Data Processing Addendum in die Online-Vertragsbedingungen einbezogen; ein separater Akzeptieren-Klick existiert dort nicht mehr.** Der Nachweis besteht aus der archivierten DPA-Fassung mit Datum plus Subprozessorenliste, nicht aus einem Screenshot.
tun: DPA-Fassung + Subprozessorenliste archivieren, Widerspruchsfrist für neue Subprozessoren notieren, je eingesetztes Produkt (Workers, R2, WAF, Turnstile) gegen den Scope prüfen. TIA schreiben: Datenkategorien (IP, Objektzugriffe, Zahlungsdaten), FISA 702 / EO 12333, ergänzende Maßnahmen — hier stark: Inhalte sind clientseitig verschlüsselt, der Schlüssel liegt im Fragment. DPF-Zertifizierung zum Vertragsdatum prüfen und Listeneintrag ablegen, SCC Modul 2 als Rückfall dokumentieren. **Technisch: Jurisdiction `eu` muss bei der Bucket-Erstellung gesetzt werden, ist nachträglich nicht änderbar, und der Zugriff läuft nur über den jurisdiktionsbehafteten Endpunkt bzw. ein entsprechend konfiguriertes Binding — der Location Hint ist best effort und rechtlich wertlos.** Regional Services und Customer Metadata Boundary sind Enterprise: EU-Only für Metadaten ist auf Self-Serve **nicht** erreichbar und darf nicht behauptet werden. Wenn EU-Only zwingend Teil des Versprechens werden soll, ist die Alternative ein EU-Hoster mit eigenem Reverse Proxy — Plattformentscheidung **vor** dem Launch.
Abh.: 1.1, 2.8

**1.6 · [B] · M · Löschkonzept inklusive Backup- und Time-Travel-Fenster** (Recht + Betrieb)
warum: privacy.html gibt selbst zu: „this build contains no delete, no clear and no database removal" und „Elsewhere this site says transfers vanish after seven days. In the preview build that word is wrong." Repoweit kein `.delete(`/`.clear(`/`deleteDatabase`; privacy.html:1898-1903 delegiert Löschung an „clear site data" im Browser. Eine Ebene tiefer, bisher völlig unbeachtet: **D1 führt Time Travel mit 30 Tagen Rückspulfenster, R2 kann Objektversionierung führen, Cloudflare cacht ausgelieferte Objekte, Logs und Support-Mails enthalten Metadaten.** Ein 24-Stunden-Drop wäre damit 30 Tage wiederherstellbar, während die Seite „die Teile sind weg" behauptet — durch mehr Löschcode nicht behebbar, weil es eine Betriebsentscheidung ist.
tun: Aufbewahrungsmatrix: Chunks bis Ablauf 24 h/3 d/7 d bzw. 30 d Pro; Burn sofort nach Erstabruf; Hard-Delete mit Nachlauf und Nachweis; Server-Logs 7 Tage; Newsletter bis Abmeldung; Abuse-Fälle eigene Frist; Rechnungsdaten nach Steuerfristen. **Zwei getrennte Speicher:** Drop-Metadaten in einer Datenbank ohne Backup-Anspruch, deren Time-Travel-Fenster ausdrücklich als Aufbewahrungsfrist in der Matrix steht („nicht mehr abrufbar sofort, endgültig entfernt nach X Tagen") oder auf das Minimum gestellt ist; Abrechnungsdaten in einem eigenen Store mit vollem Backup, damit Steueraufbewahrung und Löschzusage sich physisch nicht treffen. R2-Objektversionierung aus und das nachweisen. Wortwahl auf allen Seiten an die real garantierte Frist anpassen.
Abh.: 2.11

**1.7 · hoch · S · Betroffenenanfragen: Runbook und Identitätsprüfung ohne Konto**
warum: Art. 12 Abs. 3 setzt eine Monatsfrist; privacy.html listet die Rechte, nennt aber keinen Adressaten, keine Frist, keinen Weg („Which one depends on where the operator is established, and that is not settled yet"). Beim kontolosen Design ist die Identifizierung das eigentliche Problem: die einzige Verknüpfung ist die IP im Logfenster oder eine Drop-ID.
tun: Runbook mit benannter Eingangsadresse, Fristenlauf, Identitätsprüfung nach Art. 12 Abs. 6 (nur bei begründeten Zweifeln zusätzliche Angaben, keine Ausweiskopien), Antwortvorlagen Art. 15/16/17/18/20/21, Verlängerung nach Art. 12 Abs. 3 S. 2, Dokumentation. Ausdrücklich beschreiben, warum Inhalte nicht herausgegeben werden können (kein Schlüssel) und dass das keine Verweigerung nach Art. 12 Abs. 5 ist.
Abh.: 1.3

**1.8 · hoch · S · Breach-Runbook (72 Stunden)**
warum: Art. 33/34 plus Dokumentationspflicht nach Art. 33 Abs. 5 auch für nicht gemeldete Vorfälle. Bei einem kontolosen Dienst ist die Betroffenenbenachrichtigung technisch schwierig und muss vorher gelöst sein.
tun: Definition „Kenntnisnahme", Erstbewertung < 24 h, Meldeformular der Landesbehörde vorab ausgefüllt ablegen, Eskalationskontakte, Art.-34-Vorlage, Vorfallregister. Szenarien durchspielen: R2-Bucket öffentlich, Fragment-Leak über Referrer, kompromittiertes Client-JavaScript (hier besonders relevant, weil das Skript den Schlüssel sieht) — für den letzten Fall ist nur ein Website-Hinweis möglich, als Art. 34 Abs. 3 lit. c dokumentieren.
Abh.: 1.3

**1.9 · hoch · M · TOM-Dokumentation nach Art. 32**
warum: Nachweispflicht Art. 5 Abs. 2. Ausgeliefert würde eine Anwendung ohne CSP, HSTS, Referrer-Policy und Permissions-Policy — bei einem Produkt, dessen Sicherheit daran hängt, dass kein fremdes Skript das Fragment liest. Das ist nicht nur ein Security-, sondern ein Nachweisproblem.
tun: TOM-Dokument, das exakt die in 3.1/2.2 gesetzten Maßnahmen beschreibt, plus Kontozugriff mit 2FA und Rollen, Audit-Log, Verschlüsselung at rest, Backup-/Wiederherstellungsaussage, Verweis auf das externe Review (3.8).
Abh.: 3.1, 3.8, 5.8

**1.10 · hoch · S · § 25 TDDDG: Einordnung aller neun Speicherzugriffe, Konto-Attrappe entfernen**
warum: privacy.html:1735 sagt „Four localStorage keys. All four are listed below" — tatsächlich sind es **neun**: `-theme`, `-consent`, `-received-cleared`, `-default-expiry`, `-burn`, `-side-collapsed`, `-dense`, `-session`, `-account`. Eine abgeschlossene Aufzählung, die über die Hälfte unterschlägt, ist mit einem Grep in zehn Sekunden widerlegt. **Korrektur zur Rohfassung: `cloakdrop-account` ist nicht nach § 25 Abs. 2 Nr. 2 einwilligungsfrei** — es gibt keinen Dienst, für dessen Erbringung E-Mail + Passwort-Digest „unbedingt erforderlich" wären, die Kontoansicht ist eine Attrappe.
tun: Alle neun Schlüssel mit Inhalt, Auslöser und Löschweg aufnehmen, oder die App-Einstellungen in einen JSON-Schlüssel zusammenfassen und dann wieder von einer wirklich vollständigen Liste sprechen; „Those four points are the whole story" (1738) mitkorrigieren. Einordnung schriftlich: `-theme` = Nr. 2 (gewünschte Einstellung), `-consent` = Speicherung der Entscheidung selbst, IndexedDB `transfers`/`chunks` = die Dienstleistung selbst; `-account`/`-session` **entfernen** statt einzuordnen.
Abh.: 1.21

**1.11 · [B] · M · Cookie-Banner und Analytics-Entscheidung** (Recht + Wahrheit)
warum: Der Text „We use cookies to collect data and improve our services" ist dreifach falsch (keine Cookies, keine Erhebung, keine Auswertung) und steht in **allen 20 Dateien** (`cookie-pop` überall; nur der Footer-Link „Cookie settings" 16×). „Learn more" zeigt auf `index.html#faq` (in index.html selbst auf `#faq`), nicht auf die Datenschutzerklärung — die von § 25 TDDDG und Art. 13 verlangten Informationen sind nicht erreichbar. privacy.html widerlegt den Banner selbst: „document.cookie is never touched anywhere on this site", der Analytics-Schalter „currently controls nothing". Eine ins Leere gerichtete Einwilligung ist unwirksam (Art. 4 Nr. 11), der Banner selbst ein Transparenzverstoß (Art. 5 Abs. 1 lit. a) und irreführend (§ 5 UWG). Bei einem Privacy-Produkt ist es die Stelle, die jeder Prüfer zuerst anfasst.
tun: Entscheidung, nicht per Default. Empfehlung: Banner samt Analytics-Schalter und Präferenzspeicherung ersatzlos entfernen — die verbleibende Speicherung ist nach § 25 Abs. 2 Nr. 2 einwilligungsfrei. Nutzungszahlen serverseitig über Workers Analytics Engine im Relay-Worker (5.5), dann bleibt der Client requestfrei und `verify.html`-Check 1 stimmt weiter. **Wenn stattdessen Cloudflare Web Analytics: cookiefrei heißt einwilligungsfrei nach § 25 TDDDG (kein Zugriff auf Endgeräte-Informationen), aber es bleibt eine Verarbeitung nach DSGVO, die eine Rechtsgrundlage (Art. 6 Abs. 1 lit. f), die Aufnahme von Cloudflare als Auftragsverarbeiter und eine Drittlandbewertung braucht** — dann Snippet manuell hinter einen echten Consent (Default aus), `static.cloudflareinsights.com` in die CSP und die drei Absätze in privacy.html plus Check 1 in verify.html anpassen. Falls der Banner bleibt: Einwilligungsnachweis um Textversion, Policy-Version, Sprache, abgefragte Zwecke und Widerrufsereignisse erweitern (heute nur `{choice, ts}`).
Abh.: 0.3, 5.5

**1.12 · [B] · M · Nutzungsbedingungen und ihre wirksame Einbeziehung**
warum: `index.html:1858` lässt Nutzer „By clicking any buttons below you agree to our Terms of Service" zustimmen — der Link ist `href="#"`, `terms.html` existiert nicht. Ohne AGB gibt es keine Leistungsbeschreibung, keine Haftungsbegrenzung, keine Sperr- und Kündigungsrechte und keine der von DSA Art. 14 verlangten Angaben; ohne zumutbare Kenntnisnahme (§ 305 Abs. 2 BGB) gelten sie nicht.
tun: `terms.html`: Vertragsgegenstand mit ausdrücklicher Abgrenzung (kein Backup, kein Archiv, kein Virenscan, Ablauf ist Feature), Limits Free/Pro, Verfügbarkeit ohne SLA, Sperr- und Kündigungsrechte, Haftung nach deutschem Recht, Datenverlustrisiko bei verlorenem Fragment-Link, Rechtswahl und Verbrauchergerichtsstand. **Änderungsvorbehalt: keine Zustimmungsfiktion — BGH XI ZR 26/20 (27.04.2021) hat pauschale Zustimmungsfiktionsklauseln in Verbraucher-AGB nach § 307 BGB für unwirksam erklärt. Tragfähig ist nur ein auf konkrete, benannte Anlässe begrenzter Vorbehalt plus ausdrückliche Zustimmung oder Sonderkündigungsrecht mit Ankündigungsfrist** — gilt auch für Änderungen der Free-Limits. Echten Link im Auth-Formular, Text auf die konkrete Aktion beziehen statt „any buttons", bei Vertragsschluss separate, nicht vorbelegte Checkbox mit Datum. Bis die Seite existiert: Auth-Ansicht nicht ausliefern.
Abh.: 1.1, 1.13, 1.14

**1.13 · [B] · S · Acceptable Use Policy**
warum: `grep "Acceptable"` = 0. Ein anonymer Hoster für große Blobs ohne Konto wird für CSAM, Malware, Leaks und Phishing benutzt. Ohne AUP fehlt die vertragliche Grundlage zu sperren und die Basis für das DSA-Verfahren.
tun: `aup.html` mit abschließend benannten Verboten (rechtswidrige Inhalte, sexualisierte Gewalt gegen Minderjährige, Malware, Phishing-Hosting, Urheberrechtsverletzung, Umgehung von Größenlimits durch Splitting, automatisierte Massennutzung, jugendgefährdende Inhalte), Durchsetzungsmitteln (Drop sperren, Link invalidieren, IP-Rate-Limit) und Eskalationsstufen. Aus den AGB verlinken.
Abh.: 1.12

**1.14 · [B] · L · DSA: Meldeverfahren, Kontaktstellen, Transparenz, Sperrmechanik, Abuse-Betrieb** (Recht + Betrieb + Missbrauch)
warum: Sobald das Relay fremde Dateien speichert, ist der Betreiber Hostingdienst nach **Art. 3 lit. g Ziff. iii DSA**. Repoweit 0 Treffer für abuse/takedown/DSA; die einzige Erwähnung (`opensource.html:1764` „Billing and abuse handling") behauptet fälschlich vorhandenen Code. **Korrektur: das Haftungsprivileg folgt aus Art. 6 DSA, nicht aus „§ 10 DDG" — mit dem DDG (in Kraft seit 14.05.2024) sind die §§ 7–10 TMG entfallen; Bußgeldtatbestände stehen in § 33 DDG, die Koordinierungsstelle bei der Bundesnetzagentur in § 12 DDG.** Zero-Knowledge und Art. 16 kollidieren praktisch: Inhalte sind nicht prüfbar, und **Hash-Sperrlisten (PhotoDNA, NCMEC) funktionieren prinzipiell nicht, weil jeder Drop einen frischen Zufallsschlüssel bekommt** — derselbe Klartext ist zweimal völlig unterschiedlicher Ciphertext.
tun: `report.html` mit den Pflichtfeldern aus Art. 16 Abs. 2 (Begründung, exakte URL bzw. Drop-ID, Name und Mail des Melders mit Opt-out bei Straftaten gegen Minderjährige, Richtigkeitsbestätigung), automatische Empfangsbestätigung, Entscheidung mit Begründung nach Abs. 5 — braucht den Endpunkt aus 2.19. Kontaktstellen nach Art. 11 (Behörden/Kommission/Koordinator) und Art. 12 (Nutzer) mit Amtssprachen Deutsch und Englisch; Sitz in der EU heißt kein Vertreter nach Art. 13 — Feststellung dokumentieren. Art.-14-Abschnitt in den AGB: was gesperrt werden kann, auf welcher Informationsbasis (nur Meldungen, Größen- und Zugriffsmuster — nie Inhalt), dass keine automatisierte Inhaltsprüfung möglich ist, wer entscheidet, welche Fristen gelten. Statement-of-Reasons-Vorlage (Art. 17) und Meldeweg an LKA/BKA (Art. 18) als Runbook. Ersatzplan statt Hash-Listen aufschreiben und umsetzen: dokumentiertes Vier-Augen-Verfahren zur Verifikation eines gemeldeten Drops (die Meldung enthält typischerweise den vollständigen Link, der Melder legt den Schlüssel damit freiwillig offen — das muss im Formular ausdrücklich so erklärt sein), Sperrliste über Drop-IDs (`state='blocked'`, sofort an allen Lese-Endpunkten wirksam, Objekt bleibt für Beweiszwecke mit eigener Frist), Verhaltensheuristiken ohne Inhaltszugriff (Uploadvolumen pro IP/ASN, ein Upload plus hunderte Abrufe = Verteilung, referrer-lose Massenabrufe), klare Grenze in der AUP. Konsequenz benennen: proaktiv nicht filterbar, nur reaktiv sperrbar. **Cloudflares AUP beachten: großvolumige Nicht-HTML-Auslieferung über den CDN-Cache ist auf Nicht-Enterprise-Plänen eingeschränkt — R2 mit Worker-Streaming ist der zulässige Weg, Chunks nie über cachebare Zone-Pfade.**
Abh.: 1.12, 1.13, 2.13, 2.19

**1.15 · hoch · S · Behördenanfragen und Logpolitik als Zusage**
warum: Anordnungen nach Art. 9 und Auskunftsanordnungen nach Art. 10 DSA sind fristgebunden. Ohne vorab festgelegte Logpolitik entstehen genau die Daten, die man nicht haben will, oder es fehlen die, die man zur Missbrauchsabwehr braucht — und Cloudflare-Logs fallen standardmäßig an.
tun: Runbook: Eingangskanal (Art.-11-Adresse), Identitäts- und Zuständigkeitsprüfung, Rechtsgrundlagenprüfung, Fristen, Umfang des technisch Möglichen (Ciphertext, Zeitstempel, IPs im Logfenster — keine Schlüssel, keine Dateinamen), Benachrichtigung nach Art. 9 Abs. 2 lit. c, Dokumentation. Dass Cloudflare Anfragen an den Kunden weiterleitet, klären und die dortige Kontaktadresse aktuell halten. Das Fragment-Design ist hier ein dokumentierbarer Vorteil: vollständige URLs mit Schlüssel können in Serverlogs strukturell nicht auftauchen.
Abh.: 5.6

**1.16 · mittel · S · Regulatorische Einordnung datiert ablegen**
warum: Ob nur Hostingdienst oder zusätzlich NIKD entscheidet über Auskunftspflichten. Ohne festgehaltene Einordnung wird die erste Behördenanfrage falsch beantwortet. **Korrektur: Art. 24 DSA (Transparenzbericht) gilt nur für Online-Plattformen; für reines Hosting ist Art. 15 die Norm, und Art. 15 Abs. 2 nimmt Kleinst- und Kleinunternehmen ausdrücklich aus. Art. 19 befreit zusätzlich von Abschnitt 3.**
tun: Kurzgutachten: Hostingdienst nach Art. 3 lit. g Ziff. iii ja; keine Online-Plattform, weil Inhalte nicht öffentlich verbreitet werden (Art. 3 lit. i) — diese Begründung ist für die Befreiung tragend; kein NIKD (§ 3 Nr. 40 TKG). **NIS-2 lässt sich datiert verneinen, nicht nur „prüfen": das NIS2-Umsetzungsgesetz ist verabschiedet, ein Ein-Personen-Betrieb liegt unter den Schwellen (50 Beschäftigte / 10 Mio. € Umsatz) und fällt in keinen Sektor der Anlagen 1/2.** Kleinstunternehmensstatus nach Empfehlung 2003/361/EG mit Beschäftigtenzahl und Umsatz festhalten, jährliche Wiedervorlage, Befreiung entfällt zwölf Monate nach Überschreiten. Bei Feature-Änderungen (Receive inbox, Drop-Adressen) erneut prüfen.
Abh.: 1.1

**1.17 · [B] · M · Verbraucherrecht: Widerruf, Button-Lösung, Kündigungsbutton**
warum: `pricing.html:1920` sagt zu „At launch, EU consumers get their statutory 14-day withdrawal right", `grep "Widerruf"` = 0. Der Bestellbutton heißt „Get notified". „Cancel anytime" und „cancelling is one click" (1826/1920) stehen schriftlich, ein Kündigungspfad existiert nicht. **Korrektur: Pro ist eine digitale Dienstleistung, also § 356 Abs. 4 BGB — nicht Abs. 5 (der betrifft digitale Inhalte). Und Abs. 4 lässt das Widerrufsrecht nur bei vollständiger Erbringung erlöschen; bei einem laufenden Monatsabo tritt das nie ein. Der Verbraucher kann 14 Tage widerrufen; mit vorherigem ausdrücklichem Verlangen und Belehrung schuldet er Wertersatz pro rata nach § 357 Abs. 8 BGB.** Ein Checkout-Schritt „Zustimmung zum Erlöschen" läuft beim Abo leer.
tun: Widerrufsbelehrung + Muster-Widerrufsformular als Seite und als Anhang der Bestellbestätigung. Im Checkout: nicht vorbelegte Zustimmung zum vorzeitigen Leistungsbeginn **plus Wertersatz-Belehrung** (ohne sie entfällt sogar der Wertersatz). § 312j Abs. 3: Bestellübersicht mit Leistung, Gesamtpreis inkl. USt, Abrechnungsintervall, Mindestlaufzeit, Kündigungsfrist unmittelbar über dem Button, Beschriftung „Zahlungspflichtig bestellen" — gilt auch für „Upgrade plan" aus settings.html. § 312k: zwei Buttons („Verträge hier kündigen" + Bestätigung), speicherbare Kündigungserklärung, Zugangsbestätigung mit Vertragsendedatum, Verlinkung im Footer aller Seiten, ohne Login-Zwang. Kundenportale von MoR-Anbietern ersetzen die deutsche Button-Pflicht auf der eigenen Website nicht.
Abh.: 1.20

**1.18 · [B] · S · Preisangaben nach PAngV**
warum: Vier Stellen nennen Preise ohne Steuerangabe (`pricing.html:1825/1861`, `index.html:2071`, `api.html:2161/2208`); der einzige Treffer für VAT ist `imprint.html:1755` „VAT identification number — not filled in". **Korrekturen: § 3 PAngV verlangt den Gesamtpreis, und „Gesamtpreis" ist nach § 2 Nr. 3 PAngV definitionsgemäß der Preis einschließlich Umsatzsteuer — ein zusätzliches „inkl. MwSt." ist nach PAngV nicht zwingend, ein Verstoß liegt nur vor, wenn 4,99 € netto sind. Die Informationspflicht über Steuern folgt aus Art. 246a § 1 Abs. 1 Nr. 4 EGBGB. Der Jahrespreis existiert entgegen der Rohfassung (`pricing.html:2244` „Billed yearly as €49.90 — 2 months free.", :2245 „€49.90/yr") — er ist nur ohne JavaScript unsichtbar, und die Rabattaussage nennt keinen Bezugspreis; sie wird nach § 5 UWG beurteilt, nicht nach § 11 PAngV.**
tun: Brutto/Netto entscheiden und konsistent auszeichnen; bei B2B-Ausweisung getrennte Darstellung mit Adressatenkennzeichnung und USt-ID-Prüfung im Checkout. Jahrespreis ohne JavaScript sichtbar, Monatsäquivalent nennen, Rabattaussage mit Bezugspreis belegen. Bei MoR-Anbietern länderabhängige Bruttopreise ankündigen („Endpreis je Land im Checkout"). „€0 forever" (1809) gegen „Limits may evolve" (1908) angleichen und den Änderungsvorbehalt aus 1.12 dazu.
Abh.: 1.19

**1.19 · [B] · L · Steuern, Buchhaltung, Geschäftskonto** (Recht + Betrieb)
warum: Keine Steuerlogik, keine Länderkonfiguration, kein Konto, keine Belegablage. **Korrekturen zur Rohfassung, die auf Stand 2024 war: (a) „Die Kleinunternehmerregelung deckt Auslandsumsätze nicht" ist seit 01.01.2025 überholt — die EU-Kleinunternehmerregelung (§ 19a UStG, Kleinunternehmer-Meldeverfahren beim BZSt, unionsweite Schwelle 100.000 €) erlaubt genau die Steuerbefreiung in anderen Mitgliedstaaten. (b) Unterhalb von 10.000 € EU-weitem Umsatz mit auf elektronischem Weg erbrachten Leistungen bleibt der Leistungsort nach § 3a Abs. 5 UStG Deutschland — ohne OSS, ohne Länder-Steuersätze; für einen Launch mit 4,99 €/Monat ist das der realistische Ausgangszustand. (c) § 19 UStG hat seit 2025 neue Grenzen: 25.000 € Vorjahr / 100.000 € laufendes Jahr, netto.** Zusätzlich ungeregelt: Cloudflare rechnet in **USD** ab (5 USD Workers, 0,015 USD/GB, 4,50 USD/Mio. Klasse A), Erlöse stehen in EUR — Kursdifferenzen schmälern eine Marge von 4,99 € spürbar, und der Leistungsbezug ist ein Reverse-Charge-Fall nach § 13b UStG mit eigener Zeile in der Voranmeldung und Pflicht zur USt-IdNr.
tun: Mit Steuerberater klären: Kleinunternehmer oder Regelbesteuerung, § 3a Abs. 5-Schwelle, MoR oder eigene OSS-Registrierung beim BZSt, USt-IdNr. **Diese Klärung vor der Anbieterwahl, sie entscheidet sie mit.** Geschäftskonto vor der ersten Cloudflare-Rechnung. Buchhaltung mit Belegautomatik, Reverse-Charge-Buchungsschlüssel, Voranmeldungsturnus. Rechnungsstellung mit Pflichtangaben nach § 14 UStG (bzw. Beleg des MoR), Nummernkreis, Versand und Download; Aufbewahrung 8 Jahre für Rechnungen und Buchungsbelege (§ 14b UStG, § 147 AO in der ab 2025 geltenden Fassung), 10 Jahre für Handelsbücher und Abschlüsse — als eigene Zeile im Löschkonzept und als Art. 6 Abs. 1 lit. c in der Datenschutzerklärung, damit der Widerspruch zum Minimierungsversprechen erklärt ist. **E-Rechnungspflicht nach § 14 UStG: Empfangspflicht im inländischen B2B seit 01.01.2025, Ausstellungspflicht gestaffelt ab 2027/2028** — für B2C irrelevant, für Geschäftskunden relevant. GoBD-konforme, unveränderbare Ablage **plus Verfahrensdokumentation** — die wird bei digitalen Belegen als Erstes verlangt. USD-Kostenpuffer und Wechselkursannahme in die Preiskalkulation. Nebenbei: `expired.html:2960/2963` zeigt eine erfundene `invoice-2209.pdf` als Demodatensatz, während keine Rechnungsfunktion existiert.
Abh.: 1.1

**1.20 · [B] · L · Zahlungsanbieter, Rollenverteilung, Entitlement, Dunning**
warum: `pricing.html` zeigt zwei Preise und eine Vergleichstabelle, der Pro-Knopf heißt „Get notified" und scrollt zum Newsletter; kein Checkout, kein Kundenobjekt, kein Webhook. Zahlungsdaten sind die einzige Verarbeitung, die das Zero-Knowledge-Versprechen nicht abdeckt. Zusätzlich verspricht `pricing.html:1916` ein präzises Verhalten bei Zahlungsausfall: „Existing links keep the lifetime they were created with. New drops simply use the free limits again." **Korrektur: „Stripe Billing = kein MoR" gilt nicht mehr pauschal — Stripe bietet inzwischen ein Merchant-of-Record-Modell (Managed Payments); Lemon Squeezy gehört seit 2024 zu Stripe und wird dort konsolidiert, ist als Neuempfehlung für 2026 unsicher. Paddle bleibt tragfähig. Produktstand vor der Wahl prüfen.**
tun: MoR wählen, damit die Steuerpflicht nicht beim Betreiber landet; Rollenverteilung im Vertrag festhalten (AVV bei Auftragsverarbeitung, Art. 26 falls gemeinsam, oder MoR-Konstruktion), Drittlandtransfer separat bewerten, Datenkategorien und Fristen ins VVT und in privacy.html. Hosted Checkout verlinken statt „Get notified", **niemals im selben Origin wie der Composer einbetten** — die CSP soll `form-action` restriktiv halten, also externer Redirect. Webhook im Worker mit Signaturprüfung, Secret aus dem Secrets Store, idempotente Verarbeitung von `subscription.created/updated/canceled/payment_failed`; Planzustand nach D1, Plan-Limits als Feature-Flags nach KV. Entitlement kontofrei lösen: signiertes, nicht identitätsgebundenes Token mit `exp` und Quote, das der Client bei `POST /v1/drops` mitsendet. Dunning-Kette: `payment_failed` → Anbieter-Retries → Kulanzfenster mit Mail → Downgrade auf Free-Quote, **wobei bestehende Drop-Datensätze ihre `expires_at` behalten — die Ablauffrist wird beim Erzeugen persistiert, nicht aus dem Planzustand abgeleitet.** Bis das steht, `pricing.html:1916` auf „geplant" umschreiben.
Abh.: 1.1, 1.19, 1.21, 2.7

**1.21 · [B] · M · Kontomodell entscheiden, Passwort-Attrappe entfernen** (Recht + Krypto)
warum: `index.html:1937` bewirbt „no account", `settings.html:2940` sagt „CloakDrop has no accounts", gleichzeitig zeigt index.html eine vollständige Auth-Ansicht mit SSO, Google, GitHub, Apple und Passkey (1821-1834) und `roadmap.html:1752` führt „Real accounts" als in Arbeit. Drei Seiten widersprechen sich über die zentralste Produkteigenschaft. Dahinter liegt `hashPw`: `SHA-256('cloakdrop:' + pw)` (index.html:2514) landet mit E-Mail in `localStorage` (:2634) — kein Salt, kein Work Factor, mit Wortliste in Sekunden rückrechenbar; privacy.html:1840-1846 legt das selbst dar. Auf einer Seite, die Sicherheit verkauft, ist das das tödliche Detail für jeden Reviewer. Für ein Abo ist der Widerspruch nicht kosmetisch: Rechnung, Kündigungsbutton, Widerruf und Zahlungsausfall-Kommunikation brauchen einen dauerhaften Kanal zum Kunden.
tun: Eine Option wählen und alle 20 Seiten daran ausrichten. Empfehlung: „Kein Konto zum Senden und Empfangen — bezahlen erzeugt einen Abrechnungsdatensatz": Free bleibt kontofrei, Pro erhält ein minimales Konto (E-Mail + Magic Link), Kündigung und Rechnungen über das Anbieterportal. Alternativen: Lizenz-Token per Mail (Konto verschoben, nicht beseitigt; verlorenes Token braucht Wiederherstellung), anonyme Gutscheine über blinde Signaturen (passt zur Haltung, macht Widerruf und Kündigung praktisch unbedienbar), Prepaid ohne Abo (umgeht Kündigungsbutton und Dunning vollständig). Sofort und unabhängig: Auth-Ansicht und Passwortfeld aus der Auslieferung nehmen, die fünf nicht verdrahteten Anbieterknöpfe entfernen statt sie zu bezetteln, `cloakdrop-account`/`-session` bei bestehenden Besuchern aktiv löschen (2.18). Wenn je echte Auth kommt: Argon2id serverseitig (oder PBKDF2-SHA-256 ≥ 600.000 Iterationen), Per-Nutzer-Salt, Pepper, HttpOnly-Cookie im Worker validiert, **niemals Passwortmaterial clientseitig persistiert**; App-Seiten per Cache Rule auf `no-store` und `X-Robots-Tag: noindex`.
Abh.: 0.8

**1.22 · hoch · M · Markenanmeldung**
warum: „CloakDrop" steht in Wortmarke, Domain und Produktversprechen auf 20 Seiten; `grep "trademark"` = 0. Eine kollidierende ältere Marke bedeutet nach Launch Unterlassung, Umbenennung und Kosten — der teuerste Zeitpunkt. Eintragung dauert 4–6 Monate, die Anmeldung muss also in Stufe 1 liegen.
tun: Recherche aus 0.7 auswerten; bei Freiheit EU-Wortmarke anmelden, hilfsweise DE-Marke; Nutzung dokumentieren, um Priorität belegen zu können.
Abh.: 0.7

**1.23 · [B] · M · Lizenzen: AGPL, Schriften, Fremdlogos, expo.dev-Kommentar**
warum: `opensource.html` ist als bindende Zusage formuliert („This page is the commitment, in writing", „An AGPL licence keeps every fork open", CTA „Star us on GitHub" auf `href="#"`), AGPL wird 9× genannt, der Lizenztext existiert 0× — der Code ist damit „alle Rechte vorbehalten", die Seite verspricht etwas, das die Auslieferung verbietet. Inter und JetBrains Mono liegen als Base64 in allen 20 Dateien (privacy.html:1853 wirbt damit), stehen unter OFL und verlangen Copyright- und Lizenzweitergabe — im Projekt liegt kein Lizenztext. Die 8 Messenger-Logos in der CHANNELS-Liste von index.html sind fremde Marken, als `image/png` deklariert, tragen aber JPEG-Magic `ffd8ffe0` plus Exif/Photoshop-Metadaten inklusive Erzeuger-Kennung. Und Zeile 32/33 **jeder** der 20 Dateien enthält im Klartext „Design-Tokens 1:1 aus expo.dev", während `imprint.html:1796` behauptet „The text and design of this site belong to its authors".
tun: `LICENSE` mit vollem AGPL-3.0-Text, SPDX-Kopfzeile `AGPL-3.0-or-later`, § 13 AGPL beachten (Quellcode-Angebot für Netzwerknutzer, konkret Link im Footer), Abgrenzung Client/Relay lizenzrechtlich dokumentieren, Repo veröffentlichen — oder AGPL-Zusage und GitHub-CTA entfernen, bis es das Repo gibt. `licenses/` mit beiden OFL-Texten und Copyright-Zeilen, Abschnitt „Schriften" im Impressum. Markenrichtlinien der 8 Messenger prüfen oder durch neutrale Glyphen ersetzen; MIME-Typen korrigieren, Metadaten strippen. expo.dev-Kommentar aus allen 20 Dateien entfernen, „Expo-style" in `changelog.html:1771` umformulieren, tatsächlich übernommene Tokens prüfen (Farben, Radien, Easing kaum schutzfähig; ein vollständig übernommenes Komponentensystem ist eine andere Frage) und `imprint.html:1796` an die Wahrheit anpassen. Personenfotos in changelog/blog zusätzlich auf das Recht am eigenen Bild prüfen.
Abh.: 0.1

**1.24 · [B] · M · BFSG-Betroffenheit feststellen, Informationen zur Barrierefreiheit**
warum: `grep -i 'accessibilit|barrierefrei|WCAG|BFSG|EN 301'` = 0 in allen 20 Dateien. Das BFSG gilt seit **28.06.2025** für entgeltliche B2C-Dienstleistungen des elektronischen Geschäftsverkehrs; der Pro-Plan macht CloakDrop dazu. Heute ist der 30.07.2026 — die Frist ist über ein Jahr verstrichen. **Korrektur: für private Dienstleistungsanbieter gilt § 14 BFSG i.V.m. § 3 BFSGV und Anlage 3 BFSGV — „Informationen zur Barrierefreiheit", bereitzustellen in den AGB bzw. mit der Dienstleistung, mit Beschreibung, wie die Anforderungen erfüllt werden. Die „Erklärung zur Barrierefreiheit" samt Feedback-Mechanismus und Hinweis auf die Durchsetzungsstelle ist öffentlich-rechtliches Recht (§ 12b BGG, BITV 2.0) und gilt für einen privaten Anbieter nicht. Zuständig ist die Marktüberwachungsstelle der Länder für Barrierefreiheit von Produkten und Dienstleistungen (MLBF), Schlichtung nach § 16 BGG. Maßstab: EN 301 549 / WCAG 2.1 AA. Kleinstunternehmensausnahme für Dienstleistungen nach § 3 Abs. 3 BFSG: < 10 Beschäftigte und ≤ 2 Mio. € Jahresumsatz oder Bilanzsumme** — beurteilbar erst nach 1.1.
tun: Status feststellen und schriftlich festhalten. Wenn nicht befreit: Selbstbewertung gegen EN 301 549 für Composer, Empfangsansicht, Auth, Bestellstrecke und App-Schale, Informationen zur Barrierefreiheit in den AGB, Prüfergebnisse fünf Jahre aufbewahren. **Vorher die Blocker aus 4.5 abarbeiten, sonst ist der Text ein schriftliches Schuldeingeständnis** — die dort belegten Verstöße (13 Phantom-Tabstopps, `--text-quaternary` bei 3,30:1, fehlende Live-Regionen, kein Skip-Link) sind genau die, die in einer Beschwerde zuerst auftauchen.
Abh.: 1.1, 4.5

**1.25 · [B] · M · Jugendschutz, Mindestalter, CSAM-Runbook**
warum: 0 Treffer für Alter/Mindestalter/Minderjährige/Jugendschutz. Drei Folgen: ein Abo, das ein 15-Jähriger abschließt, ist nach §§ 104 ff. BGB schwebend unwirksam; die Newsletter-Einwilligung Minderjähriger unterliegt Art. 8 DSGVO; und ein anonymer Filehoster ohne Inhaltseinsicht ist der klassische Kanal für strafbare Inhalte.
tun: Mindestalter in den AGB (18 für Pro, 16 für kostenlose Nutzung), im Checkout als nicht vorbelegte Selbstauskunft mit Zeitstempel. § 7 JMStV (Jugendschutzbeauftragter) prüfen und die Einordnung datiert ablegen — auch ein negatives Ergebnis ist ein Nachweis. **Getrenntes Runbook für den CSAM-Verdachtsfall, das den Betreiber schützt: Sofortsperre über die Drop-ID, keine Sichtung über das für die Meldung Erforderliche hinaus, Beweissicherung ohne Kopie in private Ablagen, Meldung an das BKA, Vier-Augen-Dokumentation, Aufbewahrungsfrist.** Diese Handlungsanweisung fehlt vollständig und ist der Fall, in dem Improvisation den Betreiber strafrechtlich exponiert.
Abh.: 1.13, 1.14

**1.26 · [B] · S · Versicherungen**
warum: Nicht im Repo, nirgends erwähnt (0 Treffer insurance/Versicherung, auch in `tasks/todo.md`). Das Produkt bündelt Risiken, die ein Einzelbetreiber nicht selbst tragen kann und die diese Liste selbst belegt: abmahnfähige Zustände (Impressum, Preisangaben, UWG), Schadensersatz nach Art. 82 DSGVO bei Schlüssel-Leak über kompromittiertes Client-JavaScript, Nutzer-Datenverlust durch Burn- oder Ablauffehler, ungedeckelte Cloud-Rechnung. **Entscheidend für den Zeitpunkt: Versicherer decken keine Vorfälle, die vor Vertragsbeginn eingetreten sind — nach dem ersten öffentlichen Request ist der Abschluss für alles Laufende wertlos.**
tun: Vermögensschaden-/Berufshaftpflicht für IT-Dienstleister mit ausdrücklicher Deckung von Datenschutzverstößen und Vertragsstrafen; Cyber-Police mit Betriebsunterbrechung, Forensik, Meldekosten, Krisenkommunikation; Rechtsschutz **mit ausdrücklichem Baustein Wettbewerbs- und Urheberrecht** (Standardpolicen schließen genau das oft aus, und genau das ist hier das wahrscheinlichste Ereignis). Deckungssumme gegen ein durchgerechnetes Szenario wählen, nicht gegen den Beitrag. Vor dem ersten öffentlichen Request.
Abh.: 1.1

**1.27 · mittel · S · Exportkontrolle und Sanktionen**
warum: AES-256 fällt grundsätzlich unter Kategorie 5 Teil 2 der VO (EU) 2021/821; für Massenmarkt- und öffentlich zugängliche Software greifen Ausnahmen, die festgestellt sein müssen. **Korrektur: die US-Notifikation nach 15 CFR § 742.15(b) greift erst mit der Veröffentlichung des Quellcodes — heute ist nichts veröffentlicht. Die Pflicht entsteht zeitgleich mit dem AGPL-Release und gehört in dessen Checkliste, nicht in die des Website-Launches; der Betrieb der Website löst sie nicht aus.**
tun: Einstufung schriftlich (5A002/5D002, Kryptografie-Anmerkung, Allgemeine Software-Anmerkung; erwartbar keine Genehmigungspflicht) mit Begründung ablegen. Sanktionsseite: Geoblocking für vollständig embargierte Gebiete bewerten — der Zahlungsdienstleister prüft Listen, der kostenlose Dienst nicht.
Abh.: 1.23

**1.28 · mittel · S · § 36 VSBG**
warum: `imprint.html` lässt es ausdrücklich offen. Der Hinweis auf die zum 20.07.2025 eingestellte EU-ODR-Plattform ist korrekt entfernt (die drei „ODR"-Treffer liegen ausschließlich in Base64-Fontdaten).
tun: Entscheidung treffen, genau ein Satz in Impressum und AGB; bei Nichtteilnahme ausdrücklich „nicht verpflichtet und nicht bereit", Beschäftigtenzahl als Ausnahmegrund dokumentieren.
Abh.: 1.1, 1.12

**Stufe 1 gesamt: 30–45 PT eigene Arbeit + 6–10 Wochen Fremdlaufzeit** (Anwalt, Steuerberater, Versicherung, Marke, Registereintrag).

---

## Stufe 2 — Plattform & Backend

**2.1 · [B] · S · DNS-Zone, Records, DNSSEC, CAA**
warum: Ohne Zone kein Host, kein Zertifikat, keine Mail, keine `api`-Subdomain — `api.html:1958` verlangt `api.cloakdrop.com`. Ohne DNSSEC ist Zonenmanipulation nicht erkennbar, ohne CAA kann jede CA ein Zertifikat ausstellen; `verify.html` sagt selbst, dass die Checks nicht beweisen, dass cloakdrop.com die gelesene Quelle ausliefert.
tun: Zone als Free-Plan anlegen, beide Nameserver beim Registrar setzen, Proxy für alle Web-Records orange. Records: Apex als Custom Domain des Workers, `www` CNAME + 308 auf Apex, `api` als Worker-Route, MX für Email Routing, TXT für SPF/DMARC, CAA. Keine ungenutzten Wildcards. DNSSEC aktivieren, DS-Record beim Registrar, Validierung prüfen. **CAA: dieselben CAs in `issue` UND `issuewild` freigeben — `0 issuewild ";"` blockiert das Renewal, weil Universal SSL für die Zone ein Zertifikat inklusive `*.cloakdrop.com` ausstellt.** Für Universal SSL: `pki.goog`, `letsencrypt.org`; bei Advanced Certificate Manager zusätzlich `ssl.com`, `sectigo.com`; `0 iodef "mailto:security@cloakdrop.com"`. Liste vor jeder Zertifikatsumstellung nachziehen. **Zone Hold aktiv lassen** (verhindert Aufnahme derselben Domain in ein fremdes Konto), **„Pause Cloudflare on Site" aus.**
Abh.: 0.7

**2.2 · [B] · S · HTTPS-Erzwingung, HSTS, Mindest-TLS**
warum: Ohne Always Use HTTPS ist `http://cloakdrop.com/#t=…&k=…` erreichbar und der erste Request geht unverschlüsselt hinaus — genau in diesem Request steckt der Schlüssel. Ohne HSTS bleibt ein SSL-Stripping-Fenster beim allerersten Aufruf offen; wer HTML ausliefert, ist bei einer E2EE-App gleichbedeutend mit „Schlüssel gestohlen". Zusätzlich: `crypto.subtle` existiert nur im Secure Context, ein http-Downgrade tötet die App still. **Korrekturen: neu angelegte Zonen erhalten seit 2023 „Automatic SSL/TLS" und werden auf die sicherste funktionierende Stufe gesetzt — „Flexible" wird für Neuzonen nicht mehr vorbelegt. Und der SSL/TLS-Verschlüsselungsmodus regelt ausschließlich Cloudflare → Origin; bei Workers Static Assets gibt es keinen Origin-Fetch, der Modus hat also keine Schutzwirkung. Der wirksame Riegel ist Always Use HTTPS + HSTS + Minimum TLS 1.2.**
tun: Always Use HTTPS on, Minimum TLS 1.2, TLS 1.3 on, Automatic HTTPS Rewrites on. HSTS: erst 1–2 Wochen `max-age=300`, dann `max-age=31536000; includeSubDomains`. **Preload ist praktisch unumkehrbar und bewusst zu entscheiden — vorher sicherstellen, dass jede künftige Subdomain (api, status, Mail-Domains) HTTPS spricht; die Einreichung bei hstspreload.org gehört in Stufe 6.** Universal SSL deckt nur Apex und eine Subdomain-Ebene; zweistufige Subdomains brauchen ACM (10 USD/Monat). Mit testssl.sh und SSL Labs prüfen.
Abh.: 2.1

**2.3 · [B] · M · Hosting: Workers Static Assets, wrangler.toml, Umgebungen, Preview-Abschirmung**
warum: **Cloudflare Pages kanonisiert Pfade zwingend: `/docs.html` wird per 308 auf `/docs` umgeleitet, nicht abschaltbar** — bei 648 internen `.html`-Links kostet jede Navigation einen Redirect-Hop. Workers Static Assets erlaubt `html_handling = "none"`. Es gibt kein `wrangler.toml`, keine `package.json`, kein `dist/`: ein Direct Upload würde `tasks/app.js`, `tasks/todo.md` (97 KB interne Notizen) und `tasks/_shots/` mitveröffentlichen. Preview-Deployments landen auf `*.workers.dev`, sind ohne Schutz erreichbar und indexierbar; schlimmer: wer versehentlich dort landet, führt ungeprüften Client-Code aus und legt Drops in einer anderen IndexedDB-Origin ab. **Korrektur: Cloudflare Access kann `*.workers.dev` nicht schützen — Access-Applications setzen einen Hostnamen in einer eigenen Zone voraus, `workers.dev` gehört Cloudflare. Richtig: `workers_dev = false` und `preview_urls = false`. Die eingebaute Access-Integration für Preview-Deployments existiert nur bei Pages.**
tun: `assets = { directory = "./dist", binding = "ASSETS", html_handling = "none", not_found_handling = "404-page", run_worker_first = false }`. `[env.preview]`/`[env.production]` mit getrennten Bindings und Ressourcennamen (`cloakdrop-chunks-prod` vs. `-preview`). Niemals das Repo-Root als `assets.directory`. `.dev.vars` in `.gitignore`, Secrets nur per `wrangler secret put` bzw. Secrets Store, nie als `[vars]`. Git-Integration als Build-Quelle, Deploy nur über GitHub Actions mit API-Token minimalen Scopes (Workers Scripts:Edit, R2 Storage:Edit, D1:Edit, Zone:Cache Purge) als Repository Secret; Deploy-Hooks nur bei echtem Bedarf — sie sind unauthentifizierte Build-Trigger. `X-Robots-Tag: noindex, nofollow` für Preview-Builds per Environment-Flag. **Origin-Guard im Client: läuft `location.hostname` nicht auf der Produktionsdomain, Composer sperren und Banner zeigen** (0 Treffer für `location.hostname` in index.html). Rollback über `wrangler versions upload` / `versions deploy` / `wrangler rollback` — nicht über den Pages-Rollback-Knopf, der auf der verworfenen Plattform lebt; drei Rohpunkte waren auf Pages formuliert und sind umzuschreiben (Preview-Deployments, Rollback, `functions/api/subscribe.ts` → Route im selben Worker).
Abh.: 0.2, 0.4, 2.1

**2.4 · [B] · M · E-Mail-Infrastruktur und Kontaktadressen** (Recht + Betrieb)
warum: `hello@cloakdrop.com` ist der einzige Rückkanal des Produkts — **17 Textvorkommen über 7 Seiten, davon 10 mailto-Links** (about, api, contact, docs, glossary, help, imprint). `imprint.html:1752` führt sie als Pflichtangabe nach § 5 DDG, `:1767` weist selbst darauf hin, dass E-Mail allein gerichtlich angegriffen wurde. Ohne SPF/DKIM/DMARC landen Antworten im Spam und die Domain ist offen für Spoofing: eine gefälschte „CloakDrop Support"-Mail, die um einen Drop-Link bittet, ist ein direkter Angriffsweg auf die eigenen Nutzer, weil der Link den Schlüssel trägt. **Nicht zu tun: „die 8 leeren mailto-Verweise reparieren" — es gibt keine. Die 8 Vorkommen sind JS-erzeugte Menüeinträge (`dashboard.html:3879`: `href: 'mailto:' + mail`), die zur Laufzeit die Adresse einsetzen.**
tun: Email Routing aktivieren (empfängt nur, sendet nicht), MX automatisch setzen, `hello@`, `security@`, `abuse@`, `dsa@`, `dpo@`, `postmaster@` auf ein **tatsächlich gelesenes** Postfach forwarden, Catch-All verwerfen. SPF (`include:_spf.mx.cloudflare.net` bzw. Include des Versenders, `-all`), DKIM des Versenders, DMARC `p=none` mit `rua`, Rampe auf `p=quarantine`/`p=reject` in Stufe 6. MTA-STS und TLS-RPT passen zur Positionierung. Für ausgehende Mail EU-Anbieter (Postmark EU, Brevo, Mailjet) auf **getrennter Versandsubdomain** mit eigenem SPF-Include, eigenem DKIM-Selektor und alignendem Return-Path, `sp=` der Hauptdomain bewusst setzen — sonst hängt die Zustellbarkeit der Support-Mails an der Spam-Rate des Newsletters. Bounce- und Complaint-Verarbeitung vor der ersten Kampagne. Zweiter Schnellkanal nach § 5 DDG. Wenn die Domain keine Mail empfangen soll: Null-MX (`MX 0 .`) plus `v=spf1 -all`.
Abh.: 2.1, 1.2

**2.5 · [B] · M · Host-Trennung für Nutzdaten (Domain-Reputation)**
warum: Der geteilte Link zeigt auf den Apex (index.html:2459) — dieselbe Domain wie Impressum, Preise, Newsletter und `hello@`. Weil ID und Schlüssel im Fragment stehen, sieht jede Blocklisten-Infrastruktur nur **einen** URL: die Startseite. Google Safe Browsing, SmartScreen, Spamhaus DBL und Unternehmens-Mailgateways können deshalb keinen einzelnen Drop sperren, nur die Domain. Ein einziger gehosteter Phishing-Payload führt zur roten Warnseite in Chrome, Edge und Safari — und damit sind schlagartig **alle bestehenden Drop-Links** tot, die Marketingseiten unerreichbar und die Mailzustellung beschädigt. Für einen anonymen, kontofreien Filehoster ist das das erwartbare, nicht das unwahrscheinliche Szenario.
tun: Trennung **vor dem ersten erzeugten Link**, weil Links unveränderlich in Chats leben: Nutzdaten und Empfangsseite auf einen eigenen Hostnamen, besser eine zweite Domain, damit eine Listung Marke, Impressum und Mail nicht mitnimmt; Marketing und Mail bleiben auf `cloakdrop.com`. Chunk-Antworten immer `Content-Disposition: attachment` plus `nosniff`, damit unter der Domain nie HTML gerendert wird — das ist der eigentliche Auslöser für Safe-Browsing-Einträge. Search Console, Bing Webmaster Tools und Safe Browsing Transparency Report vor Launch verifizieren: dort landet die Warnmeldung, und nur dort läuft der Einspruch (1–14 Tage, in denen der Dienst faktisch tot ist). Wöchentlicher Cron-Check gegen Safe Browsing API, URLhaus, Spamhaus, plus Runbook „Domain gelistet".
Abh.: 0.7, 2.1

**2.6 · [B] · M · Kryptoformat v2 einfrieren: AAD, ivPrefix, chunkSize, Linkversion** (Krypto + Transport)
warum: Das Manifest ist `{ v: 1, created, files: [] }` (index.html:2427) mit nur name/size/type/start/chunks je Datei; `v` wird geschrieben und **nie gelesen**. `encrypt`/`decrypt` laufen ohne `additionalData` (2436-2438, 2729-2731) — Drop-ID, Formatversion und Chunk-Index sind nicht kryptografisch an den Inhalt gebunden. Der geteilte Link ist `#t=<id>&k=<base64url>`: keine Version, kein Algorithmus, keine Angabe, ob der Schlüssel roh oder abgeleitet ist. Sobald ein zweites Verfahren dazukommt (Passphrase, AAD, anderer Chunk-Zuschnitt), kann ein Client alte Links nur durch Ausprobieren unterscheiden — und alte Links leben bis 30 Tage weiter. **Verschärfung gegenüber der Rohfassung: die Einstufung des unauthentifizierten `ivPrefix` als reines Verfügbarkeitsproblem gilt nur für den Lesepfad. Übernimmt der Client das `ivPrefix` jemals von der Serverseite (Resume, Wiederaufnahme eines halben Uploads, erneutes Verschlüsseln), kann ein böswilliges Relay Nonce-Reuse unter demselben Schlüssel erzwingen — und das kostet bei GCM Authentizität und Vertraulichkeit (XOR zweier Klartexte, Authentication-Key-Recovery).**
tun: Formatbruch **vor** dem Relay-Bau erledigen. Manifest v2 und Versionsprüfung, die bei unbekannter Version eine ehrliche Meldung zeigt statt eines Tag-Fehlers: pro Datei `clen[]` (Chiffrat-Bytes je Chunk, ohne die kann der Client Resume und Fortschritt nicht rechnen), `sha256` des Klartexts, normalisiertes `mime`, optional Thumbnail-Chunk; global `total`, `expiresAt`, `burn`, `chunkSize`. **AAD = Drop-ID ‖ Formatversion ‖ Rolle (`manifest`/`chunk`) ‖ Chunk-Index — nicht die Chunk-Gesamtzahl: die steht erst nach dem letzten Chunk fest, verschlüsselt wird vorher; sie gehört ins authentifizierte Manifest, das ohnehin zuletzt geschrieben und als Seal-Operation definiert ist.** `ivPrefix` deterministisch aus der Drop-ID ableiten oder ins AEAD-geschützte Manifest nehmen, statt es als serverseitiges Klartextfeld zu führen. Verfahrenskennung in den Link (Versionspräfix im Fragment oder Kennbyte vor dem Schlüsselmaterial). Harte Regel dokumentieren und testen: ein Schlüssel gehört genau einem Drop. **Chunkgröße: die 4 MiB sind durch das IV-Zählerschema fixiert, und R2-Multipart verlangt mindestens 5 MiB je Teil (außer dem letzten, max. 10.000 Teile) — dynamische Chunkgrößen sind ein Formatbruch und nur als `chunkSize` im authentifizierten Manifest zulässig, nicht als freie Client-Entscheidung.** Passwortschutz jetzt entscheiden (3.5), damit nicht zwei inkompatible Linkformate entstehen. Linkformat als Invariante in einem Test festnageln: der Teilen-Link enthält **kein Pfadsegment mit der Drop-ID** — diese Eigenschaft ist der Grund, warum Vorschau-Bots heute nicht einmal die Existenz eines Drops erfahren, und `api.html:2070` (`/v1/drops/{id}/chunks/{n}`) normalisiert die Pfadform bereits.
Abh.: 0.5 (Tests)

**2.7 · [B] · XL · Relay: API, Drop-Datenmodell, Zustandsspeicher**
warum: Es gibt **überhaupt keinen Relay** — ein Drop existiert nur im Browser, der ihn erzeugt hat; repoweit 0 Treffer für fetch/XMLHttpRequest/WebSocket/sendBeacon. index.html:2665 sagt „the cloud relay is the next milestone", :2279 „IndexedDB als Adapter, später R2". `api.html` spezifiziert sieben Endpunkte vollständig, keiner existiert. Ohne autoritativen Datensatz kann der Server nichts erzwingen: der heutige Satz ist `{manifest, ivPrefix, created}` (2453-2457) und kennt weder Chunkzahl noch Ablaufzeitpunkt noch Burn. Ohne Relay ist jede weitere Zusage (Ablauf, Burn, Historie, Quoten) gegenstandslos.
tun: Worker `cloakdrop-relay` auf `api.cloakdrop.com/v1/*`, Router ohne Framework (URLPattern), Bindings R2 (CHUNKS), Durable Object (DROP), D1 (META). Drop-Satz: `id, ivPrefix, created_at (Serverzeit), expires_at (absolut, UTC), chunk_count, per-chunk ciphertext_len, total_bytes, sealed, burn, download_count, state, plan_tier`. **Zustandsaufteilung, sonst wird sie im Code zufällig festgelegt: Durable Object je Drop (SQLite-backed) hält alle schreibenden Operationen serialisiert — Seal, Burn, Quota atomar — plus den Ablauf-Alarm. D1 hält den Sekundärindex für Sweeper, Abrechnung und Missbrauchszählung, Schreibpfad asynchron per `ctx.waitUntil`, damit der Upload nicht wartet. KV ausschließlich für lesedominante, unkritische Dinge (Feature-Flags, Kill-Switch, Plan-Limits) — KV ist global eventual consistent bis ~60 s und erlaubt etwa einen Write pro Sekunde und Key, für Seal, Burn oder Download-Zähler damit unbrauchbar: zwei parallele Downloads würden beide gewinnen. Kein Zustand in R2-Metadaten, die sind nicht transaktional.** Spec-Regeln hart durchsetzen: Counter 0 nur für den Manifest-PUT, PUT nach dem Seal → 409, Body ausschließlich `application/octet-stream` und per `request.body` direkt in R2 gestreamt (**kein `arrayBuffer()` — 128-MB-Isolate-Limit und CPU-Zeit**), `GET /chunks/{n}` mit Range-Support und ETag. **CORS: `Access-Control-Allow-Origin` nur die eine Produktionsorigin, `Allow-Headers` Content-Type und Authorization, kein Wildcard — und `Vary: Origin` ist Pflicht, weil ein konkreter Origin gesetzt wird, sonst cacht ein Zwischen-Cache die Antwort für den falschen Origin. `Access-Control-Max-Age` wird von Chromium bei 7200 s gekappt, 86400 ist wirkungslos.** Kein Endpunkt darf je einen Query- oder Header-Parameter namens `k` akzeptieren — als CI-Test verankern. Client-Adapter `storage.saveChunk/loadChunk/saveTransfer/loadTransfer` (index.html:2315-2320) hinter derselben Signatur gegen HTTP tauschen; die Schnittstelle ist bereits richtig geschnitten.
Auf: ~20 PT.
Abh.: 2.3, 2.6, 2.8

**2.8 · [B] · M · R2: Buckets, EU-Jurisdiction, Objektlayout, Kostenrechnung**
warum: Kein Bucket, keine Zugriffsstrategie. Weil 4 MiB unter der Multipart-Mindestgröße liegen, muss jeder Chunk ein eigenes Objekt sein: ein 3-GB-Drop sind 768 PutObject, ein 50-GB-Pro-Drop 12.800 — der wichtigste Kostentreiber des Systems, zu rechnen **vor** der Preisentscheidung. **Korrekturen zur Rohfassung: das R2-Freikontingent für Speicher liegt bei 10 GB/Monat; 1.000 Drops à 3 GB sind ~3 TB und damit um Faktor ~300 darüber — die Behauptung „im Freikontingent" gilt nur für Class-A-Operationen (768.000 von 1 Mio.). Class B beim Abruf fehlt in der Rechnung: 768 GET pro Drop zu 0,36 USD/Mio. plus je eine Worker-Invocation.** Löschungen sind kostenfrei, Egress ist 0 — das trägt „zero-egress relay" und macht Burn und Ablauf billig.
tun: `cloakdrop-chunks-prod` und `-preview` **mit Jurisdiction `eu` bei der Erstellung** (nachträglich nicht änderbar; Location Hint ist best effort und rechtlich wertlos). Objektlayout mit Ablauf-Präfix, damit Lifecycle greifen kann: `d1/{drop_id}/{n}`, `d3/…`, `d7/…`, `d30/…`, Manifest unter `…/0`. Multipart nur für Server-zu-Server-Pfade; im Browser-Pfad ist ein Objekt pro Chunk korrekt. **Writes und Reads über den Worker mit R2-Binding streamen, kein Presigning — nur so greifen Burn, Ablauf-Gate und Quota, und nur so bleibt der Zugriff an die Drop-ID gebunden statt an einen weiterverteilbaren Link. Presigned URLs (S3-API, SigV4) ausschließlich intern für Backup-Export, Migration, Support-Debugging mit Ablauf ≤ 15 Minuten — eine Presigned URL ist bis zu 7 Tage gültig, also potenziell länger als ein 24-Stunden-Drop. Bucket niemals public oder über eine eigene Custom Domain öffnen: das würde jeden Chunk dauerhaft unauthentifiziert erreichbar machen und Cloudflare würde ihn zusätzlich cachen.**
Abh.: 1.5, 2.7

**2.9 · [B] · L · Upload-Transport: Parallelität, Retry, Idempotenz, Resume, Abbruch** (Transport + Robustheit)
warum: `cloakAndStore` schreibt streng sequenziell (index.html:2429-2447, `await` pro Chunk). Über HTTP wären das bei 3 GB und 4 MiB 768 serielle Round-Trips; ein einziger 500er killt den ganzen Drop, weil kein Wiederholungspfad existiert — am Launchtag scheitert jeder Transfer über Mobilfunk. Der Transfer-Satz wird erst **nach** dem letzten Chunk geschrieben (2453): bricht es vorher ab, liegen Waisen-Chunks im Speicher und der Drop ist unauffindbar. `#st-progress` (1761-1770) hat einen Balken und einen Text, aber keinen Knopf; repoweit 0 Treffer für `abort`/`cancel` in index.html. Kein `beforeunload`, `pagehide`, `visibilitychange` (0 Treffer) — ein Klick aufs X mitten im Verschlüsseln von 3 GB verwirft alles ohne Warnung.
tun: 3–6 gleichzeitige `PUT /v1/drops/{id}/chunks/{n}`, exponentielles Backoff, `Retry-After` beachten, Idempotenz über den Chunk-Index (gleicher Index = Overwrite, kein Fehler). Drop **zuerst** per `POST /v1/drops` anlegen, Upload-Zustand lokal mitschreiben (File-Handle-Referenz, Chunk-Bitmap), beim Wiedereinstieg vorhandene Chunks abfragen und nur Lücken senden — Chunk-Bitmap bzw. HEAD-Endpunkt gehört in die API-Liste. AbortController durch `cloakAndStore` und `downloadFile`, Abbruch-Knopf in `#st-progress` und pro Empfangszeile; bei Abbruch laufende Requests abbrechen, `DELETE` für den halben Drop, lokale Chunks löschen, Zustand zurück auf `#st-list` mit erhaltener Dateiliste. `beforeunload` solange ein Transfer läuft, `visibilitychange`/`pagehide` zum Sichern der Bitmap. Beim Senden `slice.stream()` statt `slice.arrayBuffer()` (2435).
Abh.: 2.7

**2.10 · [B] · L · Download-Streaming: File System Access, Service Worker, ZIP** (Transport + Kompatibilität)
warum: `parts.push(plain)` sammelt alle entschlüsselten Chunks, `new Blob(parts)` kopiert sie erneut (2724-2734) — rund doppelte Dateigröße als Speicherspitze, plus die Object-URL. iOS Safari beendet Tabs bei etwa 1–1,5 GB **ohne Fehlermeldung**, `catch` greift nicht, der Tab verschwindet; verkauft werden 3 GB Free und „up to 50 GB" Pro. „Download all" (2710-2715) startet zusätzlich N Blob-Downloads gleichzeitig — Browser blocken Mehrfach-Downloads, und ein Drop mit 40 Dateien ist praktisch nicht abholbar. Kein `serviceWorker`, `WritableStream`, `showSaveFilePicker`, `ReadableStream` im Repo. Zusätzlich werden Manifestwerte ungeprüft übernommen: ein böswilliger Absender kann `chunks` oder `size` beliebig setzen und den Empfänger-Tab gezielt aufhängen.
tun: Zwei Pfade: `showSaveFilePicker()` + `createWritable()` wo verfügbar (konstanter Speicherbedarf), sonst Service Worker, der eine synthetische URL abfängt und mit `new Response(readableStream)` antwortet, gefüttert per MessageChannel. **Priorisierung korrigieren: der Service-Worker-Pfad ist kein Fallback, sondern der Hauptpfad für zwei von drei Engines — Firefox unterstützt File System Access nur für OPFS, nicht für Nutzerpfade.** Content-Length aus dem Manifest, damit der Browser Fortschritt zeigt; kein `new Blob(parts)` mehr. „Download all" als ZIP-Stream (STORE, kein Deflate — Chiffrat ist nicht komprimierbar), ZIP64 für > 4 GB, Dateinamen beim Packen bereinigen (3.12). Manifest validieren: `chunks` gegen `size`/`chunkSize`, Obergrenzen für Dateizahl und Namenslänge, `start`-Bereiche auf Überschneidungsfreiheit. Vor dem Umbau harte Gerätegrenzen messen (echtes iPhone, iPad, Android-Mittelklasse), im UI benennen und die 3-GB-/50-GB-Angaben an das Messergebnis anpassen. Empfangsansicht: beim Öffnen `GET /v1/drops/{id}` abfragen und Chunkzahl plus Chiffrat-Längen gegen das Manifest abgleichen — heute verspricht sie „2 cloaked files · 9.0 MB" allein aus dem Manifest (2688-2691) und merkt das Fehlen erst beim Klick (2727-2728).
Abh.: 2.7, 2.6

**2.11 · [B] · L · Ablauf serverseitig, Löschjob, Backstop, Totmannschalter, Löschnachweis** (Recht + Betrieb + Transport)
warum: `EXPIRE_MS = 7 * 24 * 3600 * 1000` (2241) ist eine Client-Konstante, geprüft nur in der Empfangsansicht (2668), und `created` stammt aus der Absenderuhr (2456). Wer die Prüfzeile umgeht oder den Chunk direkt abholt, kommt an abgelaufene Daten. Gelöscht wird nichts. R2-Lifecycle allein reicht nicht: es kennt nur Tagesgranularität und löscht asynchron — ein 24-Stunden-Drop wäre unter Umständen noch 40 Stunden auf der Platte, während die Website behauptet, er sei weg. Ein stiller Ausfall des Löschjobs ist der teuerste und rechtlich heikelste Ausfallmodus: die Daten bleiben liegen, obwohl die Seite ihre Löschung behauptet, und die Website liefert weiter 200.
tun: Drei Ebenen. (1) Harte Sperre: `expires_at` beim `POST /v1/drops` aus **Serverzeit** + gewählter TTL berechnen; jeder Lese-Endpunkt prüft es, bevor R2 berührt wird; `EXPIRE_MS` als Wahrheitsquelle entfernen, Client zeigt nur, was der Server liefert. (2) Aktive Löschung: DO-Alarm auf `expires_at`, der alle Chunks plus Manifest per **`bucket.delete(keys[])` (max. 1000 Keys pro Aufruf — `DeleteObjects` ist der S3-Name, nicht der Worker-Name)** entfernt, danach den Metadatensatz auf `gone` setzt; idempotent und wiederaufsetzbar. (3) Backstop: R2-Lifecycle je Präfix (`d1/`=1 Tag, `d3/`=3, `d7/`=7, `d30/`=30) plus „Abort incomplete multipart uploads after 1 day" (sonst kosten abgebrochene 3-GB-Uploads dauerhaft Geld), plus Cron-Trigger (`*/15 * * * *`), der über den D1-Index verwaiste Drops nachfegt. **Totmannschalter: der Cron-Worker pingt nach jedem Lauf einen Dead-Man's-Switch; Alarm, wenn zwei Zyklen ausbleiben. Täglicher Abgleichlauf: Bucket listen, jedes Objekt ohne Index-Eintrag und jeden Index-Eintrag ohne Objekt in eine Quarantäneliste. SLI ist das Alter des ältesten fälligen Objekts.** Löschprotokoll mit minimalen Feldern (Drop-ID-Hash, Ablaufzeit, Löschzeit, Bytes), das selbst nach 30 Tagen verfällt, über einen Status-Endpunkt abfragbar, damit ein Absender „deleted at" sehen kann. Clientseitig fehlt jedes Aufräumen der IndexedDB: Store `chunks` hat keinen Index nach Transfer-Id (2286-2287), Löschung müsste über `IDBKeyRange.bound(id+':', id+':\uffff')` laufen.
Abh.: 2.7, 1.6

**2.12 · [B] · L · Burn: Zustandsmaschine, Lease, Commit-Token** (Krypto + Transport + Wahrheit)
warum: Das Versprechen steht auf der Startseite und als Schalter in `settings.html:2969`; der Schalter schreibt `localStorage['cloakdrop-burn']` und meldet per Live-Region „New drops delete after the first download." index.html liest den Schlüssel **nie** (0 Treffer), der Composer hat keine Burn-Option, die sieben dokumentierten Routen enthalten keinen Burn-Mechanismus. Eine beworbene Sicherheitsfunktion, die nichts tut und dem Nutzer aktiv bestätigt, sie sei aktiv, ist der gefährlichste Punkt der ganzen Liste. Technisch: R2 allein hat kein Compare-and-Swap, KV oder ein naives D1-UPDATE lassen zwei parallele Empfänger beide gewinnen.
tun: Burn-Flag in `POST /v1/drops`, Composer-Schalter mit Default aus den Settings. Zustandsübergang im Drop-DO, `blockConcurrencyWhile`, einmaliges kurzlebiges Download-Lease-Token (z. B. 30 Minuten), das Range-Requests und Resume desselben Empfängers erlaubt, aber keinen zweiten. **Auslöser ist der abgeschlossene Nutzdaten-Abruf, nicht der Manifest-Abruf — ein Prefetch, ein Virenscanner-Proxy, ein Link-Vorschau-Bot oder ein doppelter Klick darf den Drop nicht vernichten; „der erste `GET /manifest` burnt" ist reproduzierbarer Datenverlust.** Löschung nach vollständiger Auslieferung des letzten Chunks (Response-Stream-Ende in `ctx.waitUntil`) plus Kulanzfenster für Retries. **Autorisierung ohne Schlüsselwissen: der Client übergibt beim Anlegen einen Commit-Token (HMAC über die Drop-ID mit einem per HKDF aus dem Dateischlüssel gewonnenen Unterschlüssel); beim Löschen vergleicht der Server nur zwei Werte und besitzt nie ein Derivat, das eine Entschlüsselung erlaubt.** Ohne Besitznachweis könnte jede Fremdseite mit bekannter ID Drops vernichten. Semantik auf der Oberfläche exakt benennen: „der erste vollständige Download vernichtet den Drop; ein abgebrochener Download darf einmal wiederholt werden." Pflichttest mit zwei parallelen Clients. Bis das existiert: Schalter deaktiviert mit Begründung, Live-Region-Ansage streichen.
Abh.: 2.7, 2.6

**2.13 · [B] · L · Quoten, Rate-Limits, Turnstile, WAF, Ausgabendeckel, Kill-Switch** (Missbrauch + Kosten)
warum: `MAX_TOTAL = 500 * 1024 * 1024` (2240) deaktiviert lediglich einen Button (2373); serverseitig begrenzt nichts Größe, Dateizahl, Chunkzahl oder Drops pro Zeit. `api.html:2200`: „Are there rate limits? None yet, because there is nothing running to limit." Ein anonymer, kostenloser 3-GB-Blob-Store ohne Hürde ist innerhalb von Stunden ein Warez-Ablageplatz. **Weder R2 noch Workers lassen sich auf einen Maximalbetrag begrenzen: ein Missbrauchsfall, der 5 TB in Free-Drops schiebt, erzeugt eine ungedeckelte Rechnung, und ein Solo-Betreiber merkt es erst nach der Abrechnung.** Zusätzlich: ein geteiltes Pro-Token erlaubt Fremden unbegrenzt 50-GB-Uploads auf dieselbe Rechnung.
tun: Gestaffelt. (1) WAF: Free Managed Ruleset aktivieren; Custom Rules (Free: 5) für „Methode nicht in GET/PUT/POST/DELETE/OPTIONS auf `/v1/*` → Block" und „Content-Type ≠ `application/octet-stream` bei PUT `/chunks/*` → Block". (2) Rate Limiting: auf Free gibt es genau **eine** Regel — auf `POST /v1/drops` legen (z. B. 5 pro 10 Minuten je IP); für mehr Pro (20 USD/Monat) einplanen. (3) Die eigentliche Quote im Drop-DO plus D1: max. Gesamtbytes pro Drop nach Plan, max. Chunkgröße (**Content-Length prüfen, nicht dem Client glauben**), max. Chunkzahl, max. Dateien pro Manifest, Bytes und Drops pro IP-Präfix (/24 bzw. /48, gehasht mit rotierendem Salt) und pro ASN je 24 h, Zähler pro Token (Bytes/Monat, gleichzeitige Drops, Uploads/Stunde), Token-Rotation und Sperrliste. (4) Turnstile vor `POST /v1/drops`, Verifikation per siteverify im Worker — nicht vor dem Download, sonst bricht der Empfangsfluss; siehe die CSP/COEP-Kollision in 3.3. (5) **Bot Fight Mode bleibt aus** (3.2), stattdessen die gezielten Regeln. (6) Eigene Buchhaltung: laufende Byte-Summe pro Tag in D1, Cron vergleicht gegen ein Budget in KV, bei Überschreitung Kill-Switch-Flag, das `POST /v1/drops` mit 503 antwortet — **Reads und Deletes bleiben an, damit bestehende Drops nicht kaputtgehen**. Cloudflare Billing/Usage-Notifications plus R2-Class-A-Schwellwertalarm. Kostenmodell schriftlich: Zone Free 0, Workers Paid 5 USD/Monat, R2 0,015 USD/GB-Monat, Class A 4,50 USD/Mio., Class B 0,36 USD/Mio., Egress 0 — 1.000 Drops à 3 GB mit 7 Tagen ≈ 700 GB-Monate, also 10–11 USD Speicher plus 5 USD Workers, realistisch 15–25 USD/Monat, **linear ohne Obergrenze**. `MAX_TOTAL` im Client aus derselben Quelle wie das Marketing beziehen.
Abh.: 2.7, 1.20

**2.14 · hoch · M · Owner-Token, Widerruf und vorzeitiges Löschen durch den Absender**
warum: Das Zeilenmenü bietet bewusst nur Copy link, Show details und Email — der Kommentar sagt „nur Aktionen, die es wirklich gibt" (`dashboard.html:3865-3881`). Der Absender hat keine Möglichkeit, einen versehentlich geteilten Drop zu stoppen. Bei einem Dienst, dessen Verkaufsargument Kontrolle ist, ist das die erste Aktion, die ein Nutzer sucht; `api.html:2075` nennt DELETE „the only revocation there is" — nur auf Papier.
tun: `DELETE /v1/drops/{id}` implementieren, „Drop jetzt löschen" mit Bestätigung ins Zeilenmenü und in die Detailspalte. Autorisierung ohne Konto: beim Anlegen ein Owner-Token zurückgeben, das nur lokal beim Absender liegt und `DELETE` autorisiert — sonst könnte jeder mit der ID löschen. Zustand danach als `revoked` anzeigen, Zeile nicht verschwinden lassen.
Abh.: 2.7, 2.18

**2.15 · hoch · S · Ablaufwahl im Composer, Settings-Preset verdrahten** (Funktion + Wahrheit)
warum: Drei Ablaufzeiten werden versprochen, der Composer (1738-1788) hat keine Auswahl, `#st-done` trägt den Festtext „Expires in 7 days" (1784), und `settings.html:2962-2965` setzt ein Preset (`cloakdrop-default-expiry`), das index.html **nicht liest** (0 Treffer). Der Satz „each one can still be changed before sending" beschreibt eine nicht existierende Oberfläche, die Zusammenfassung „Default expiry 3 days" (settings.html:3020) ist frei erfunden. Wer 24 h einstellt und 7 Tage bekommt, wird konkret getäuscht.
tun: Segmented Control (Muster liegt in `settings.html:2965` fertig vor) neben `#cloak-btn`, Wert an `POST /v1/drops`, Default aus dem Preset, Ergebnis in `#st-done` aus der Serverantwort. Alternativ: Einstellung entfernen und überall „7 Tage, nicht wählbar" schreiben — inkl. `glossary.html:1892` und `pricing.html:1814`.
Abh.: 2.11

**2.16 · hoch · S · Drop-IDs auf 128 Bit, einheitliche Fehlerantwort**
warum: `crypto.getRandomValues(new Uint8Array(9))` sind 72 Bit (2422). Für sich nicht trivial zu raten, aber `GET /v1/drops/{id}` liefert laut `api.html:2072` Chunkzahl, Bytelängen und Zeitstempel und wird ohne Ratenbegrenzung zur Sonde: Existenz, Größenverteilung, Aktivitätsmuster. **Ungelöster Widerspruch in der Rohliste: eine Forderung will einheitliche 404 für „unbekannt", „abgelaufen" und „verbrannt" (gleicher Body, gleiche Latenz), zwei andere fordern 410 Gone — 410 bestätigt genau die Existenz, die 404 verbergen soll. Beides ist nicht gleichzeitig möglich.**
tun: Auf 16 Byte gehen, kostet nichts. Entscheidung erzwingen: 404 für alles (Enumerationsschutz, schlechtere Diagnose) **oder** 410 (bessere UX, Existenz-Leak). Tragfähiger Kompromiss: 404 ohne Token, 410 nur gegenüber dem Inhaber eines gültigen Owner-Tokens. Metadaten aus `GET /v1/drops/{id}` auf das Minimum für Resume reduzieren.
Abh.: 2.7, 2.14

**2.17 · hoch · M · Zeitbasis: UTC, ISO, Intl, Uhrenversatz**
warum: Über alle 20 Dateien: `new Date(` = 0, `toISOString` = 0, `Intl.DateTimeFormat` = 0, `getTimezoneOffset` = 0, `timeZone` = 0 — die einzige Zeitquelle ist `Date.now()` (28×). Es gibt kein `setInterval`; `data-left` ist eine Stundenzahl, die nur zum Sortieren geparst wird (`dashboard.html:3566`), angezeigt werden Festtexte („22 hours", „Jul 29, 2026 at 9:12 AM"). Konsequenzen: „24 h" gegen „bis morgen" ist nicht definiert; Löschjob läuft UTC, Anzeige in Ortszeit, Support in MESZ — an der Ablaufgrenze systematisch eine bis zwei Stunden Abweichung, genau dort, wo Nutzer Dateien verlieren; Sommerzeit verschiebt jede „läuft um 9 Uhr ab"-Aussage; Widerrufs- und Kündigungsfristen brauchen nachweisbare Zeitzonen. Zusätzlich: `created` stammt aus der Absenderuhr und wird gegen die Empfängeruhr geprüft (2668) — eine um Tage falsch gestellte Uhr (frisches Gerät, leere Knopfzelle, VM, Firmen-Image) zeigt „abgelaufen" für einen eben erzeugten Drop, und niemand kann das von echtem Datenverlust unterscheiden.
tun: Serverseitig ausschließlich UTC, Speicherung als absoluter Zeitstempel, Übertragung als ISO-8601 mit `Z`, Anzeige über `Intl.DateTimeFormat` in der Zone des Betrachters mit dem absoluten Wert in `title`/`datetime`. TTL-Semantik festlegen („24 Stunden nach Erstellung", nicht „morgen") und so formulieren. Countdown mit restzeitabhängigem Intervall (> 1 Tag stündlich, < 1 Stunde minütlich), bei `visibilitychange` neu berechnen, über die Grenze hinweg ohne Reload auf „abgelaufen" umschalten. Cron-Zeiten mit Zone dokumentieren. **Beim ersten Request den `Date`-Antwortheader gegen `Date.now()` vergleichen, den Versatz je Sitzung merken, alle Anzeigen darüber rechnen; bei Versatz über wenigen Minuten einen freundlichen Hinweis statt einer stillen Falschanzeige, plus Fehlercode `CLOCK_SKEW` in der Taxonomie (5.3).**
Abh.: 2.11

**2.18 · [B] · L · App-Schale aus echten Daten, IndexedDB-Migration v2, Leg
**2.18 · [B] · L · App-Schale aus echten Daten, IndexedDB-Migration v2, Legacy-Keys löschen** (Plattform + Wahrheit + Datenschutz)
warum: `dashboard.html:3059-3146` sind statische `<tr data-key=… data-size="1.4 GB" data-made="Jul 29, 2026 at 9:12 AM">` mit erfundenen Links (`k=SAMPLE_KEY_NOT_REAL_…`), erfundenen Dateinamen und **fremden E-Mail-Adressen** (ana@vsl.io, legal@northbridge.com, tomas@buero.de, mix@roomtone.fm). Der Begleittext behauptet, es seien die eigenen Daten („This list is a local history of links opened on this device", received.html:2941). Die einzige echte Persistenz ist `cloakdrop-received-cleared`; index.html schreibt nirgends einen Drop-Datensatz, den diese Seiten lesen könnten. Zusätzlich unbeachtet: Preview-Besucher tragen bereits drei Dinge im Browser — eine IndexedDB `cloakdrop` v1 mit `transfers`/`chunks` (verschlüsselte Dateien, die nur dort existieren), `cloakdrop-account` mit dem ungesalzenen Passwortabdruck, und in Chats verteilte `#t=…&k=…`-Links. `openDb()` öffnet fest Version 1 und legt in `onupgradeneeded` beide Stores **ohne `objectStoreNames.contains`-Prüfung** an (2281-2292): jede künftige Schemaänderung (Chunk-Index, Drop-Index, Upload-Bitmap) erzwingt Version 2, und ohne Prüfung wirft `createObjectStore` bei Rückkehrern einen ConstraintError — die App startet für genau die Leute nicht mehr, die sie schon benutzt haben.
tun: Lokalen Drop-Index in IndexedDB führen (Drop-Id, Dateinamen, Größe, created/expires, burn, Link, Owner-Token); der Schlüssel bleibt clientseitig, damit „kein Konto" hält. `dashboard/received/expired` daraus rendern, mit echtem Leerzustand als Startpunkt; Empfängerhistorie erst schreiben, wenn ein Drop tatsächlich geöffnet wurde, mit sichtbarem Hinweis, dass sie nur in diesem Browser liegt. Bis dahin die vier Seiten **nicht ausliefern** oder unter `/demo/` mit persistentem Banner und `noindex`; Beispieladressen auf `example.com`/`example.org` (RFC 2606) umstellen. Migrationsschritt beim Start: `indexedDB.open('cloakdrop', 2)` mit echter Migration und `contains`-Prüfung je Store, Test, der von einer v1-Datenbank aus startet; Legacy-Schlüssel (`-account`, `-session` und die fünf App-Schlüssel) aktiv entfernen und die Löschung im Changelog benennen — stille Löschung fremder Daten ist auch dann erklärungsbedürftig, wenn sie richtig ist. Preview-Links mit eigener Meldung beantworten („dieser Link stammt aus der Vorschau und lebte nur in deinem Browser") statt mit „abgelaufen"; alte lokale Drops als „nur auf diesem Gerät" ausweisen und löschbar machen.
Abh.: 2.7, 1.21, 0.3

**2.19 · [B] · M · Formulare mit echtem Endpunkt: Newsletter, Kontakt, Meldung**
warum: **21 `<form>`-Tags im Projekt, keines mit `action`** (16× `newsletter-form`, plus blog `bsub`, contact `cfform`, help `hcform`, index login/signup). Der Newsletter validiert, spielt eine mehrsekündige Erfolgschoreografie, schreibt „Subscribed" und setzt den Placeholder auf „You're in — watch your inbox." — und sendet nichts; privacy.html:1819-1823 stellt richtig: „does nothing at all with the address … the value is discarded when you close the tab", während `pricing.html:1904` behauptet, der Knopf trage die Adresse „into the launch list". Am Feld fehlen Zweck, Verantwortlicher, Widerrufshinweis und Link zur Datenschutzerklärung. Kontakt und Hilfe laufen über `mailto:` mit MAX 1500 bzw. 1200 Zeichen — mailto-URLs jenseits von ~2000 Zeichen werden von Windows/Outlook abgeschnitten, Webmail-Nutzer sehen gar nichts, Anhänge sind unmöglich. Das DSA-Meldeformular (1.14) braucht ohnehin einen Endpunkt.
tun: Route im Relay-Worker (nicht als Pages Function — die Plattform ist Workers) mit Turnstile: `/api/subscribe` mit **Double-Opt-In** (in der EU zwingend; Erfolgstext dann „Bestätigungsmail unterwegs", nicht „Subscribed"), Protokollierung von Zeitpunkt, IP und Textfassung, Abmeldelink in jeder Mail, AVV mit dem Versanddienst, Drittlandprüfung, Nennung in privacy.html unter Processors; `/api/contact` und `/api/report` mit Empfangsbestätigung. Fehler- und Netzwerkpfade behandeln. **Wenn das vor Launch nicht zu schaffen ist: Newsletter-Formular auf allen 16 Seiten und den Erfolgshaken entfernen und durch einen mailto-Hinweis ersetzen — nicht mit der Fake-Bestätigung live gehen.** Kontakt: Zeichenlimit auf ~800, kopierbare Adresse plus „Copy to clipboard" als Fallback. Der Warnhinweis „Never paste a drop link" bleibt, er ist richtig. **CSP-Kopplung: sobald ein HTML-Formular abgeschickt wird, blockiert `form-action 'none'` es — auch bei `<form>` ohne `action`, weil das auf die aktuelle URL postet. Entweder alle Formulare per `fetch` (dann greift `connect-src` und `'none'` kann bleiben) oder `form-action 'self'`. Diese Entscheidung muss vor dem CSP-Enforcement fallen.**
Abh.: 2.7, 3.1, 1.14

**2.20 · hoch · M · Caching, Kompression, Asset-Hashes, Response-Header**
warum: Heute ist strukturell nichts cachebar: 71 KB CSS und 16 Font-Data-URIs liegen in jeder Datei, jeder Seitenwechsel lädt 300–437 KB HTML neu (allein die Fonts 112,4 KB gzip **pro Aufruf**). Gleichzeitig darf App-HTML nicht am Edge oder im Browser hängen bleiben: bei einer E2EE-App muss ein Sicherheitsupdate des Clients sofort greifen. Ohne Content-Hash im Dateinamen ist `immutable` nicht setzbar — dann bringt das Auslagern nur die halbe Wirkung, und Nutzer hängen auf altem CSS mit neuem HTML.
tun: Nach 0.3: `dist/assets/<name>.<sha256-8>.<ext>`, `dist/asset-manifest.json`, Zusicherung, dass keine Seite auf einen nicht existierenden Hash-Namen zeigt. `_headers` (funktioniert auch bei Workers Static Assets): `/assets/*`, `/fonts/*` → `public, max-age=31536000, immutable`; `*.html` → `public, max-age=0, must-revalidate`. **Cache Rules (nicht Page Rules, abgekündigt) für: App-Pfade → Bypass Cache; Marketingseiten → Edge TTL 1 h, Browser TTL 0. Wichtig: Cache Rules setzen keine Response-Header — `Cache-Control: no-store` und die Sicherheitsheader als Netz gehören in eine Response Header Transform Rule.** Brotli ist für `text/html` automatisch aktiv, Zstandard prüfen; Brotli-Vorkompression der Assets. Cache Purge (by prefix) als letzter Deploy-Schritt, `cf-cache-status` muss HIT liefern.
Abh.: 0.3, 2.3

**2.21 · hoch · M · Speicher-Vorprüfung und Fehlerklassen statt `alert()`**
warum: `navigator.storage` kommt im Projekt nicht vor (0 Treffer) — `persist()` wird nie angefragt, also darf der Browser die IndexedDB unter Speicherdruck jederzeit räumen, und **Safari löscht nicht-persistenten Speicher nach sieben Tagen ohne Besuch, im privaten Modus sofort beim Schließen**, während die Oberfläche „Expires in 7 days" verspricht: der Drop kann vor seinem Ablaufdatum weg sein, und das sieht wie Datenverlust aus. Jeder Fehler landet in `alert('Encryption failed: ' + err.message)` (2407-2411) — ein `QuotaExceededError` bei 3 GB, der wahrscheinlichste Fehler überhaupt, sieht damit aus wie ein Krypto-Fehler, nach Minuten Arbeit, mit halb geschriebenen Chunks in der Datenbank. Kein Offline-Zustand, kein Retry-Knopf, keine Unterscheidung zwischen „nochmal versuchen" und „geht nicht".
tun: Vor dem Verschlüsseln `navigator.storage.estimate()` gegen Gesamtgröße plus Reserve prüfen und bei Unterdeckung **vorher** absagen — nicht bei 40 %; `persist()` anfragen und das Ergebnis auswerten (Safari gewährt es selten, dann muss die Oberfläche die kürzere Lebensdauer benennen). Bei Fehlschlag mitten im Lauf die geschriebenen Chunks aufräumen. Fehlerklassen (Quota, Netz/Offline, Serverfehler mit Retry, endgültig) und je Klasse ein eigener Zustand mit passender Aktion — die Fehlerkarte `#st-error` (1917-1923) existiert, wird aber nur beim Empfang benutzt. `navigator.onLine` plus `online`/`offline`-Events. `alert()` ersatzlos raus.
Abh.: 2.9

**2.22 · mittel · S · Plattformgrenzen als Konstanten spiegeln**
warum: Mehrere Zusagen stehen dicht an harten Limits, und ohne vorherige Rechnung merkt man das erst beim ersten großen Drop.
tun: Schriftlich fixieren und im Code spiegeln: Workers Free 10 ms CPU pro Request und 100.000 Requests/Tag → für den Relay zwingend Workers Paid (5 USD/Monat, Standardlimit 30 s, bis 5 min konfigurierbar). **Subrequests: Free 50 pro Invocation, Paid 1.000 — die in der Rohliste genannten 1.000 gelten nicht für Free.** Request-Body-Limit der Zone 100 MB auf Free/Pro — genau der Grund, warum Chunkung nicht optional ist. Isolate-Speicher 128 MB → niemals einen ganzen Chunk plus Kopien puffern. Ein Request fasst nie mehr als ein R2-Objekt an, außer beim Batch-Delete. Static Assets max. 20.000 Dateien, 25 MiB je Datei. R2 Multipart min. 5 MiB, max. 10.000 Teile. KV Wert max. 25 MiB, ein Write/s je Key. D1 max. 10 GB.
Abh.: 2.7

**2.23 · hoch · M · Mehrfachempfänger: entscheiden und konsistent bauen**
warum: `dashboard.html:3089/3134` zeigen `data-rcpt="legal@northbridge.com +2"`. Im Modell existiert ein Link mit einem Schlüssel im Fragment: keine Empfänger-Entität, keine Per-Empfänger-Links, keine Zustellung — und die angezeigten Adressen implizieren einen Mailversand, den es nirgends gibt. Mit Burn ist Mehrfachempfang logisch unvereinbar, solange nicht geklärt ist, wer der „erste" ist.
tun: Entweder aus der UI entfernen und die `data-rcpt`-Werte korrigieren, oder Empfänger-Entität: n einmalige Download-Token pro Drop, jedes mit eigenem Zustand und eigenem Burn-Zähler, Schlüssel identisch im Fragment. Ein Mailversand widerspricht „kein Konto, kein Tracking" und braucht eine eigene Datenschutz-Begründung — im Zweifel nicht bauen und die E-Mail-Spalte als lokales Notizfeld deklarieren.
Abh.: 2.12, 2.18

**2.24 · [B] · M · Exit-Plan und Portabilität** (Plattform + Betrieb)
warum: Registrar, DNS, TLS, WAF, Hosting, Objektspeicher, Datenbank, Zustand, Mail-Routing und Metriken liegen in **einem** Cloudflare-Konto. Eine AUP-Beschwerde, eine fehlgeschlagene Kartenbelastung oder eine Kontosperre nimmt gleichzeitig Website, API, Drops, Domain und den einzigen Kontaktkanal — inklusive der Möglichkeit, Nutzer darüber zu informieren. Technisch verschärft: Durable Objects und D1 haben bei anderen Anbietern keine Entsprechung, ein Umzug ist ein Rewrite. Bei 30-Tage-Pro-Links ist ein zweiwöchiger Ausfall gleichbedeutend mit Datenverlust bei zahlenden Kunden.
tun: Portabilität dort erzwingen, wo sie billig ist: R2 über die S3-kompatible API ansprechen statt über Binding-Idiome, Zustandszugriffe hinter ein schmales Interface legen (keine DO-Aufrufe im Handler verstreut), Krypto- und Manifestformat frei von Anbieterspezifika. Registrar bewusst trennen oder mindestens Auth-Code und Zonendatei offline sichern — eine Kontosperre darf nicht die Domain mitnehmen. Mail-Routing-Ziel bei einem dritten Anbieter, Statusseite und Zweitkanal außerhalb von Cloudflare (5.4). Exit-Plan schriftlich: Zonen-Export, R2→S3-Kopie mit Zeit- und Kostenschätzung, Ziel-RTO, und die ehrliche Feststellung, welche Funktionen beim Umzug wegfallen.
Abh.: 2.7, 2.8

**Stufe 2 gesamt: 70–95 PT** (davon ~20 PT allein 2.7).

---

## Stufe 3 — Sicherheit

**3.1 · [B] · L · Security-Header und strenge CSP als HTTP-Header mit Build-Hashes**
warum: `grep "Content-Security-Policy"` = 0 in allen 20 Dateien, kein `_headers`. Bei CloakDrop ist der ausgelieferte HTML-Bytestand die Sicherheitsgrenze: wer eine Zeile JavaScript einschleusen kann, liest `location.hash` und damit Schlüssel plus Drop-ID. Der Schlüssel liegt zusätzlich im Klartext im Wert von `#link-input` (2460) — ohne `form-action` genügt ein injiziertes Formular. Es gibt kein einziges `<script src>` und kein `<link rel=stylesheet>`: je Seite 2 inline `<style>` und 2 inline `<script>`. `script-src 'self'` greift damit nicht, `'unsafe-inline'` wäre eine CSP, die genau den Angriff erlaubt, gegen den sie schützt. Statisch ausgelieferte Seiten können keine Per-Response-Nonce erzeugen — bleiben SHA-256-Hashes, die bei jeder Textänderung neu berechnet werden müssen. Positive Ausgangslage: 0 Inline-Event-Handler in index.html, 0 `eval`/`new Function`/`document.write`, 0 externe Ressourcen — eine strenge CSP ist konfliktfrei erreichbar.
tun: Der Bau berechnet die SHA-256 jedes verbleibenden Inline-Blocks (nach 0.3 nur noch der 234-B-Theme-Bootstrap) und schreibt sie in die versionierte `_headers`. **CSP ausschließlich als HTTP-Header: `frame-ancestors`, `report-uri`/`report-to` und `sandbox` werden im `<meta http-equiv>` ignoriert — ein Meta-Tag taugt höchstens als Rückfallebene ohne diese Direktiven.** Policy: `default-src 'none'; script-src 'self' 'sha256-…'; style-src 'self'; style-src-attr 'none'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; connect-src 'self' https://api.cloakdrop.com; frame-src 'none'; worker-src 'self'; manifest-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests`. **Korrekturen gegenüber der Rohliste: `worker-src 'none'` und `manifest-src 'none'` blockieren die Service-Worker-Registrierung (Pflichtpfad für Streaming-Download und ZIP, 2.10) und das Web-Manifest (4.10) — beide müssen `'self'` sein. `media-src` fehlt komplett und hat keinen Fallback auf `img-src`: `<video>`/`<audio>`-Vorschau wäre blockiert. `blob:` in `img-src`/`media-src` für die Objekt-URL-Vorschau (der reine Download über `<a download>` braucht es nicht — Downloads fallen nicht unter die Fetch-Direktiven; im Report-Only-Lauf messen statt annehmen). `font-src 'self'` statt `data:`, weil die Fonts nach 0.3 als Dateien ausgeliefert werden. `frame-src` wird gebraucht, sobald ein sandboxed iframe für Vorschau (3.13) oder Turnstile (3.3) dazukommt.** Die 13 `style="`-Attribute in index.html (plus je 1 in blog/docs/pricing und den App-Seiten) in Klassen bzw. Custom Properties überführen, dann `style-src-attr 'none'`; alternativ `'unsafe-hashes'` mit Attributwert-Hashes. Per CSSOM gesetzte Werte (`bar.style.transform`, 2443/2470-2476) sind von CSP nicht betroffen. Weitere Header: `Referrer-Policy: no-referrer` (0 Treffer für `referrer`; Default `strict-origin-when-cross-origin` verrät bei jedem künftigen Auslink Origin und Pfad, und schon „diese Person hat cloakdrop geöffnet" ist ein Metadaten-Leak — plus Regel `rel="noopener noreferrer"` für alle externen Links, heute existiert keiner, also der billigste Moment); `X-Content-Type-Options: nosniff`; `X-Frame-Options: DENY`; `Origin-Agent-Cluster: ?1`; `Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=(), serial=(), hid=(), payment=(), idle-detection=(), screen-wake-lock=(), browsing-topics=()` — **`interest-cohort=()` weglassen: FLoC ist eingestellt, das Feature existiert nicht mehr und wird ignoriert; `clipboard-write` muss nicht erlaubt werden, weil der Default-Allowlist `self` ist** (der Copy-Button nutzt `navigator.clipboard.writeText`, 2480). **Vorlauf: eine Woche `Content-Security-Policy-Report-Only` über alle 20 Seiten, Verstöße zählen, dann scharf. Falls `report-to` genutzt wird: an einem echten Empfangs-Link verifizieren, dass `document-uri` das Fragment nicht enthält — sonst wird die Sicherheitsmaßnahme selbst zum Leak. Ebenso festschreiben: kein Sentry-CDN, kein Logpush mit vollständigen URLs, `window.onerror`-Reporting nur mit Fragment-Scrubbing (5.3).** Ehrlich dokumentieren, dass CSP allein den Schlüssel nicht vor XSS retten kann: `navigate-to` ist aus dem Standard verschwunden, ein injiziertes `location = 'https://…' + location.hash` ist von keiner Direktive gedeckt — deshalb ist die Fragment-Bereinigung (3.4) die eigentliche Gegenmaßnahme. Mit securityheaders.com und Mozilla Observatory verifizieren.
Abh.: 0.3, 2.3, 2.19

**3.2 · [B] · S · Cloudflare-Features abschalten, die Fremdskripte injizieren, plus Byte-Diff-Test**
warum: Email Address Obfuscation schreibt `mailto:`-Links um und lädt `email-decode.min.js` — es gibt 10 mailto-Links, u. a. den Impressumskontakt. Rocket Loader lädt einen weiteren Injector, Bot Fight Mode fügt `/cdn-cgi/challenge-platform/`-Skripte und ein Cookie hinzu, Browser Insights den Analytics-Beacon. Jedes einzelne macht „No third-party anything. No CDN, no external script" (privacy.html:1853) und „document.cookie is never touched" (1811) zur Falschbehauptung **und zerschlägt jede hash-basierte CSP**, ohne dass eine Zeile im Repo geändert wurde. **Korrekturen: „Network → Cloudflare Fonts" ist falsch verortet — Cloudflare Fonts sitzt unter Speed → Optimization → Content Optimization und betrifft ohnehin nur Google-Fonts-Referenzen (hier: keine). Mirage ist ein Pro+-Feature und auf einer Free-Zone nicht aktivierbar. Automatic Platform Optimization ist WordPress-spezifisch. Speed Brain injiziert Speculation Rules, kein Fremdskript.**
tun: Nach Planverfügbarkeit sortiert vor dem ersten öffentlichen Request abschalten: Scrape Shield → Email Address Obfuscation OFF, Server-side Excludes OFF; Speed → Optimization → Rocket Loader OFF, Browser Insights/RUM OFF; Security → Bots → Bot Fight Mode OFF (stattdessen gezielt Turnstile, 2.13). Danach den ausgelieferten HTML-Bytestand per curl gegen die lokale Datei **byteweise diffen** — nicht auf die Schalterstellung vertrauen — und diesen Diff als wiederkehrenden Smoke-Test in den Deploy hängen.
Abh.: 2.3, 3.1

**3.3 · hoch · S · COOP/COEP/CORP entscheiden — im Konflikt mit Turnstile**
warum: Es gibt null externe Ressourcen, also bricht `require-corp` heute nichts, und wer es erst nach dem Launch nachrüstet, muss jede eingebettete Ressource nachziehen. **Korrektur der Begründung: `crossOriginIsolated` ist Voraussetzung für SharedArrayBuffer, `measureUserAgentSpecificMemory()` und hochauflösende Timer — nicht für Streaming. Streams, File System Access und Service-Worker-Responses funktionieren ohne Isolation. Und: Turnstile lädt ein Skript und ein iframe von `challenges.cloudflare.com`; unter `require-corp` müssen eingebettete Cross-Origin-Ressourcen CORP/CORS mitliefern, was Turnstile nicht zuverlässig tut. Wer Turnstile will, braucht `COEP: credentialless` (oder keine COEP) plus `script-src` UND `frame-src https://challenges.cloudflare.com` — genau diese Konstellation führt am Launchtag zur weißen Seite.**
tun: Entscheiden: entweder `COOP: same-origin` + `COEP: require-corp` + `CORP: same-origin` und Turnstile weglassen (Bot-Schutz dann nur über WAF-Regeln und Quoten), oder `COEP: credentialless` mit Turnstile und den beiden CSP-Direktiven. In beiden Fällen Report-Only-Lauf über alle 20 Seiten. CORP auch auf den Relay-Antworten setzen, damit Ciphertext-Chunks nicht fremd eingebettet werden können. Turnstile in privacy.html benennen.
Abh.: 3.1, 2.13

**3.4 · [B] · S · Fragment-Hygiene: replaceState, Feld leeren, Zwischenablage-Hinweis**
warum: `enterReceiveMode` (2660-2686) fasst `location.hash` nicht an. Der Schlüssel bleibt über die gesamte Sitzung in `document.URL`, im Verlaufseintrag, in der Adressleiste (Screenshot, Screen-Sharing, Aufzeichnung), in der Verlaufssynchronisation über Geräte hinweg und für jedes später injizierte Skript lesbar. **Das Muster existiert im Code bereits (`history.replaceState` in 2541/2547), es wird an der wichtigsten Stelle nur nicht verwendet.** `#link-input` behält den vollen Link samt Schlüssel bis zum nächsten Zustandswechsel; `#new-btn` setzt nur `pending = []` (2490-2493). Zusammen mit `navigator.clipboard.writeText` landet er in Zwischenablage-Verläufen, die auf Windows, macOS/iOS und Android geräteübergreifend synchronisieren.
tun: Direkt nach `crypto.subtle.importKey` ein `history.replaceState(null, '', location.pathname)`, Schlüssel nur in einer Closure halten, in `verify`- und Fehlerpfaden ebenfalls bereinigen. Nebeneffekt bedenken und benennen: Reload verliert dann den Drop. Feldwert beim Verlassen des Fertig-Zustands leeren, Feld standardmäßig maskiert, `autocomplete="off"`/`spellcheck="false"`, kurzer UI-Hinweis auf den Zwischenablage-Verlauf. In help.html/verify.html erwähnen, dass der Link im Verlauf des Empfängers landet, wenn er ihn selbst eintippt.
Abh.: keine — billigster Sicherheitsgewinn der ganzen Liste.

**3.5 · [B] · M · Threat-Model: Fragment-Leakwege, Metadaten, optionale Passphrase**
warum: `docs.html:1869` behauptet „Browsers never transmit fragments", `:2042` „Fragments never leave the browser by specification — the key exists in your link and nowhere else". Standardkonform und im echten Nutzungspfad trotzdem falsch: **Unternehmens-Mailgateways (Microsoft Defender Safe Links, Proofpoint, Barracuda) schreiben Links um und kodieren die Original-URL inklusive Fragment in einen Query-Parameter auf ihrer eigenen Domain — damit liegt der Schlüssel in den Logs eines Dritten.** Dazu Verlaufssynchronisierung an Google/Apple/Microsoft, Zwischenablage-Sync, Screenshots, Weiterleitung in Chats. Zweiter Leak: es gibt kein Padding — jeder Chunk ist Klartextlänge + 16 Byte Tag, die letzte Chunk-Länge verrät die Dateigröße byte-genau, die Manifest-Länge verrät Dateizahl und Namenslängen. Das Relay kann bekannte Dateien per Größen-Fingerprint wiedererkennen, obwohl es sie nicht entschlüsseln kann.
tun: Threat-Model-Abschnitt, der die Leakwege einzeln benennt und `docs.html:1869/2042` entsprechend präzisiert. Offenlegen, was Betreiber und CDN zwangsläufig sehen: IP, Zeitpunkt, Gesamtgröße, Chunk-Zeitmuster, Abrufzahl — bei Cloudflare als TLS-Terminator gehört diese Position ins Modell. Mindestens eine Gegenmaßnahme anbieten: getrennter Versand von Link und Schlüssel (Drop-ID im Link, Passphrase separat) als Option für sensible Fälle. **Passwortschutz jetzt entscheiden, weil ein Nachrüsten mit falschen Parametern zwei inkompatible Linkformate erzeugt: Argon2id über WASM (dann ist die „null Abhängigkeiten"-Position zu verhandeln) mit dokumentierten Parametern, sonst PBKDF2-SHA-256 mit ≥ 600.000 Iterationen und 16-Byte-Salt; der abgeleitete Schlüssel wickelt den Dateischlüssel (Key-Wrapping), ersetzt ihn nicht.** Padding entweder einführen (Chunks auf konstante Größe, Manifest auf Bucket-Größe — Kosten: Speicher und Bandbreite) oder die Restexposition explizit dokumentieren.
Abh.: 2.6

**3.6 · hoch · S · Secure-Context- und Feature-Gate vor `crypto.subtle`**
warum: `index.html:2419` ruft `crypto.subtle.generateKey` ohne Vorprüfung; außerhalb eines Secure Context ist `crypto.subtle` undefined und die App stirbt beim ersten Klick mit einem TypeError im `alert()` — im schlimmsten Fall auf einer http-Preview-URL, also genau dort, wo Vertrauen entsteht. Für eine App, deren einziges Versprechen Verschlüsselung ist, ist ein stummer Ausfall der Verschlüsselungsschicht das schlechteste mögliche Verhalten. `tasks/todo.md:632` notiert die Feature-Erkennung als offen. **Präzisierung: eine Feature-Detection existiert bereits — `navigator.clipboard && window.isSecureContext` in `settings.html:4076` bzw. `tasks/app.js`, identisch in den vier App-Seiten; für `crypto.subtle` in index.html gibt es sie nicht (0 Treffer für `isSecureContext`).**
tun: Beim Start prüfen: `window.isSecureContext`, `crypto?.subtle`, `indexedDB`, `File.prototype.slice`. Fehlt etwas, den Composer sperren und im Klartext sagen, was fehlt und warum. `indexedDB` mitprüfen — im Private-Modus mancher Browser fehlt es oder scheitert leise (2281-2292 hat keinen Nutzerpfad für `req.onerror`).
Abh.: keine

**3.7 · [B] · L · Krypto- und Transport-Testsuite plus E2E-Transferpfad**
warum: Die Krypto ist echt verdrahtet und trägt das gesamte Produktversprechen — IV-Aufbau (8 Byte Zufall + 32-Bit-Zähler, Zähler 0 für das Manifest), frischer Schlüssel pro Transfer, GCM-Tag-Prüfung — aber es existiert kein einziger automatisierter Test: `verify_app.py` lädt index.html nie. **`changelog.html:1783` und `roadmap.html:1727` behaupten „Verified with byte-identical round-trips — wrong keys fail loudly"; belegt sind nur Handtests in `tasks/todo.md:596-604` (9-MiB-Rundlauf byte-identisch, gekipptes Bit → „Failed", gelöschter Chunk → „Failed", falscher Schlüssel → „The key doesn't fit").** Eine Verschlüsselung, deren Rundlauf niemand automatisch prüft, ist bei jedem Refactoring — und Manifest v2 plus AAD ist eines — eine Wette.
tun: Zwei Ebenen. (1) Unit-Tests in Node (`node:test` + WebCrypto) für `makeIv`, `b64urlEncode/Decode` und den Roundtrip; dafür die Funktionen aus dem Inline-Script in ein importierbares Modul ziehen: 0 Byte, 1 Byte, exakt Chunkgrenze (4194304), Grenze ±1, Mehrdatei, Unicode-Dateiname; gekipptes Bit im Chiffrat muss `OperationError` werfen; vertauschte Chunks, abgeschnittene Chunks, manipuliertes `ivPrefix`, falscher Schlüssel müssen scheitern; fehlender Chunk darf keinen Download auslösen; IV-Ableitung gegen feste Vektoren. (2) Playwright-E2E über zwei Browser-Contexts: in A cloaken, `#link-input` auslesen, in B öffnen, herunterladen, Byte-Gleichheit gegen das Original per `crypto.subtle.digest` — in chromium, firefox **und webkit** (Safaris WebCrypto und IndexedDB sind die wahrscheinlichste Bruchstelle). Beides ins CI-Gate. Bis die Tests laufen, die Formulierung in changelog/roadmap auf „von Hand geprüft" zurücknehmen.
Abh.: 0.5, 2.6

**3.8 · [B] · L · Externes Krypto- und Web-Review**
warum: `verify.html` sagt selbst „What this does not prove" und „It does not audit the code you are running". Ohne mindestens einen externen Blick auf Schlüsselerzeugung, IV-Schema, Manifest-Integrität, Linkformat und Relay-Autorisierung ist jede Sicherheitsaussage auf der Startseite unbelegt — und ein Reviewer findet die ungesalzene Passwort-Prüfsumme in zehn Minuten.
tun: Kompakten bezahlten Review beauftragen (Umfang: Client-Krypto, Linkformat und Fragment-Handling, Relay-Autorisierung inkl. Burn, CSP-Wirksamkeit). **Vorher intern aufräumen — Passwort-Attrappe (1.21), AAD (2.6), Fragment-Bereinigung (3.4) —, damit das Budget nicht für Offensichtliches verbrannt wird.** Terminvorlauf und Nachbesserungsrunde in 0.9 einplanen. Ergebnis samt Behebungsstand veröffentlichen — das ist der Inhalt, den verify.html braucht.
Abh.: 3.4, 3.7, 2.12, 1.21

**3.9 · hoch · M · Supply-Chain-Guard, Secrets, Rotationstabelle**
warum: Null Laufzeitabhängigkeiten sind der große Vorteil, aber nur eine Momentaufnahme: ein nachträglich eingebundenes Skript oder ein Cloudflare-Feature hebt sie auf, und in 20 Dateien fällt das niemandem auf. Keine Secrets im Repo — aber auch kein Prozess; mit Relay, R2-Credentials, Turnstile-Secret und Zahlungs-API kommen die ersten echten Geheimnisse, und mehrere davon können **nicht hart getauscht** werden: der Signaturschlüssel der Owner-Token und Download-Leases, der IP-Hash-Salt, DKIM, der DNSSEC-KSK. Ein harter Tausch bricht laufende Downloads, entwertet Owner-Token und macht Missbrauchszähler blind.
tun: Guard-Rail-Check, der bei jedem Build fehlschlägt, wenn eine `src`/`href`/`url()`-Referenz auf eine fremde Origin zeigt (`data:` und relative Pfade erlaubt); schriftliche Regel „keine Laufzeitabhängigkeiten, keine externen Origins"; jede künftige externe Subressource nur mit `integrity` und `crossorigin="anonymous"` (heute gegenstandslos, verhindert die Regression später). Geheimnisse ausschließlich als Worker Secrets / Secrets Store, `.env`-Muster in `.gitignore`, gitleaks als Pre-Commit- und Build-Gate, API-Token mit minimalem Scope statt Global API Key, 2FA. **Rotationstabelle: Geheimnis, Kadenz, Verfahren, Überlappungsfenster, wer rotiert — mit zwei gültigen Schlüsseln während der Umstellung für alles, was Signaturen prüft, und einem Salt-Übergang, der die Zähler nicht zurücksetzt. DNSSEC- und DKIM-Rollover in den Kalender, nicht in den Ernstfall.**
Abh.: 0.5, 2.3

**3.10 · hoch · M · Build-Verifizierbarkeit: deploy-manifest, Signatur, Provenienz**
warum: „No trust required. Check it yourself." ist heute nicht einlösbar: keine Versionskontrolle, keine Releases, keine veröffentlichten Prüfsummen. Ein Nutzer kann nicht feststellen, ob der Client, den er bekommt, derselbe ist wie gestern — für ein E2EE-Produkt die relevanteste Frage. Der Betreiber kann jederzeit ein manipuliertes index.html ausliefern, und niemand kann es beweisen.
tun: Build in GitHub Actions, Artefakt-Provenienz per `actions/attest-build-provenance` (Sigstore), Deploy nur aus diesem Artefakt. Pro Deploy ein signiertes `deploy-manifest.json` mit SHA-256 je Datei unter `/deploy-manifest.json`, in verify.html als fünfter, echter Check verlinkt (Nutzer vergleicht den Hash der geladenen Seite selbst) plus Ein-Zeilen-Befehl zum Nachrechnen. Voraussetzung: alle 20 Seiten aus dem Generator (0.3), sonst ist „Build == Quelle" nicht formulierbar. Bis dahin die Aussage in verify.html so belassen, wie sie ist — sie ist heute korrekt.
Abh.: 0.3, 0.5, 1.23

**3.11 · hoch · S · security.txt und Disclosure-Policy**
warum: Kein `.well-known/`, 0 Treffer für `security.txt` oder responsible disclosure. Ein Sicherheitsprodukt ohne Meldeweg lädt zur Veröffentlichung ohne Vorwarnung ein: der Finder postet öffentlich, weil `/.well-known/security.txt` 404 liefert und die Kontaktseite nur eine allgemeine mailto-Adresse hat. Billigster Punkt der Liste, am Launchtag maximal peinlich.
tun: `/.well-known/security.txt` nach RFC 9116 (`Contact`, `Expires`, `Preferred-Languages: de, en`, `Policy`, `Encryption` mit PGP-Key), Disclosure-Policy-Seite mit Safe-Harbor-Zusage und Reaktionsfristen, `security@`-Postfach, `Expires` in den Kalender. Optional Hall of Fame statt Prämie.
Abh.: 2.4

**3.12 · mittel · S · Dateinamen bereinigen**
warum: `a.download = fileEntry.name` (2738) übernimmt einen vom Absender kontrollierten String. Der Browser entschärft heute das meiste, aber sobald der ZIP-Pfad oder `showSaveFilePicker` dazukommt, sind `../`, absolute Pfade, Steuerzeichen, Unicode-Richtungsumkehrung (RLO-Trick zum Verstecken der echten Endung) und Windows-Reservenamen echte Risiken.
tun: Zentrale `sanitizeFilename()`: Pfadtrenner entfernen, führende Punkte und Leerzeichen strippen, Steuer- und Bidi-Zeichen (U+202A-U+202E, U+2066-U+2069) verwerfen, Reservenamen (CON, NUL) umbenennen, Länge begrenzen, Kollisionen im ZIP durchnummerieren. Vor dem Schreiben **und** vor dem Anzeigen anwenden. Zusätzlich iOS-Fall: der Picker liefert HEIC-Aufnahmen als JPEG mit generischem Namen, bei Mehrfachauswahl mehrfach denselben — Namenskollisionen im Manifest auflösen und vor dem Cloaken sichtbar machen.
Abh.: 2.10

**3.13 · mittel · M · Vorschau mit MIME-Allowlist**
warum: Der Empfänger sieht nur Name, Größe und einen Download-Knopf (1911, 2695-2708) — bei einem 2,3-GB-Video muss er blind herunterladen. Und `new Blob(parts, { type: fileEntry.type })` (2734) übernimmt einen vom Absender kontrollierten String ungeprüft: `text/html` in einem Object-URL wäre ein XSS auf der eigenen Origin.
tun: Allowlist (image/jpeg, png, webp, gif, video/mp4, audio/mpeg, application/pdf), bei Nichttreffer auf `application/octet-stream` zwingen. Rendering über `<img>`/`<video>` mit Object-URL oder sandboxed iframe, nie über `innerHTML` — beides braucht die Direktiven aus 3.1 (`media-src`, `frame-src`, `blob:`). Für große Medien einen separat verschlüsselten Thumbnail-Chunk ins Manifest v2.
Abh.: 3.1, 2.6

**3.14 · mittel · L · Trusted Types**
warum: Ungewöhnlich billig zu haben — kein `eval`, kein `new Function`, kein `document.write` im gesamten Projekt. Genau die Verteidigung, die eine Schlüssel-Exfiltration per DOM-XSS strukturell unmöglich macht, ist nur durch 4 `innerHTML`-Zuweisungen in index.html (2359-2362 Dateizeile, 2698-2701 Empfangszeile) und 10 in `tasks/app.js` bzw. je App-Seite versperrt.
tun: Die Zuweisungen auf `<template>`-Klone oder `createElement` + `textContent` umstellen — die Inhalte sind statische SVG-Icons plus drei Textknoten, das ist mechanische Arbeit. Danach `require-trusted-types-for 'script'; trusted-types 'none'`, erst Report-Only, dann scharf.
Abh.: 3.1, 0.3

**Stufe 3 gesamt: 30–40 PT** (davon 5–10 PT extern in 3.8, plus Terminvorlauf).

---

## Stufe 4 — Qualität & Inhalt

**4.1 · [B] · M · Wahrheitsabgleich aller Produktaussagen** (Wahrheit + Recht)
warum: Die eigene Datenschutzseite ist der beste Kronzeuge gegen die Website. Belegte Falschaussagen: **Löschung** — „Cloaked transfers vanish after 7 days" (2669), „Links expire after 7 days" (1984), „its encrypted pieces are deleted from the relays" (expired.html:2941/3018) gegen privacy.html:1793-1800 „this build contains no delete … In the preview build that word is wrong". **Server** — „Ciphertext only — the server never sees your files" (1926), „The server-side record holds ciphertext, sizes, and a timestamp" (2035, wörtlich in docs.html:1861), „Zero-knowledge server" als Häkchen (pricing.html:1878), „never reaches a server" (dashboard.html:3197, settings.html:2994) — es gibt keinen Server; nur index.html:2097 und privacy.html sagen es richtig. **Größe** — `MAX_TOTAL` = 500 MB und deaktivierter Knopf (2240/2373) gegen „Up to 3 GB per drop" in pricing.html:1813/1865, index.html:2060, **api.html:2160 und :2208** sowie der Seitenleiste aller vier App-Seiten (:2922); nur index.html:1744 und help.html:2026 sagen die Wahrheit — **docs.html enthält entgegen der Rohfassung keine 3-GB-Aussage.** **Rechner** — pricing.html:2274 „Fits comfortably in the free plan" bis 3 GB, darüber Verweis auf ein nicht kaufbares Pro; ein interaktives Element, das aktiv fehlleitet, wiegt schwerer als ein falscher Satz. **Burn und Ablaufpreset** (2.12/2.15). **Newsletter** (2.19). **Version** — settings.html:3002 „0.9.2 · preview" gegen changelog.html:1764 „Preview 0.3". **localStorage** — „Four localStorage keys" gegen neun (1.10). **Doku** — docs.html:2042 „A 256-bit key from crypto.getRandomValues" gegen `crypto.subtle.generateKey` (2419); beide kryptografisch in Ordnung, aber genau hier zählt Präzision, weil verify.html zum Gegenlesen auffordert. **Prüfanweisung** — index.html:2093 „Open your dev tools' network tab while dropping — every byte that leaves is ciphertext"; es verlässt kein Byte, der Tab bleibt leer, und verify.html:1806/1852 sagt das korrekt. **Roadmap** — pricing.html verkauft „Your own drop address" und „Receive inbox" als Pro-Merkmale „At launch", roadmap.html:1778 führt beide unter „Exploring", was about.html:1916 selbst als „nicht gut genug, um sie zu versprechen" definiert. **„€0 forever"** (1809) gegen „Limits may evolve" (1908). **„Developer API"** in Nav und Footer aller 20 Seiten ohne Statuszusatz, während api.html selbst sagt „nothing here is callable yet". **Datumsangaben** — „Last updated 24 Jul 2026" (status.html:1771), 25 July (privacy/imprint:1726), July 24 (docs.html:1905), „Today"/„Yesterday"/„Opened 20 minutes ago" in den App-Seiten; heute ist der 30.07.2026. Sobald die Seite entgeltlich beworben wird, ist das keine Preview-Detailfrage mehr, sondern §§ 5, 5a UWG mit Beweisführung im eigenen Repo.
tun: Zeilenweise abgleichen und entweder bauen oder streichen. Zwei Klassen trennen: prüfbare Client-Eigenschaften bleiben („der Schlüssel liegt im Fragment, Browser senden Fragmente nicht" — mit den Einschränkungen aus 3.5), Aussagen über Serververhalten bis zum Relay in den Konjunktiv oder gestrichen, wie privacy.html:1865-1879 es vormacht. Bis das Relay löscht: „öffnet nicht mehr" statt „verschwindet"/„gelöscht" — betrifft index.html:1784/1984/2669, expired.html:2941/3018/3037, glossary.html:1891-1894, pricing.html:1814. Jede noch nicht wahre Aussage braucht einen unmissverständlichen Statusmarker im Sichtbereich, nicht im Fußtext. Alle Datumsangaben zur Bauzeit injizieren, relative Angaben zur Laufzeit rechnen (4.15), `<time datetime>` verwenden (0 Treffer im ganzen Projekt).
Abh.: 0.3 (damit die Korrektur nicht 20× von Hand passiert)

**4.2 · [B] · M · 85 tote `href="#"` auflösen**
warum: Verteilung gemessen: docs.html 17, blog.html 7, index.html 6, App-Seiten 0 — die vier gebauten Seiten haben dank `assert 'href="#"' not in t` (build_app.py:894) exakt null. Gruppen: Footer-Wortmarke 16× (springt nach oben statt nach Hause); 32 Footer-Social-Icons X und GitHub (bei einem Krypto-Produkt ist das GitHub-Icon eine Vertrauensaussage — ein Klick, der nichts tut, liest sich als „behauptet Open Source, hat keinen Code"); drei prominente CTA-Buttons (`opensource.html:1731` „Star us on GitHub" als Primärbutton, `changelog.html:1756`, `api.html:2270` Footer-Selbstlink); „Terms of Service" im Signup (1.12); 13 in docs.html (4.3); „Cookie settings" 16× als Link mit JS-Handler, obwohl es ein Button ist.
tun: Wortmarken auf `index.html` bzw. `/`; `index.html:1627` (`id="home-link"`) separat entweder auf `/` oder als `<button>`. Social-Icons: X-Handle und GitHub-Organisation anlegen und eintragen (`rel="me noopener"`, `sameAs` im JSON-LD) **oder beide entfernen** — „Icon bleibt, Link tot" ist die schlechteste Variante, weil sie beides verspricht und nichts hält; `opensource.html:1740` sagt selbst „Not published yet — and we won't pretend otherwise", der Knopf darüber tut genau das. „Cookie settings" als `<button type="button">` (Handler bleibt) oder nach 1.11 ersatzlos. Link-Checker im CI (0.5) plus die `build_app.py:894`-Zusicherung auf alle 20 Seiten ausdehnen, damit der Bau schon vor dem CI-Lauf scheitert.
Abh.: 0.3, 1.23

**4.3 · [B] · M · Blog, Docs und Hilfe: schreiben oder Attrappe entfernen**
warum: **Blog:** drei vollständige Vorschaukarten mit Titel, Kategorie, „July 2026", Teaser und „Read post", alle `href="#"`; darüber Suche und Kategoriefilter für exakt drei Karten, darunter ein „Load more", das nach 1150 ms gefaktem Spinner „That's all for now" sagt — eine simulierte Netzwerkanfrage in einem Projekt, das sich rühmt, keine zu machen. `changelog.html:1763` verlinkt „Read more" dorthin und führt ins Leere weiter. **Docs:** Sidebar mit vier Bereichen und sieben Kapiteln, Breadcrumb, „Previous: Introduction"/„Next: Receiving files", Suche, „Was this doc helpful?"-Widget ohne Empfänger, „Last updated on July 24, 2026" — existiert ist ein Kapitel. Besonders bitter: die Anker `#overview`, `#receiving`, `#troubleshooting`, `#the-link` **existieren bereits**, drei Sidebar-Links könnten heute funktionieren. **Hilfe:** „34 articles · six topics" (1988) und sechs Kartenzähler — tatsächlich 34 `hcart`-Links auf **14** Ziele (7× pricing.html, 5× index.html#security, 5× index.html#faq, 3× index.html#login, 3× docs.html#before-you-start); es existiert kein einziger Artikel, „I forgot my password" landet auf dem Login-Formular. `contact.html:1801` wiederholt die Falschzahl.
tun: Blog: drei Beiträge als eigene Seiten mit Titel, Description, OG-Karte, `<time datetime>` und Article-JSON-LD schreiben und verlinken, **oder** blog.html auf „Erste Beiträge folgen" reduzieren (Karten, Suche, Filter, Load-more weg) und aus Sitemap und Footer nehmen. Docs: sofort verdrahtbare Anker setzen, „Accounts" und „The security model" schreiben oder entfernen (alternativ auf `index.html#security` und help.html zeigen), Pager entfernen bis Docs mehrseitig ist, Feedback-Widget verdrahten (4.19) oder entfernen — ein Bewertungsknopf ohne Empfänger ist dieselbe Kategorie Unwahrheit wie der Newsletter-Haken. Hilfe: 34 kurze Antworten wirklich schreiben (dann funktioniert auch die Suche) oder Zahlen und Kartenzähler auf sechs Themen reduzieren, die auf bestehende Seiten verweisen; 1988, die sechs „N articles"-Fußzeilen und contact.html:1801 mitkorrigieren.
Abh.: 0.3, 4.1

**4.4 · [B] · S · Statusseite entschärfen** (Wahrheit + Betrieb)
warum: Die Seite verspricht in ihrer Meta-Beschreibung „honest uptime, no invented numbers" und liefert eine erfundene Zahl: `status.html:1787` „100% uptime since 22 Jul" hartcodiert, Kicker „Live status" (1761), „Last updated 24 Jul 2026" (1771), zwei Komponenten „Operational". **Gemessen: 264 `nodata` + 6 `ok` über 3 Karten (88/2 je Karte)** — der Rest sagt korrekt „no data". Die Seite ist statisch, kann den eigenen Zustand nicht kennen, und wenn sie liegt, ist die Aussage sowieso wertlos. Dieselbe Behauptung steht als Pille „All systems operational" **in 16 Dateien** (nicht 20 — die vier App-Seiten haben sie nicht).
tun: Kicker auf „Komponentenübersicht (statisch)", die drei grünen Balken und „100% uptime since 22 Jul" entfernen, Datum aus dem Deploy generieren. Footer-Pille entweder ersatzlos entfernen (einfachster ehrlicher Zustand) oder aus `/status.json` speisen mit neutralem Startzustand „Status wird geladen" und Fallback „unbekannt"; Änderung im Generator **und** in den 16 Handdateien, sonst driftet sie. Echte Daten kommen mit 5.1/5.4.
Abh.: 0.3, 5.1

**4.5 · [B] · L · Barrierefreiheits-Blocker beheben**
warum: Zehn belegte Verstöße, jeder auf allen oder fast allen Seiten. **13 unsichtbare Tabstopps:** `.nav-dd-panel` (index.html:205-225) und `.theme-menu` (1437-1444) verlieren nur `opacity` und `pointer-events`, nie `visibility` oder `display` — die 10 Panel-Links und 3 Theme-Knöpfe bleiben dauerhaft im Tab-Index und im Screenreader-Baum, `aria-expanded="false"` lügt dabei aktiv; `grep` trifft 20 von 20 Dateien. Vorbild macht es richtig: `.dmenu` in dashboard.html:2504 mit `visibility: hidden`. **Kontrast:** `--text-quaternary: #8b8d98` auf `#fff` = **3,30:1**, auf `--bg-element: #f0f0f3` = **2,87:1**, nötig sind 4,5:1 — ~90 Verwendungen für echten Text (`.progress-note` bei 11 px, `::placeholder`, `.receive-sub`); der Autor kennt es (`dashboard.html:1779` Kommentar) und ist nur für eine Klasse ausgewichen; im Dark-Theme 5,97:1, fällt beim Entwickeln also nicht auf. Zielwert **`#6e7178` = 4,89:1**. **Keine Live-Regionen in index.html** (0 Treffer `aria-live`/`role="status"`): Fortschritt (2456), fertiger Link (2462), Download (2721-2748) und Fehler (`alert`, 2409) sind stumm — ein blinder Nutzer weiß nach „Cloak & create link" bei 3 GB minutenlang nicht, ob etwas passiert; die App-Seiten haben eine ausgereifte `announce()`-Mechanik (`#dsh-sr`, dashboard.html:3254). **Kein Skip-Link** auf keiner der 20 Seiten, obwohl jede ein `<main>` hat (WCAG 2.4.1, Level A). **Fokus unsichtbar:** `.link-input` (564/567) und `.email-input` (946/951) setzen `outline: none` und ersetzen den Ring durch `rgba(28,32,36,0.08)` ≈ 1,1:1 — das Feld mit dem erzeugten Freigabelink ist das wichtigste Bedienelement des Produkts; die `:focus-visible`-Regeln der Knöpfe (279-281) liegen in `@media (hover: hover) and (pointer: fine)` und entfallen auf Touch-Geräten mit Tastatur. **Fokusverlust:** `showState()` (2324-2337) versteckt den Behälter des gerade fokussierten Knopfes, der Fokus fällt auf `<body>`; `enterAuthMode` (2578-2580) macht es richtig. **`role="alert"` auf dauerhaft vorhandenem Text:** `.open-error` (1734) wird nur per `visibility` geschaltet — die Region feuert bei Inhaltsänderung, hier ändert sich nichts: Ansage beim Laden, Schweigen im Fehlerfall; `aria-describedby` verweist dauerhaft auf einen Fehlertext. **Kontextmenü:** `Tab` schließt mit `close(false)` ohne Fokusrückgabe (dashboard.html:3371), `#dmenu` liegt am DOM-Ende (3225) — der Nutzer tabbt aus dem Dokument. **Cookie-Dialog:** `role="dialog"` (2213) ohne `aria-modal`, erscheint nach 600 ms ohne Fokus, liegt hinter dem Footer, `pop.hidden = true` (3111) gibt den Fokus nicht zurück; `<dialog>` kommt im Projekt nirgends vor. **Tabelle:** sechs `<th>` ohne ein einziges `scope` (0 Treffer im ganzen Projekt) und ohne `<caption>`; Detailspalte wechselt Inhalt ohne Ansage; „6 of 6 row(s) selected" in `#dt-sel` ist keine Live-Region.
tun: `visibility: hidden` in die geschlossenen Zustandsklassen und in `.open`/`.theme-menu.open` aufheben; `focusin`/`focusout` an `.nav-dd`, Pfeiltasten/Home/End wie dashboard.html:3363. Token im Light-Theme auf `#6e7178` oder Textverwendungen auf `--text-secondary` (5,94:1); dekorative SVGs dürfen bleiben, brauchen 3:1. `announce()` nach index.html übernehmen und an `showState`, Fortschritt (auf 10-%-Schritte gedrosselt), Fertigmeldung mit Ablaufdatum und Download-Start/Ende/Fehler anbinden; `alert()` durch eine `role="alert"`-Fläche ersetzen, die Fokus erhält. Skip-Link als erstes Kind von `<body>`, `id="main"` + `tabindex="-1"`, sichtbar nur bei `:focus` (nicht `display:none`). `outline: none` entfernen bzw. `outline: 2px solid var(--text-default); outline-offset: 2px` bei `:focus-visible` (dieselbe Regel nutzt app.css schon); `:focus-visible`-Zeilen aus dem `@media hover`-Block herausziehen. In `showState()` Fokus gezielt setzen (bei `st-done` auf `#link-input`), beim Cookie-Banner sichern und zurückgeben. Fehlercontainer leer lassen und Text erst bei Fehlereintritt per `textContent` setzen, `aria-describedby` nur solange ein Fehler ansteht. `close(true)` in Zeile 3371, Ereignis nicht abbrechen, `inert` auf `.dsh-shell` bei offenem Menü. Cookie-Dialog als `<dialog>` mit `showModal()` **oder** als nicht-modales `role="region"` mit `aria-label`, direkt nach dem Skip-Link platziert und einmal per Live-Region angekündigt — die zweite Variante ist ehrlicher, weil das Banner nicht blockiert, und nach 1.11 bleibt ohnehin höchstens ein Hinweis ohne Schalter. `scope="col"` an alle `<th>`, `<caption class="sr-only">`, `aria-live="polite"` am Inhaltsbereich der Detailspalte, `#dt-sel` in die Live-Region. Änderungen über den Generator, damit der geteilte Unterbau nicht auseinanderläuft.
Abh.: 0.3, 1.24 (Reihenfolge: erst beheben, dann erklären)

**4.6 · [B] · M · a11y-, Reflow-, Motion- und Zielgrößen-Gates im CI**
warum: `verify_app.py` startet nur `chromium.launch()` (48), lädt nur die vier App-Seiten (56), `WIDTHS` beginnt bei **400** (16) und es gibt 0 Treffer für `axe`/`reduced_motion`/`forced_colors`. WCAG 1.4.10 verlangt Bedienbarkeit bei **320** CSS px (= 400 % Zoom auf 1280) — genau dort brechen `calc(100dvh - var(--top-h))`, `height: 48px` und das Raster `var(--side-w) minmax(0,1fr)`, und die schmalste Media-Query endet bei 560 px. Screenshots werden geschrieben, aber **mit nichts verglichen** (`tasks/_baseline` enthält zwei HTML-Dateien für die Generator-Idempotenz, keine Bilder). Und eine bestehende Zusicherung ist wirkungslos: `check(page.locator('#theme-menu').is_visible(), …)` (189, 276, 285) — **Playwright bewertet `opacity: 0` als sichtbar**, das Menü wird nur über `opacity` geschaltet; beim `#dmenu` trifft es zufällig zu, weil dort `visibility` verwendet wird. Eine Testlücke, die Sicherheit vortäuscht. 226 fest verdrahtete px-Schriftgrößen gegen 2 rem-Werte, `body { font-size: 16px }` und `-webkit-text-size-adjust: 100%` ignorieren die Systemschriftgröße; fixe Elementhöhen beschneiden wachsenden Text. Zielgrößen (WCAG 2.5.8, 24×24) sind nie gemessen: `.dkebab` hat nur `margin-left: auto`, `.dact` keine Maße, `.dtools` 2 px Abstand, und die umschaltbare Dichte schrumpft alles zusätzlich.
tun: `@axe-core/playwright` über alle 20 Seiten × {light, dark} × {reduced-motion an/aus}, Schwelle „0 serious/critical"; Lighthouse-CI mit `categories:accessibility >= 0.95`. `WIDTHS` auf `[320, 400, 560, 720, 1040, 1440, 1920, 2560]`, bestehende `overflow()`-Prüfung (26-44) unverändert mitlaufen lassen und **auf alle 20 Seiten** ausdehnen; zusätzlich echter Browser-Zoom (200 %, 400 %) und Root-Schriftgröße auf 24 px, dann fällt jede Beschneidung auf. `reduced_motion='reduce'`-Kontext, der prüft, dass `animationName === 'none'` für `.cloak-track`, `.dot-pulse`, `.state.entering` und alle `.scroll-reveal` ohne Scrollen `opacity: 1` haben — drei getrennt gepflegte JS-Pfade (2774, 2971, 3131) können einzeln ausfallen. `forced_colors='active'`-Lauf plus eine `@media (forced-colors: active)`-Sektion, die Rahmen und Fokusringe auf Systemfarben zurückholt (0 Treffer im Projekt). Trefferflächen messen: alle `a, button, input, [role=menuitem], [tabindex="0"]` per `getBoundingClientRect()`, Ziele unter 24×24 melden, Ausnahmeliste für Inline-Links im Textfluss, Läufe für beide Dichten und `pointer: coarse`. Alle `is_visible()`-Zusicherungen für animierte Ein-/Ausblendungen durch `opacity`- und `visibility`-Prüfung ersetzen und für jedes Menü prüfen, dass Bedienelemente im geschlossenen Zustand **nicht fokussierbar** sind — genau der Test, der die 13 Phantom-Tabstopps gefunden hätte. `body`-Basis auf `1rem`, Skala über rem-Tokens, fixe `height` durch `min-height` + Innenabstand; `-webkit-text-size-adjust: 100%` nur mit dokumentierter Begründung behalten.
Abh.: 0.5, 4.5

**4.7 · [B] · M · Onboarding, Leerzustände, Warnung vor dem unumkehrbaren Fehler**
warum: 0 Treffer für onboarding/get started/tour/walkthrough/first drop. Der Composer (1743-1788) führt in drei Klicks zu einem Link, dessen Verlust endgültigen Datenverlust bedeutet und dessen Weitergabe die Weitergabe des Schlüssels ist — weder vor dem Cloak-Knopf noch danach steht ein Satz dazu. `#new-btn` (2490-2493) verwirft den Zustand ohne Rückfrage, und das in 3.4 geforderte Leeren des Link-Felds macht den Verlust sogar wahrscheinlicher, wenn davor keine Führung steht. Für den Empfänger erklärt nichts, was gerade passiert. Das ist der wahrscheinlichste Support-Fall des Launchtags und der einzige mit unlösbarem Ausgang.
tun: Ein Satz vor dem Cloak-Knopf („Der Link ist der Schlüssel — kein Konto, keine Wiederherstellung, keine zweite Chance"). Nach dem Erzeugen ein expliziter Schritt „Link gesichert/gesendet", der den Zustand erst dann freigibt; Warnung bei Verlassen ohne Bestätigung (2.9 baut `beforeunload` für laufende Transfers, hier geht es um den fertigen Link). Empfangsseite mit erklärendem Satz statt nur Name/Größe/Knopf. Echte Leerzustände statt Demodaten (2.18) — ein Leerzustand **ist** Onboarding. 60-Sekunden-Pfad in help.html, der ohne Vorwissen funktioniert, statt 34 Verweise auf andere Seiten.
Abh.: 2.18, 3.4

**4.8 · [B] · M · Social-Karten, Canonical, Indexierung**
warum: `grep 'og:'` und `'twitter:'` = **0 in allen 20 Dateien**. Jeder geteilte Link rendert als nackte URL ohne Titel, Text und Bild — bei einem Produkt, das ausschließlich über geteilte Links wächst, die teuerste einzelne Lücke. Es gibt **keine einzige Bilddatei** im Repo (nur `tasks/*.py/.css/.js/.md` und `tasks/_shots/*.png`); die einzigen Bilder sind 11 eindeutige Inline-PNGs in 90 Vorkommen, größtes 320×320 — weit unter der OG-Mindestgröße. Kein `canonical` (0 Treffer), kein `robots.txt`, keine `sitemap.xml`, kein `name="robots"` — und `dashboard/received/expired/settings` tragen Werbe-Descriptions („The CloakDrop dashboard: what expires next, who it went to"): landet das im Index, findet ein Interessent über Google eine Seite mit fremden Dateinamen und Empfängeradressen und das Datenschutzversprechen ist in einem Suchergebnis zerstört. index.html ist die **einzige** Seite ohne `meta description` (nur Zeilen 4-7: charset, viewport, color-scheme, theme-color) und hat **vier `<h1>`** (Hero 1689, App-Ansicht 1898, Login 1804, Signup 1844) gegen genau eine in den 19 anderen. **Korrektur der Begründung: Cloudflare Pages liefert nicht zwei Pfade aus — es leitet `.html` per 308 auf die extensionslose Form um; der echte Duplikatraum sind `*.pages.dev`/`*.workers.dev` und Preview-Hostnamen sowie apex/www.**
tun: Pro Seite in den `<head>`: `og:type`, `og:site_name`, `og:title`, `og:description` (kann die Description spiegeln), `og:url` absolut, `og:image` mit `width`/`height`, `og:locale`, `twitter:card=summary_large_image` plus Titel/Description/Image. OG-Bild als **Datei** bauen (1200×630, Wortmarke + „Send anything. Reveal nothing." + dunkler Hintergrund, < 300 KB) — Crawler laden `og:image` über HTTP, nicht als data-URI; Varianten für pricing und docs. `rel="canonical"` je Seite auf die extensionslose Form, index.html auf `/`. `robots.txt` (Allow, Disallow für die App-Pfade, Sitemap-Zeiger), `sitemap.xml` mit den 16 öffentlichen Seiten und `lastmod` aus dem Bau. `<meta name="robots" content="noindex, nofollow">` in den vier App-Seiten **plus** `X-Robots-Tag` in `_headers` — doppelt gesichert; Preview-Hostnamen ebenso. Description für index.html (140–155 Zeichen: Verschlüsselung im Browser, Schlüssel im Fragment, Limit, kein Konto). Login-, Signup- und App-Überschrift auf `<h2>`, eine `<h1>` im Hero. **Nicht in `build_app.py` patchen — index.html ist Handarbeit, der Generator erzeugt nur die vier App-Seiten; entweder manuell oder erst nach 0.3.**
Abh.: 0.3, 2.3

**4.9 · hoch · S · 404-Seite**
warum: Kein `404.html`. `not_found_handling` braucht sie im Asset-Verzeichnis; ohne sie sieht ein Nutzer eine nackte Cloudflare-Seite ohne Branding, Navigation und CSP. Bei einem Produkt, dessen Kernmechanik das Ablaufen von Links ist, ist die 404 eine **Produktseite**: sie muss die zwei Fälle trennen. Zusätzlich macht sie Deploy-Fehler sichtbar — ein Deploy, der einzelne Dateien verliert, fällt keinem Health-Check auf, der nur `/` prüft.
tun: `404.html` im Projektstil (gleicher Header/Footer, kein Cookie-Banner-Neuanstoß, `noindex`), Überschrift trennt „Link falsch getippt" von „Drop abgelaufen oder nach dem ersten Download gelöscht", Links auf Start, Docs, Hilfe, Status. `not_found_handling = "404-page"`, sicherstellen, dass der Statuscode **404** ist und nicht 200; für `/dashboard`, `/received`, `/expired`, `/settings` prüfen, dass unbekannte Unterrouten nicht 200 liefern. Monitoring erweitern: bekannte Datei → 200, erfundener Pfad → 404, beides muss stimmen.
Abh.: 2.3, 0.3

**4.10 · hoch · S · Favicon-Satz, Web-Manifest, theme-color, color-scheme**
warum: `rel=` existiert im ganzen Projekt **ausschließlich** als `rel="icon"` (20×) — ein einzelnes 64px-PNG als data-URI (4082 Zeichen) 20-fach dupliziert; iOS-Homescreen, Android-Chrome, Windows-Kacheln und Pinned Tabs finden kein passendes Icon. Kein `rel="manifest"` (die „manifest"-Treffer in api/index/docs sind der Fachbegriff). `theme-color` ist 20× hart `#fff` ohne `media`-Variante, obwohl 19 Seiten ein vollständiges Dark-Theme über `data-theme` haben: im Dark Mode leuchtet eine weiße Adressleiste über einer dunklen Seite. Und index.html ist die einzige Seite mit `<meta name="color-scheme" content="light">` (Zeile 6) und `class="light-theme"` am `<html>` (Zeile 2), obwohl dasselbe Inline-Skript `data-theme="dark"` setzt und 20 Dark-Regeln existieren — Folge nur auf der Startseite: helle Scrollbars, helle Formularfelder, hell gerenderte native Controls im Signup.
tun: Icons aus dem 320px-Logo ableiten: `favicon.ico` (32+16), `icon-192.png`, `icon-512.png` (maskable), `apple-touch-icon.png` (180), `icon.svg`; data-URI in allen 20 `<head>` durch echte `<link>`-Zeilen ersetzen. `manifest.webmanifest` (name, short_name, `start_url: "/"`, `display: standalone`, background/theme-color aus den Tokens, Icons) und `<link rel="manifest">` — **bewusst ohne Service Worker für Caching: ein Cache-First-SW auf einer Krypto-App ist ein eigenes Risiko; der SW aus 2.10 ist ausschließlich Streaming-Proxy.** Zwei `theme-color`-Tags mit `media: (prefers-color-scheme: light|dark)` und das Attribut im bestehenden Inline-Theme-Skript mitsetzen, weil der Nutzer manuell umschalten kann. index.html Zeile 6 auf `content="light dark"` (wie die 19 anderen), verwaiste `class="light-theme"` entfernen bzw. gegen die genutzte Mechanik prüfen — **manuell, index.html wird nicht generiert.**
Abh.: 0.3

**4.11 · hoch · S · Interne Links auf extensionslose Pfade**
warum: **648** interne `href="*.html"` über alle 20 Seiten. Auf Workers Static Assets mit `html_handling = "none"` funktionieren sie, aber Canonical und Verlinkung müssen dieselbe URL-Form nutzen, sonst konkurrieren zwei Adressen; und ein späterer Wechsel auf Pages würde jede Navigation einen 308 kosten.
tun: Skriptlauf über die Quellen: `href="x.html"` → `href="/x"`, gleichzeitig mit dem Canonical-Patch, Link-Checker im CI als Netz. Alle 20 referenzierten Dateinamen existieren — es gibt heute keine toten Seitenlinks.
Abh.: 0.3, 4.8

**4.12 · hoch · S · Zielmarkt und Sprachauszeichnung**
warum: Die Seite widerspricht sich: `lang="en"` auf allen 20 Seiten (0 abschnittsweise Overrides), Preise in Euro, `imprint.html` beruft sich auf § 5 DDG. Diese Frage muss **vor** Canonical, Sitemap, hreflang und OG-Locale entschieden sein, sonst wird die SEO-Struktur zweimal gebaut. Und sie wird beim Launch akut: Impressum, Datenschutzerklärung und die BFSG-Informationen müssen für deutsche Verbraucher auf Deutsch vorliegen — ohne `lang="de"` am Abschnitt liest VoiceOver/NVDA deutsche Rechtsprosa mit englischer Aussprache vor und der Text ist für Screenreader-Nutzer nicht verwertbar (WCAG 3.1.1/3.1.2).
tun: Launch-Empfehlung: englisch-only bleiben, hreflang weglassen, nur `x-default`, `og:locale=en_US`. Deutsche Rechtstexte mit `lang="de"` am umgebenden Abschnitt (bzw. `<html lang="de">`, wenn die Seite ganz deutsch wird); axe-Regeln `html-has-lang`/`valid-lang` im CI, Sprachmischung im Screenreader-Durchgang abhören. **Nicht: deutsche Rechtstexte unter `lang="en"` mischen.** Ein `/de/`-Baum verdoppelt die Pflege und gehört in Stufe 6.
Abh.: 1.2, 1.3

**4.13 · hoch · M · HTML-Validierung, Linting, Link-Checker, visuelle Regression**
warum: Kein Validator, kein Linter, kein `.editorconfig`. Der `$1`-Defekt war genau das, was ein CSS-Linter in einer Sekunde findet: Regeln ohne Selektor. Der Generator prüft nur Tag-Balance per Regex (`build_app.py:794-795`). Positiv geprüft: keine doppelten `id`, alle `<img>` mit `alt`, jede Seite mit `<main>` und `lang` — die Struktur ist nicht schlecht, sie ist unbewacht. Bei 20 Dateien, die einen byte-identischen Unterbau duplizieren, ist eine unbemerkte Divergenz zwischen Hand- und Generatorseiten der wahrscheinlichste Regress überhaupt, und genau der wird nicht erkannt.
tun: `html-validate` oder vnu über `dist/*.html`, Gate „0 errors", plus eigener Strukturcheck (genau eine `<h1>`, lückenlose Überschriftenordnung; axe prüft `heading-order` mit). `stylelint` über `src/css` (selektorlose Regeln, unbekannte Eigenschaften, Duplikate), `eslint` über `src/js` (ES5-Stil mit `'use strict'`, Regeln entsprechend), prettier/`.editorconfig` für lesbare Diffs. Pixelvergleich: entweder `@playwright/test` mit `toHaveScreenshot` oder Pillow-Differenz gegen ein versioniertes `tests/baseline` mit Schwelle und Artefakt-Upload; 20 Seiten × {light, dark} × {320, 720, 1440}. Billige Zusatzprüfung, die das Divergenzproblem direkt fängt: Hash-Vergleich, dass der geteilte CSS-Block in allen Dateien identisch ist.
Abh.: 0.5, 0.3

**4.14 · hoch · S · Demodaten und Zeitstempel aus einer Datenquelle**
warum: `build_app.py:138-141` trägt fest verdrahtete Dateinamen, Empfängeradressen, Größen und Zeitstempel wie „Jul 29, 2026 at 9:12 AM" und **„Today"** — heute ist der 30. Juli 2026, „Today" ist bereits falsch und wird jeden Tag falscher. Die Tests zementieren es (`verify_app.py:80, 306, 338, 345` prüfen konkrete Dateinamen und „2 weeks ago"), sodass jede Korrektur zwangsläufig Tests bricht.
tun: Demodaten nach `src/data/demo-drops.json`, relative Zeiten zur Lade- oder Bauzeit berechnen. Zusicherung: kein Datum in der Vergangenheit wird als „Today" oder „in x h" beschriftet. Tests mit injizierter Uhrzeit (Playwright clock/fake timers) statt echtem Datum — dann sind sie stabil **und** die Seite altert nicht. Solange kein Relay existiert, sichtbar als Beispieldaten kennzeichnen (2.18).
Abh.: 0.3, 2.18

**4.15 · hoch · S · Größenbudget und Core Web Vitals messen**
warum: 6,35 MB roh / 3,30 MB gzip über 20 Dateien, index.html 427,6 KB roh / **216,3 KB gzip**, davon 224.373 B Style und 94.028 B Skript. Es gibt keine Lighthouse-Messung, kein Gate, keine Felddaten. Risiken benennbar: 156 KB Style vor dem ersten Markup verzögert den LCP-Start; die durchgängigen scroll-reveal-Klassen können CLS erzeugen, wenn sie ohne Platzhalterhöhe animieren; 94 KB Inline-Skript belasten INP. Ohne Budget wächst jede Datei weiter, weil jede Ergänzung lokal klein aussieht — die 6,35 MB sind nicht durch eine Entscheidung entstanden, sondern durch deren Abwesenheit.
tun: Lighthouse mobil mit gedrosselter CPU auf index, pricing, docs, dashboard, Ergebnis als Zahl festhalten. Budget in `budget.json`: HTML ≤ 60 KB roh je Seite nach Extraktion, CSS-Bundle ≤ 90 KB roh / 20 KB Brotli, Fonts ≤ 120 KB, Erstansicht ≤ 200 KB Brotli; Zusicherungen „kein data-URI > 2 KB im HTML" und „keine Datei ohne Inhalts-Hash in `/assets/`". Zunächst warnen, ab dem ersten grünen Extraktionsschritt blockierend. scroll-reveal ausschließlich auf `transform`/`opacity`, nie auf `height`/`margin`.
Abh.: 0.3, 4.13

**4.16 · hoch · M · Kompatibilität: @supports-Fallbacks, Browser-Matrix, Echtgeräte**
warum: `@supports` = **0** und `100vh` = **0** in allen 20 Dateien — bei gleichzeitig 34 `:has(`, 53 `color-mix(`, 52 `dvh`, 8 `@container`, 82 `backdrop-filter`. Layoutentscheidungen hängen an modernen Features ohne Rückfallebene: `.dsh-work:has(.drail:not([hidden]))` schaltet das Raster der Detailspalte, `@container work (min-width: 800px)` das Kennzahlenraster, `calc(100dvh - var(--top-h))` die Höhen von Seitenleiste und Detailspalte, 12 `color-mix()` erzeugen alle Zustandsflächen. Wo eines fehlt, fällt die Deklaration still aus: ohne 100vh-Fallback wird `overflow-y: auto` auf einer auto-hohen Fläche wirkungslos, `color-mix`-Hintergründe verschwinden ersatzlos. Betroffen: iOS Safari 15.0–15.3 (kein `:has`, kein `dvh`), Safari 16.0/16.1 (kein `color-mix`), Firefox ≤ 109 (keine Container Queries), ältere Android-WebViews. Dazu iOS-Fokus-Zoom: `.link-input` 12 px (562), `.auth-input` 15 px (1275), `.email-input` 14 px (939) und **0 Treffer für `pointer: coarse` in index.html** — der Nutzer tippt auf das Feld mit dem fertigen Link, die Seite springt auf ~133 %, der Kopieren-Knopf rutscht aus dem Bild; die App-Seiten haben es für zwei Suchfelder gelöst (dashboard.html:1778/2211), die Produktseite nicht. `-webkit-text-size-adjust: 100%` (106) verhindert das Gegensteuern. Und: `backdrop-filter` auf `rgba(255,255,255,0.7)` in einer `position: sticky` Kopfzeile plus drei geschachtelte Sticky-Ebenen plus `dvh` ist die Kombination, die im Simulator gut und auf Geräten schlecht aussieht (Streifenbildung beim Scrollen, Ruckeln beim Ein-/Ausfahren der Adressleiste).
tun: Erst messen: `verify_app.py` auf webkit + firefox erweitern, Screenshots vergleichen. Dann absichern: `@supports not (selector(:has(*)))` mit Einspalten-Layout, `100vh` immer vor `100dvh` deklarieren, statische `rgba()`-Zeile vor jedem `color-mix()`, `@media (min-width: …)`-Basis vor `@container`, `@supports (-webkit-backdrop-filter: blur(1px))` mit deckendem Rückfall. `@media (pointer: coarse) { .link-input, .auth-input, .email-input, #open-input { font-size: 16px } }`. Dokumentierte Browser-Matrix als Datei (Safari 16.4+, Firefox ESR 115+, Chrome/Edge 111+, iOS 16.4+) und eine erkennbare Meldung für alles darunter. **Feste Geräteliste einmal manuell durchgehen und protokollieren — iPhone aktuell, iPhone eine Hauptversion älter, iPad, Android-Mittelklasse, macOS Safari, Windows Firefox ESR, Windows Edge — je Gerät: Kopfzeile scrollen, Detailspalte scrollen, Menü öffnen und scrollen, Composer bis `st-done`, Download auslösen, Dateiauswahl aus der Fotobibliothek mit Mehrfachauswahl. Das findet kein Emulator.**
Abh.: 0.5, 4.6

**4.17 · hoch · M · Version, Changelog, Build-Stempel**
warum: `changelog.html` trägt vier handgeschriebene Einträge („Preview 0.3/0.2/0.1"), keine Semver-Zeichenkette, kein `CHANGELOG.md`, kein Tag, keinen Generator — nach dem Launch wird das Changelog schweigen oder falsch sein, und niemand kann feststellen, welcher Code zu „Preview 0.3" gehört. Keine Seite trägt Version, Commit-Hash oder Baudatum: ein Fehlerbericht ist keinem Stand zuzuordnen, und ein Cache-Problem nicht von einem Code-Problem zu unterscheiden. Bei einem Produkt, dessen Argument „Trust the code, not the promise" lautet, ist ein nicht nachvollziehbares Changelog ein Widerspruch zur eigenen Seite.
tun: `CHANGELOG.md` (Keep a Changelog) als Quelle der Wahrheit, im PR mitgeschrieben; `changelog.html` daraus **bauen** (Markdown → bestehendes Timeline-Markup, Optik unverändert). Version in `package.json`, annotierter Tag je Auslieferung, im Eintrag und im Build-Stempel verlinkt. Conventional Commits als Rohmaterial, Eintragstext bleibt redaktionell — die vorhandenen Einträge sind gut geschrieben. `<meta name="build" content="v0.3.0+<git-sha7> <ISO-Datum>">` in jede Seite und `dist/version.json` (Version, Commit, Bauzeit, Asset-Manifest-Hash); Version im Footer oder auf status.html sichtbar. Stempel aus Git lesen, nicht tippen; bei unsauberem Arbeitsverzeichnis `-dirty` anhängen. Zusicherung: neueste Version in `CHANGELOG.md`, `package.json` und oberstem Eintrag stimmen überein; Widerspruch 0.9.2 vs. Preview 0.3 auflösen (die Changelog-Nummer gewinnt, sie hat eine Historie).
Abh.: 0.1, 0.3

**4.18 · [B] · M · README, CONTRIBUTING, Architekturnotiz, ADRs**
warum: Kein README, kein CONTRIBUTING, kein `docs/`. Für den nächsten Menschen — oder dieselbe Person in drei Monaten — gibt es keinen Einstieg: nicht wie man baut, nicht was `tasks/app.css`/`app.js` sind, nicht dass zwei Generatoren stillgelegt waren, nicht dass 16 Seiten Handarbeit sind, nicht dass die Optik an Textankern hängt. Was existiert, ist `tasks/todo.md` (1.600 Zeilen Arbeitsprotokoll) und `tasks/lessons.md` (84 Zeilen, sehr wertvoll) — beides deutsch, während das Produkt englisch ist: bei einer AGPL-Veröffentlichung kann niemand mitarbeiten. Blocker, weil 1.23 die Veröffentlichung zusagt.
tun: `README.md` englisch: was das Produkt ist, **was heute nicht existiert** (kein Backend, keine Relays, Demodaten — diese Ehrlichkeit steht schon auf opensource.html und gehört ins Repo), Voraussetzungen, `npm ci`/`build`/`test`/`dev`, Verzeichnisbaum. `CONTRIBUTING.md`: Branch-Modell, Commit-Konvention, PR-Gate, wie man eine Seite hinzufügt, wie man CSS ändert (**immer in `src/css`, nie in `dist/`**), wie man die Pixel-Baseline neu setzt. `docs/architecture.md`: warum ein Generator, wo die Grenze zwischen Unterbau und Seiten-CSS liegt (mit den gemessenen Zahlen), warum der Theme-Bootstrap inline bleiben muss, wie das Fragment-Schlüsselmodell funktioniert, welche Zusicherungen die Optik schützen. `docs/decisions/` für ADRs (Cloudflare, AGPL, Kontomodell, Fonts, 404-vs-410). `lessons.md` bleibt — aber die harten Regeln daraus (`min-width:0` an Flex-Kindern, Custom Properties auf jedem Wurzelelement, Selektor sofort im Markup grepen) wandern zusätzlich als Lint-Regeln oder Test-Assertions in den Bau, damit sie nicht nur Prosa sind.
Abh.: 0.3, 1.23

**4.19 · [B] · M · Support- und Feedback-Kanal mit Fristen**
warum: `pricing.html:1817` nennt „Community support" als Free-Leistung, `:1833`/`:1881` „Priority email support" als bezahltes Merkmal — im gesamten Projekt 0 Treffer für community/discord/forum/discussion/mastodon/reddit: es existiert keine Community, die Support leisten könnte, und „Priority" ohne definierte Reaktionszeit ist eine unbestimmte Leistungszusage in einem entgeltlichen Vertrag. `contact.html` nennt keine Antwortzeit und kein Zeitfenster. Es gibt keinen Weg, einen Fehler zu melden, der nicht Sicherheit oder Missbrauch ist: die Roadmap ist Einwegkommunikation, das Doku-Widget (`docs.html:1901`) hat keinen Empfänger. Bei einem Produkt, dessen Krypto nie getestet wurde und das keine Telemetrie hat, ist die erste Nutzerbeschreibung eines Entschlüsselungsfehlers die wertvollste Information des Launchtags — und sie hat heute keinen Weg herein außer einem öffentlichen Post.
tun: Community wirklich aufsetzen (GitHub Discussions ist der billigste Weg und trägt zugleich die AGPL-Zusage) **oder** „Community support" aus Karte und Tabelle streichen. „Priority email support" mit konservativer, öffentlich genannter und messbarer Frist belegen (z. B. 1 Werktag Pro, 3 Werktage Free), Sprachzusage „Deutsch und Englisch" in Impressum und AGB, Erreichbarkeit ehrlich auf contact.html und status.html (Antwort an Werktagen, keine Nachtbereitschaft, Statusseite ist die Wahrheit im Ausfall). Feedback-Ziel mit Empfänger, sichtbar aus Doku, Hilfe und Empfangsseite; Triage: Sicherheit → `security@`, Missbrauch → `abuse@`, Rest → Backlog mit Sichttermin. Fehlerbericht-Vorlage, die genau die Felder erfragt, die ohne Telemetrie fehlen (Browser mit Hauptversion, OS, Dateigröße, Fehlercode aus der Taxonomie, Zeitpunkt) und **ausdrücklich verbietet, den Drop-Link mitzuschicken**. Vorlagensatz für die zehn absehbaren Fälle: Link verloren, Drop abgelaufen, Entschlüsselung scheitert, Upload bricht ab, Zahlung, Kündigung, Widerruf, Löschverlangen, Missbrauchsmeldung, Behördenanfrage.
Abh.: 2.4, 5.3

**4.20 · hoch · M · Presse- und Marken-Assets, Ankündigungskanal**
warum: Keine Bilddatei im Repo — kein Logo in Vektor, kein nutzbares PNG, keine Screenshots, kein OG-Bild. Kein X-, Mastodon- oder GitHub-Konto, kein funktionierender Newsletter, obwohl er der einzige Ankündigungsweg ist, den die Seiten behaupten. Eine Erwähnung auf HN, Product Hunt oder in einem Fachmedium rendert als nackte URL, es gibt kein Material für Rückfragen, und der Betreiber hat keinen Ort, an dem er nachfasst. Die zweite Hälfte ist gefährlicher: eine **erfolgreiche** Ankündigung ist genau der Lastspitzen- und Missbrauchsfall, für den keine Bremse existiert.
tun: `/press` mit Logo (SVG plus PNG in drei Größen), Screenshots hell und dunkel, Ein-Satz-, Ein-Absatz- und Ein-Seiten-Beschreibung, **und der Nennung der Grenzen — was das Produkt nicht kann, ist hier die glaubwürdigste Botschaft und steht auf den Seiten schon in der richtigen Tonlage.** Genau ein Kanal, der Bestand hat, und der wird auch der Zweitkanal für Vorfälle (5.4). Launch-Text, der die offenen Punkte selbst benennt. Vor der Veröffentlichung: Grenzen enger stellen, Alarme scharf, Kill-Switch getestet — die Ankündigung ist ein Lasttest mit Publikum.
Abh.: 2.13, 4.8, 5.2

**4.21 · mittel · S · Strukturierte Daten**
warum: `ld+json` = 0 in allen 20 Dateien. Kein Knowledge-Panel, keine Sitelinks-Suchbox, kein maschinenlesbarer Bezug zwischen Marke, Logo und Kontakt — bei einem Produkt, dem man vertrauen soll, ist die maschinenlesbare Identität Teil der Glaubwürdigkeit. Die Preisseite trägt zwei konkrete Angebote und Limits unstrukturiert im Markup; die fünf FAQ-Einträge auf index.html sind mit `<details>`/`<summary>` semantisch saubere Handarbeit, aber unsichtbar für Rich Results — und es sind genau die Suchanfragen der Zielgruppe („Is this really end-to-end encrypted?", „What does the server see?").
tun: Organization-JSON-LD (name, url, logo = 512px-Datei, `sameAs` sobald die Konten existieren, `contactPoint`) — `legalName`/`address` erst füllen, wenn 1.2 erledigt ist, sonst behauptet das Markup eine Entität, die es nicht gibt. `SoftwareApplication` mit `AggregateOffer` auf pricing.html, erst nach der Brutto/Netto-Entscheidung (1.18) und mit `availability: PreOrder` solange Pro nicht kaufbar ist. FAQPage aus den fünf vorhandenen Paaren generieren, Antworttexte 1:1 — abweichende Antworten im JSON-LD verstoßen gegen die Richtlinien.
Abh.: 1.2, 1.18, 4.3

**Stufe 4 gesamt: 25–35 PT.**

---

## Stufe 5 — Betrieb

**5.1 · [B] · M · Externes Monitoring und ein Health-Endpunkt, der mehr prüft als „Webserver antwortet"**
warum: Es gibt keine Instanz, die merkt, dass die Seite weg ist — Ausfälle entdecken Nutzer, nicht der Betreiber. Und ein 200 auf `/` sagt nur, dass statisches HTML ausgeliefert wird: nichts darüber, ob R2 schreibbar ist, ob der Index erreichbar ist oder ob ein Upload heute funktionieren würde. Der teuerste Ausfall ist der, bei dem alles grün aussieht und trotzdem kein Drop entsteht. **Korrekturen: Cloudflare Health Checks haben keinen Schreibpfad in KV oder D1 — sie erzeugen ausschließlich Health-Check-Analytics und Notifications; und Standalone Health Checks sind ein Pro-Feature, auf Free nicht verfügbar. Außerdem prüfen sie aus dem Cloudflare-Netz und erkennen einen Cloudflare-Ausfall gerade nicht.**
tun: Externes Monitoring aus mindestens drei Regionen (Better Stack, Uptime Kuma o. ä.) auf `GET /` mit Erwartung 200 **und** Body-Marker (der Build-Stempel aus 4.17), Intervall 60 s, 2-von-3-Bestätigung gegen Fehlalarme; mindestens vier Pfade prüfen (`/`, `/pricing`, `/docs`, ein erfundener Pfad → 404), weil ein Deploy Unterseiten kaputtmachen kann, während `/` noch 200 liefert; dazu TLS-Restlaufzeit (< 21 Tage = Warnung) und DNS-Auflösung. `/health` im Worker: Kanarien-Objekt in R2 schreiben, lesen, vergleichen, löschen; Index anpingen; Alter des letzten erfolgreichen Cron-Lauf zurückgeben — Antwort als JSON mit Einzelkomponenten und Gesamtzustand, gedeckelt auf 1 Anfrage pro 30 s, ohne personenbezogene Felder. Wenn ein Cron-Worker die Ergebnisse selbst schreibt, kann er `/status.json` nach KV legen; alternativ Health-Check-Notification per Webhook an einen Worker, der schreibt.
Abh.: 2.7, 4.17

**5.2 · [B] · S · Alarmierung und Schweregrade**
warum: Selbst mit Monitoring gibt es keinen Weg, den Betreiber zu erreichen — ein Ausfall um 3 Uhr und einer um 15 Uhr sind gleich unsichtbar. Ohne Alarm ist Monitoring Dekoration.
tun: Cloudflare Notifications auf Health-Check-Fehlschlag, Worker-Fehlerrate, Billing/Usage und R2-Schwellwerte; Kanal E-Mail **plus** Push (ntfy oder Pushover — E-Mail allein weckt niemand). Schweregrade festschreiben: S1 Totalausfall oder Datenverlustrisiko = sofort; S2 Upload-/Download-Erfolgsquote unter SLO = innerhalb 4 h; S3 kosmetisch = nächster Werktag. Flapping-Schutz: Alarm erst nach 3 Fehlschlägen in Folge. Nachts nur S1 pushen, S2/S3 sammeln. Löschjob-Rückstand > 1 Stunde und `AUTH_TAG_MISMATCH` über Schwellwert sind S1.
Abh.: 5.1

**5.3 · [B] · M · Fehlererfassung mit Scrubbing-Vertrag und Fehlertaxonomie**
warum: `index.html:2408`, `:2746`, `:3160` nutzen ausschließlich `console.error`; kein `window.onerror`, kein `unhandledrejection`, null Netz-APIs. Der häufigste sichtbare Fehler eines E2EE-Produkts ist ein fehlgeschlagenes Entschlüsseln — davon erfährt der Betreiber nichts, und er kann nicht unterscheiden, ob es an einem defekten Teil, einem verkürzten Fragment oder einem Browserfehler lag. **Gefährlicher ist die naive Nachrüstung: Sentry sendet standardmäßig die vollständige `location` inklusive `#fragment`. Ein unbedachter Sentry-Init leakt am ersten Tag jeden Schlüssel jedes Nutzers, der einen Fehler auslöst.**
tun: **Reihenfolge einhalten: erst Scrubbing-Vertrag, dann instrumentieren.** Regel: niemals `location.href`/`hash`, niemals Dateinamen, niemals Dateigrößen unter 4 Stellen Rundung, niemals DOM-Werte, niemals Breadcrumbs mit Eingaben. Wegen des „null externe Ressourcen"-Versprechens: kein Sentry-CDN, sondern eigener Worker-Endpunkt auf eigener Domain und ein 30-Zeilen-Reporter (`window.onerror` + `unhandledrejection`), Payload nur `{code, name, message-nach-Allowlist, ua-major, ts}`, kein Stacktrace mit Pfaden. Falls doch Sentry: self-hosted oder EU-Region, `sendDefaultPii: false`, `beforeSend` löscht `event.request.url` komplett und ersetzt sie durch den Seitennamen, Tunnel über eigene Domain, plus AVV. Fehlertaxonomie mit reinen Codes: `FRAGMENT_MISSING, FRAGMENT_MALFORMED, KEY_IMPORT_FAILED, AUTH_TAG_MISMATCH, PART_MISSING, PART_TRUNCATED, QUOTA_EXCEEDED, EXPIRED, BURNED, CLOCK_SKEW`. Nur Code, Teil-Nummer und Browser-Major melden — nie Dateinamen, nie Fragmentinhalt, nie Drop-ID. Serverseitig zusätzlich jede eingehende Zeichenkette mit `#` und Base64-Muster ab bestimmter Länge verwerfen, denn der Client kann alt sein.
Abh.: 3.1, 1.11

**5.4 · hoch · M · Statusseite auf getrennter Infrastruktur, Incident-Feed, Zweitkanal**
warum: Fällt die Zone aus, ist genau die Seite weg, die den Ausfall erklären soll — und es gibt keinen zweiten Kanal: `status.html:1867` hat nur „Subscribe" (Newsletter, der nichts sendet), die Footer-Social-Verweise sind tot. Cloudflare hat kein Statusseiten-Produkt. Ohne Historie sieht die Seite später aus, als wäre nie etwas passiert — bei einem Produkt, das mit Ehrlichkeit wirbt, wirkt das schlimmer als ein dokumentierter Ausfall.
tun: `status.cloakdrop.com` als CNAME auf getrennte Infrastruktur (Instatus, Better Stack, Statuspage), **DNS-only, nicht proxied**, statisch aus den Health-Check-Daten generiert, damit sie ohne das Produkt-Backend hält. Zweiten Kanal aufsetzen und im Footer wirklich verlinken, plus Incident-RSS/Atom. Vorfallsvorlage (Zeitleiste in UTC, Auswirkung, betroffene Nutzergruppe, Ursache, Maßnahmen mit Termin) und Schweregrad-Matrix ablegen; Vorfälle in dieselbe Datenquelle wie den Status schreiben, damit sie auf Seite und Feed erscheinen **und dort bleiben**. Postmortem-Pflicht ab S2, öffentlich, ohne Schuldzuweisung. Ein Vertraulichkeitsvorfall (Schlüssel oder Klartext exponiert) triggert zusätzlich 1.8. Monitoring-Zusatzprüfung: Alarm, wenn der gerenderte „Last updated"-Wert älter als 24 Stunden oder der Build-Marker älter als 30 Tage ist.
Abh.: 5.1, 4.4, 2.24

**5.5 · hoch · M · SLI, SLO, Fehlerbudget und Produktmetriken ohne Tracking**
warum: Ohne definierte Messgrößen ist jede Verfügbarkeitsaussage Meinung — genau das Problem der Statusseite. Ohne Fehlerbudget gibt es kein Kriterium, wann man aufhört Features zu bauen. Und bei 3 GB entscheidet nicht die Verfügbarkeit, sondern der Abbruch: Speicherdruck, Hintergrund-Tab, Netzwechsel, Quota erscheinen in **keiner** Serverstatistik als Fehler — der Upload hört einfach auf, die Erfolgsquote sieht hervorragend aus, während Nutzer scheitern.
tun: Vier SLIs: Upload-Erfolg (abgeschlossene Manifeste / begonnene Uploads), Download-Erfolg (erfolgreiche Entschlüsselungen / Linkaufrufe mit Fragment), p50/p95 Durchsatz je Größenklasse, Anteil verspätet gelöschter Objekte. SLO-Start ehrlich: 99,0 % Verfügbarkeit (nicht 99,9 — das wäre bei einem Ein-Personen-Betrieb eine Lüge), Upload 99 %, Download 99,5 %, Löschung innerhalb 1 h zu 99,9 %. Abbruchmessung ohne Personenbezug: beim Upload-Init eine Sitzungszahl, pro Teil einen Zähler, beim Abschluss „fertig"; nicht abgeschlossene Sitzungen nach Ablauf als Abbruch zählen, gebündelt nach Größenklasse und letzter erreichter Teil-Nummer. **Drei Produkt-Aggregate, die serverseitig ohnehin ohne Personenbezug entstehen: Drops erzeugt, Drops mindestens einmal vollständig abgeholt, Drops abgelaufen ohne Abholung. Das Verhältnis abgeholt/erzeugt ist die einzige Zahl, die dieses Produkt wirklich braucht — ohne sie ist nach dem Launch nicht entscheidbar, ob die Größengrenze, der fehlende Fortschritt oder der Cookie-Banner die Ursache ist.** Erhebung über Workers Analytics Engine, ausschließlich Aggregate, **keine Drop-Ids und keine IPs** — sonst kollidiert die Messung mit „kein Tracking". Als Aggregat in privacy.html benennen, dann bleibt die Zusage wahr. Monatlicher Blick, keine Dauerüberwachung.
Abh.: 2.7, 1.11

**5.6 · hoch · M · Logging-Konzept mit Feld-Allowlist**
warum: Cloudflare protokolliert standardmäßig Client-IP und die vollständige Request-URI. Das Fragment kommt nie am Server an — gut — aber Drop-ID im Pfad plus IP plus Zeitstempel ist bereits ein Verkehrsdatensatz, der zeigt, wer wann welchen Drop geholt hat. Für ein Produkt, das „kein Tracking" verspricht, ein Widerspruch, der **vor** dem Launch zu lösen ist. Zone-weites HTTP-Logpush ist Enterprise und kommt nicht in Frage; privacy.html:1858/1917 lässt das Feld offen.
tun: Workers Trace Events Logpush (ab Workers Paid) für den Relay-Worker in ein R2-Ziel in der EU, mit **expliziter Feld-Allowlist**: kein ClientIP (oder nur tagesrotierend gesalzen gehasht), kein vollständiger `ClientRequestURI` für Drop-Pfade (nur Route-Muster), keine Referer, kein User-Agent. Stattdessen im Worker ein eigenes strukturiertes Ereignis loggen: Event-Typ, Statuscode, Bytes, Dauer, gehashtes IP-Präfix mit täglich rotierendem Salt aus dem Secrets Store, Land. Lifecycle auf dem Log-Bucket: Löschung nach 7 Tagen. Für Metriken statt Rohlogs Analytics Engine. Das Ergebnis **wörtlich** in privacy.html unter Auftragsverarbeiter und Speicherfristen — die Seite hat dafür einen leeren Platzhalter.
Abh.: 1.3, 1.15, 2.7

**5.7 · hoch · M · Runbooks, Wartungsmodus, Notfall-Rückbau**
warum: Bei einem Ein-Personen-Betrieb ist das Runbook der Ersatz für das Team. Um 3 Uhr unter Stress zu improvisieren, was man tut, wenn der Löschjob hängt oder eine Schlüssel-Leak-Meldung eintrifft, ist der Weg zu einem Vorfall, der zwei Vorfälle wird. Ohne Wartungsmodus sieht jede geplante Arbeit wie ein Ausfall aus: Monitoring alarmiert, Statusseite behauptet Normalbetrieb, Nutzer laufen in halbe Zustände, und ein Upload, der mitten in der Wartung abbricht, hinterlässt einen halben Drop. 0 Treffer für maintenance/degraded/scheduled in allen .html.
tun: Runbooks in `docs/runbooks/`, je maximal eine Seite mit Erkennungsmerkmal, Sofortmaßnahme, Prüfschritt, Eskalation, konkreter Konsole, konkretem Befehl und Umkehrschritt: (1) Seite nicht erreichbar / Rollback, (2) Upload-Fehlerquote hoch, (3) R2-Budget überschritten inkl. Kill-Switch, (4) Löschjob steht, (5) Missbrauchs-/Takedown-Meldung, (6) gemeldete Schwachstelle oder Schlüssel-Leak, (7) TLS-/DNS-Problem, (8) **Domain gelistet** (2.5), (9) **CSAM-Verdacht** (1.25). KV-Flag `maintenance` mit Start, Ende und Text: Worker weist neue Upload-Initiierungen mit 503 und `Retry-After` ab, laufende Uploads und alle Downloads laufen weiter; Banner aus derselben Quelle wie die Statuspille; Alarme fürs Fenster stummschalten, Health-Checks weiterlaufen lassen — **der Ablauf-Job darf im Wartungsfenster nicht stillschweigend pausieren, sonst wachsen die Überfälligen an.** Automatische Degradation als Bereitschaftsersatz: KV-Flag `read_only` (Uploads gesperrt, Downloads laufen), das ein Alarm selbst setzen kann, Circuit Breaker auf Fehlerquote, harte Grenzen statt manueller Eingriffe. Notfallplan auf einer Seite: 1. Upload-Kill-Switch, 2. Banner mit Erklärung, 3. Downloads so lange wie möglich offen, 4. Statusseite auf getrennter Infrastruktur mit Vorfallstext, 5. Mail an betroffene Absender, **falls überhaupt adressierbar — ohne Konten gibt es keine Nutzerliste; das ist eine Konsequenz des Datenschutzversprechens und muss im Plan stehen, nicht im Ausfall entdeckt werden.** Betriebsregel dazu: nur Kanarien-Drops in Prüfkonfigurationen, niemals echte Links in Monitoring-URLs, Tickets, Screenshots oder Alarmtexten; Eingangsregel im Support-Postfach, die verdächtige Muster kennzeichnet.
Abh.: 5.2, 2.13, 2.11

**5.8 · [B] · S · Zugänge, 2FA, Notfallzugriff, Bus-Faktor**
warum: Jeder Notfallhebel setzt Zugang voraus: Kill-Switch, Rollback, Drop-Sperre, Wartungsmodus, 72-Stunden-Meldung. Verliert der Betreiber sein 2FA-Gerät oder ist er zwei Wochen ausgefallen, ist der Dienst weder abschaltbar noch reparierbar — und weil es keine Konten gibt, existiert keine Nutzerliste, über die man informieren könnte. Bei einem Produkt, das Fristen zusagt (30-Tage-Links) und Fristen unterliegt (72 Stunden, DSA-Antwortzeiten), ist der Ein-Personen-Zugang ein Launch-Blocker, kein Komfortthema.
tun: Passwortmanager mit Notfallzugriff; zwei Hardware-Schlüssel (einer offline verwahrt); Wiederherstellungscodes für Cloudflare, Registrar, Zahlungsanbieter und Mailkonto ausgedruckt und getrennt gelagert; Registrar-Auth-Code offline; API-Token getrennt vom Konto-Login mit Ablaufdatum und minimalem Scope, Zugriff auf den Zone-Bereich beschränkt. Eine Vertrauensperson mit Notfallkuvert, die genau zwei Dinge kann: Uploads stoppen und einen Text auf die Statusseite setzen. Abwicklungsplan: was mit laufenden Drops passiert, wenn der Betreiber dauerhaft ausfällt, und wer die Löschzusage dann erfüllt.
Abh.: 2.3, 3.9

**5.9 · [B] · S · Rollback echt üben, Deploy-Gates scharf**
warum: Wenn eine Auslieferung den Composer bricht oder — der ernste Fall — das Fragment in einen Request wandert, muss der Rückweg Minuten dauern, nicht Stunden. Ohne geübten Rollback ist „wir können zurück" eine Annahme.
tun: `wrangler versions upload` erzeugt eine unveröffentlichte Version mit Preview-URL, `wrangler versions deploy` schaltet live — optional als Gradual Deployment mit 10 % Traffic für 30 Minuten. Rückweg `wrangler rollback <version-id>` plus Cache Purge. Jeden Deploy taggen, `deploy-manifest.json` mitschreiben (3.10). Monitoring protokolliert den Build-Marker bei jeder Prüfung, damit Vorfälle Deploys zugeordnet werden können. **Rollback einmal vor dem Launch echt üben**, inklusive Cache-Purge und Marker-Verifikation. Alle CI-Jobs als „required status checks" auf `main`.
Abh.: 0.5, 2.3, 4.17

**Stufe 5 gesamt: 10–15 PT.**

---

## Stufe 6 — Nach dem Launch

Alles hier ist bewusst verschoben, nicht vergessen. Jeder Punkt mit Auslöser.

**6.1 · hoch · S · HSTS-Preload einreichen** — nach 1–2 Wochen stabilem `max-age`, wenn jede Subdomain HTTPS spricht; praktisch unumkehrbar. Auslöser: 2.2 grün, keine geplante Subdomain offen.
**6.2 · hoch · S · DMARC auf `p=quarantine`, dann `p=reject`** — nach ≥ 2 Wochen Reportauswertung. Auslöser: 2.4 plus erste Reports.
**6.3 · [B-nachgelagert] · M · Externes Audit veröffentlichen und verify.html erweitern** — Ergebnis und Behebungsstand publizieren, `deploy-manifest.json` als fünften, echten Check verlinken. Auslöser: 3.8 und 3.10 abgeschlossen.
**6.4 · mittel · L · Trusted Types scharf, Cross-Origin-Isolation, Chunk-Padding, Passphrase-Option** — jeder Punkt einzeln nach Report-Only-Lauf. Auslöser: 3.14, 3.3, 3.5 entschieden.
**6.5 · hoch · M · Markeneintragung begleiten und verteidigen** — 4–6 Monate nach Anmeldung; Widerspruchsfristen beobachten, Domainbestand nachziehen. Auslöser: 1.22 eingereicht.
**6.6 · hoch · S · Incident-Historie und Postmortems pflegen** — ab dem ersten S2. Auslöser: 5.4 steht.
**6.7 · mittel · M · Reproduzierbare Builds ausbauen** — Sigstore-Provenienz, `deploy-manifest` signiert, „Build == Quelle" formulierbar erst wenn alle 20 Seiten aus dem Generator kommen. Auslöser: 0.3 und 3.10.
**6.8 · mittel · L · `/de/`-Baum, hreflang-Paare, Sprachumschalter** — verdoppelt die Pflege; erst wenn der Generator sie trägt. Auslöser: 4.12 auf Variante B umgestellt.
**6.9 · mittel · M · Inhalte nachziehen** — echte Blogbeiträge, Docs-Kapitel „Accounts"/„The security model", 34 Hilfeartikel, strukturierte Daten (4.21), Pro-Features aus der Roadmap (Empfänger-Entität, Receive inbox) — **nur nach Roadmap-Reihenfolge, nicht weil die Preisseite sie schon verkauft.** Auslöser: 4.3 hat die Attrappen entfernt.
**6.10 · hoch · S · Wiedervorlagen** — Kleinstunternehmensstatus jährlich (1.16, 1.24), `security.txt`-`Expires`, DNSSEC-/DKIM-Rollover, VVT-Aktualisierung, DSFA-Neubewertung bei Feature-Änderungen, TIA bei Anbieterwechsel. Auslöser: Kalendereintrag am Launchtag.
**6.11 · mittel · M · Kostenmodell nachrechnen** — nach vier Wochen echter Nutzung: R2-Speicher, Class A/B, Worker-Invocations, USD-Kurs gegen die 4,99 €-Kalkulation. Auslöser: 5.5 liefert Zahlen.

**Stufe 6 gesamt: 15–25 PT, fortlaufend.**

---

## Gesamtaufwand

| Stufe | PT | Anmerkung |
|---|---|---|
| 0 Fundament | 35–50 | ~60 % davon Punkt 0.3 |
| 1 Rechtsträger & Recht | 30–45 | + 6–10 Wochen Fremdlaufzeit |
| 2 Plattform & Backend | 70–95 | ~20 PT allein der Relay |
| 3 Sicherheit | 30–40 | davon 5–10 PT externes Review |
| 4 Qualität & Inhalt | 25–35 | |
| 5 Betrieb | 10–15 | |
| 6 Nach dem Launch | 15–25 | fortlaufend |
| **Summe** | **215–305 PT** | ≈ 11–15 Monate solo in Vollzeit |

Der Vollumfang ist kein Launch, sondern ein Jahresprogramm. Deshalb ist (b) keine Option, sondern der wahrscheinliche Weg.

---

## (a) Die sieben Punkte, die zuerst dran sind

1. **0.1 Versionskontrolle, Freeze-Commit, Off-Site-Backup** — Stunden Arbeit, verhindert den Totalverlust eines nicht regenerierbaren 6,35-MB-Bestands, der einmal in einem Downloads-Ordner liegt. Jeder weitere Punkt hängt daran.
2. **0.7 Domain sichern** — 70 hartcodierte Vorkommen und ein Link, der den Schlüssel an diesen Origin schickt. Fremdlaufzeit (60-Tage-Transfersperre), und ohne sie darf nichts deployt werden.
3. **1.1 Rechtsträger, Gewerbe, USt-IdNr.** — längste Fremdlaufzeit der Liste und Voraussetzung für Impressum, Steuern, Zahlungsanbieter und Versicherung. Jeder Tag Verzögerung hier verschiebt den Launch um einen Tag.
4. **1.26 Versicherungen** — deckt keine Vorfälle vor Vertragsbeginn. Nach dem ersten öffentlichen Request ist der Abschluss für alles Laufende wertlos, also muss er davor liegen.
5. **0.8 + 0.9 Launch-Zuschnitt und Rückwärtsterminplan** — entscheidet den Umfang aller weiteren Stufen. Ohne diese Entscheidung wird an vierzig Blockern parallel gearbeitet und keiner fertig.
6. **2.6 Kryptoformat und Linkversion einfrieren** — Formatbruch nach dem ersten echten Link ist unmöglich, weil Links unveränderlich in Chats leben und bis 30 Tage gültig sind. Muss vor 2.7 stehen. Zusammen mit 2.5 (Host-Trennung), aus demselben Grund.
7. **4.1 + 4.4 Wahrheitsabgleich und Statusseite entschärfen** — kostet fast nichts, entfernt die gefährlichsten Aussagen (Löschung, Server, 3 GB, Burn, 100 % Uptime) und ist von jeder Technik unabhängig; die eigene privacy.html liefert die Belege bereits. **3.4 Fragment-Bereinigung** gehört als Einzeiler in dieselbe Sitzung.

---

## (b) Was bei einem stillen Soft-Launch mit Beta-Hinweis wegfällt

**Zuschnitt:** geschlossene Beta über Einladungs-Token, gesamte Site auf `noindex`, sichtbarer persistenter Beta-Hinweis, harte Grenze 500 MB pro Drop, eine feste TTL (7 Tage), Einzeldatei-Download, kein Bezahlplan.

**Vollständig streichbar (nicht bauen, Attrappe entfernen):**
- **Alles am Bezahlplan:** 1.17 Widerruf/Button-Lösung/Kündigungsbutton, 1.18 Preisangaben, 1.19 Steuern/OSS/Rechnungen, 1.20 Zahlungsanbieter/Dunning, 2.14 Owner-Token-Entitlement-Teil, 4.21 Offer-Markup. Ohne entgeltlichen B2C-Vertrag entfällt Fernabsatzrecht, PAngV-Streit, OSS und die Rechnungsaufbewahrung — pricing.html sagt bereits ehrlich „Preview build — Pro ships with the cloud relay".
- **1.24 BFSG** — greift für entgeltliche B2C-Dienstleistungen; ohne Pro-Plan vermutlich nicht anwendbar (Feststellung trotzdem datiert ablegen). **Die a11y-Blocker in 4.5 bleiben** — sie sind funktionaler Ausschluss, kein Papier.
- **1.21 Kontomodell** reduziert auf „Auth-Ansicht und die vier App-Seiten nicht ausliefern" — das ist der ehrliche Weg zu „kein Konto" und erledigt gleichzeitig 2.18 (App-Schale), 2.23 (Mehrfachempfänger), 4.14 (Demodaten), das `noindex`-Risiko und den Passwort-Digest.
- **2.12 Burn** — Schalter entfernen statt Atomarität bauen. Das schwerste Nebenläufigkeitsproblem der Liste fällt komplett weg; die Zusage steht ohnehin nirgends als bezahltes Merkmal.
- **2.15 Ablaufwahl** — eine feste TTL, Settings-Preset entfernen.
- **2.10 ZIP-Pfad und Mehrdatei-Download** — Einzeldatei genügt; bei 500 MB Deckel ist der Blob-Pfad tragbar, der Service Worker wird von Blocker zu „hoch".
- **2.19 Newsletter** — Formular auf allen 16 Seiten entfernen (der Erfolgshaken ist die Kernlüge), mailto-Hinweis stattdessen. Erledigt Double-Opt-In, AVV mit dem Versanddienst, § 7 UWG und die Versandsubdomain (2.4 teilweise).
- **4.3 Blog** — Seite aus Nav, Footer und Sitemap nehmen statt drei Beiträge zu schreiben; Docs auf das eine existierende Kapitel reduzieren; Hilfe auf sechs Themen ohne Artikelzahlen.
- **4.8 SEO** — bei `noindex` schrumpft der Punkt auf `robots.txt` mit Disallow plus `X-Robots-Tag`; OG-Karten, Canonical, Sitemap und JSON-LD werden nice-to-have. **Die fehlende Description und die vier `<h1>` in index.html bleiben** (Struktur, nicht SEO).
- **4.20 Presse**, **4.12 i18n-Variante B**, **4.21 strukturierte Daten**, **3.14 Trusted Types**, **3.3 Cross-Origin-Isolation**, **3.13 Vorschau**, **1.27 Exportkontrolle**, **1.28 VSBG**, **1.22 Marke** (parallel starten, blockiert nicht), **5.5 SLO-Formalismus** (die drei Produkt-Aggregate bleiben).
- **1.11** vereinfacht sich auf „Cookie-Banner ersatzlos entfernen" — keine Analytics, keine Einwilligung, kein Nachweis.

**Was auch im stillsten Soft-Launch bleibt — ein Beta-Hinweis entschuldigt nichts davon:**
0.1, 0.7, 0.8/0.9 · 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.13, 1.14 (Abuse-Meldeweg und Sperrmechanik — der erste Missbrauchsfall kommt vor dem ersten Nutzer), 1.16, 1.25, 1.26 · 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.11 (echte Löschung), 2.13 (Quoten, Rate-Limits, **Ausgabendeckel und Kill-Switch**), 2.16, 2.17, 2.20, 2.21, 2.24 · 3.1, 3.2, 3.4, 3.5, 3.6, 3.7, 3.8 (wenn „Ende-zu-Ende-verschlüsselt" behauptet wird, ist das externe Review nicht verhandelbar), 3.9, 3.11, 3.12 · 4.1, 4.2, 4.4, 4.5, 4.6, 4.7, 4.9, 4.10, 4.13, 4.16, 4.17, 4.18, 4.19 · 5.1, 5.2, 5.3, 5.7, 5.8, 5.9.

Grob: **Soft-Launch ≈ 120–160 PT** statt 215–305.

---

## (c) Die drei größten Risiken

**1. Unbegrenzte Kosten und Missbrauch — mit einer Domain, die alles gleichzeitig mitnimmt.**
Ein anonymer, kontofreier Speicher bis 3 GB pro Drop, ohne serverseitige Quote (`MAX_TOTAL` deaktiviert einen Button), ohne Rate-Limit („None yet", api.html:2200), ohne Turnstile und **ohne Ausgabendeckel — weder R2 noch Workers lassen sich auf einen Maximalbetrag begrenzen.** Wer das skriptet, erzeugt in einer Nacht Terabytes; der Betreiber erfährt es von der Rechnung. Verschärfend: weil ID und Schlüssel im Fragment stehen, sieht jede Blocklisten-Infrastruktur nur die Startseite — Safe Browsing, SmartScreen und Spamhaus können keinen einzelnen Drop sperren, sondern nur die Domain. Ein einziger gehosteter Phishing-Payload tötet **alle bestehenden Drop-Links, die Marketingseiten und die Mailzustellung gleichzeitig**, mit 1–14 Tagen Einspruchsdauer. Für einen Einzelbetreiber ist das der Pfad zum Existenzrisiko. Gegenmittel liegen in 2.5, 2.13, 5.2, 5.7.

**2. Gebrochene Sicherheits- und Löschversprechen — nachweisbar aus dem eigenen Repo.**
Burn existiert nicht, meldet dem Nutzer aber „New drops delete after the first download". Gelöscht wird nichts, während vier Seiten „vanish" und „deleted from the relays" sagen — privacy.html schreibt selbst: „In the preview build that word is wrong". Der Schlüssel bleibt nach dem Import in Adresszeile, Verlauf, Verlaufssynchronisation und Zwischenablage; Unternehmens-Mailgateways schreiben ihn in ihre Logs. Es gibt keine CSP, obwohl eine Zeile injiziertes JavaScript `location.hash` liest. Die Krypto trägt das ganze Versprechen und hat **null automatisierte Tests**, während changelog und roadmap „Verified with byte-identical round-trips" behaupten. 3 GB sind verkauft, 500 MB implementiert. Jede einzelne dieser Lücken macht die Kernaussage der Website unwahr — und die Belege stehen im eigenen Quelltext, auffindbar mit Strg-F. Folge: §§ 5, 5a UWG mit Beweisführung im Repo, Art. 5/17/32 DSGVO, und Vertrauensverlust bei genau der Zielgruppe, die das Produkt anspricht: den Leuten, die nachprüfen. Gegenmittel: 4.1, 2.11, 2.12, 3.1, 3.4, 3.7, 3.8.

**3. Ein-Personen-Betrieb ohne Fundament und ohne Redundanz.**
Kein Git, kein Backup, kein CI, keine Zugangsredundanz, ein Cloudflare-Konto für Registrar, DNS, Hosting, Speicher, Zustand, Mail und Metriken, 16 handgepflegte Seiten mit 20-fach duplizierten 224 KB Unterbau, kein Rechtsträger, keine Versicherung, kein Notfallzugriff. Ein Festplattenfehler, ein verlorenes 2FA-Gerät, eine Kontosperre oder eine Abmahnung stoppt das Projekt vollständig — und die Fristen, die dieses Produkt eingeht, halten das nicht aus: 72 Stunden nach Art. 33, DSA-Antwortzeiten, 30-Tage-Pro-Links, ein Löschjob, dessen stiller Ausfall die zentrale Zusage bricht, ohne dass die Website etwas anderes als 200 liefert. Ohne Konten gibt es zudem keine Nutzerliste — im Ernstfall ist niemand informierbar. Gegenmittel: 0.1, 0.3, 0.5, 2.24, 5.7, 5.8, plus die ehrliche Konsequenz aus 0.8: kleiner starten.
