diff --git a/public/datenschutz.html b/public/datenschutz.html index ea8c365..680fe3c 100644 --- a/public/datenschutz.html +++ b/public/datenschutz.html @@ -145,17 +145,17 @@
Manche Menschen und Betriebe suchen öffentlich im Netz jemanden, der ihnen eine Website baut, zum Beispiel in einer Kleinanzeige, auf einem Portal für Freelancer oder in einem Frage-Forum. Auf solche Gesuche melden wir uns von uns aus nur einmal und bieten unsere Leistungen an. Weil wir Ihre Daten dabei nicht bei Ihnen erheben, sondern aus Ihrem öffentlichen Beitrag, informieren wir Sie hier und in unserer ersten Nachricht darüber (Art. 14 DSGVO).
-So läuft es ab: Ein KI-Assistent (das Sprachmodell Claude von Anthropic) bewertet, ob die Anfrage zu unseren Leistungen passt, und entwirft eine Antwort. Ein Mitarbeiter von WEBklar liest den Entwurf, ändert ihn bei Bedarf und sendet ihn einmal von Hand: über die Plattform, auf der Sie gefragt haben, oder per E-Mail, wenn Ihr Beitrag darum bittet. Automatische E-Mails verschicken wir nicht. Für E-Mails gilt Abschnitt 8. In unserer Nachricht steht, dass eine KI-Assistentin von WEBklar antwortet, wie lange wir Ihre Angaben speichern und wo Sie diesen Abschnitt finden. Passt Ihre Anfrage nicht zu uns, schreiben wir Ihnen nicht; die Daten speichern wir dann trotzdem bis zur Kürzung (siehe unten), damit wir den Beitrag nicht noch einmal bewerten.
-Die Gesuche finden wir auf zwei Wegen. Erstens schicken uns Plattformen auf unseren Suchauftrag Benachrichtigungs-E-Mails (derzeit Kleinanzeigen, freelancermap und freelance.de); diese Plattformen rufen wir nicht selbst ab. Zweitens findet ein KI-Assistent mit einer Websuche öffentliche Beiträge im Netz und in Frage-Foren, derzeit in den Foren gutefrage.net, forum-hilfe.de, wer-weiss-was.de und wpde.org sowie auf anderen öffentlichen Webseiten. Die Suche und den Abruf der Seiten führt Anthropic aus. Von der Suche ausgenommen sind Plattformen, die automatisierte Zugriffe untersagen, derzeit unter anderem Facebook, Instagram, LinkedIn, Xing, Reddit, Upwork, Fiverr und MyHammer. Wir suchen nach Gesuchen, nicht nach bestimmten Personen.
+So läuft es ab: Ein KI-Assistent (das Sprachmodell Claude von Anthropic) bewertet, ob die Anfrage zu unseren Leistungen passt, und entwirft eine Antwort. Ein Mitarbeiter von WEBklar liest den Entwurf, ändert ihn bei Bedarf und sendet ihn einmal von Hand: über die Plattform, auf der Sie gefragt haben, oder per E-Mail, wenn Ihr Beitrag darum bittet. Automatische E-Mails verschicken wir nicht. Für E-Mails gilt Abschnitt 8. In unserer ersten Nachricht steht, dass eine KI-Assistentin von WEBklar antwortet, wie lange wir die Angaben aus Ihrer Anfrage in unserer Datenbank speichern, dass Sie widersprechen können, und wo Sie diesen Abschnitt finden. Passt Ihre Anfrage nach der Bewertung der KI nicht zu uns, schreiben wir Ihnen nicht und kürzen die Daten innerhalb von 30 Tagen nach dem Fund (siehe unten); bis dahin speichern wir sie, damit wir den Beitrag nicht noch einmal bewerten.
+Die Gesuche finden wir auf zwei Wegen. Erstens schicken uns Plattformen auf unseren Suchauftrag Benachrichtigungs-E-Mails (derzeit Kleinanzeigen, freelancermap und freelance.de); diese Plattformen rufen wir nicht selbst ab. Zweitens findet ein KI-Assistent mit einer Websuche öffentliche Beiträge im Netz und in Frage-Foren, derzeit in den Foren gutefrage.net, forum-hilfe.de, wer-weiss-was.de und wpde.org sowie auf anderen öffentlichen Webseiten. Die Suche und den Abruf der Seiten führt Anthropic aus. Von der Websuche und vom Abruf ausgenommen sind die Plattformen Kleinanzeigen, freelancermap, freelance.de, Facebook, Instagram, LinkedIn, Xing, Reddit, Upwork, Fiverr und MyHammer sowie Seiten, deren robots.txt den Abruf sperrt. Wir suchen nach Gesuchen, nicht nach bestimmten Personen.
Wir speichern den Titel des Beitrags, den Beitragstext (höchstens 4000 Zeichen), den Link, das Datum, Ort und Postleitzahl, wenn sie genannt sind, die Plattform, eine Punktzahl mit kurzer Begründung und, wenn der Beitrag um eine Antwort per E-Mail bittet, die E-Mail-Adresse aus dem Beitrag. Ein eigenes Feld für Ihren Namen gibt es nicht; steht Ihr Name im Titel oder im Text, speichern wir ihn damit. Telefonnummern entfernen wir beim Speichern automatisch aus dem Text; das folgt einer Faustregel und erkennt nicht jede Schreibweise. Dazu kommen der Entwurf unserer Antwort, die gesendete Nachricht mit Text und Zeitpunkt und ein Protokoll der einzelnen Arbeitsschritte, zum Beispiel „Neue Anfrage: Titel des Beitrags“.
Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, auf öffentliche Gesuche nach Website-Leistungen zu antworten. Wir melden uns je Anfrage von uns aus nur einmal, und Sie können der Verarbeitung jederzeit widersprechen.
-Wir speichern die Daten auf unserem Server bei der Hetzner Online GmbH in Deutschland (Abschnitt 3). Für die Bewertung, den Entwurf und die Websuche setzen wir das Sprachmodell Claude der Anthropic Ireland, Limited, Dublin, Irland, als Auftragsverarbeiter ein. Anthropic erhält dafür den Titel, den Text und den Ort des Beitrags sowie bei der Websuche unsere Suchaufträge und die gefundenen Seiten. Dabei können Daten in die USA übermittelt werden, abgesichert durch Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO).
-Die Bewertung der KI entscheidet nicht allein: Sie bereitet nur einen Entwurf vor. Ob und was wir Ihnen schreiben, entscheidet ein Mitarbeiter (Abschnitt 14).
-Wie lange wir die Daten speichern: Wir kürzen Ihre Daten spätestens 90 Tage nach dem Fund. Sagen Sie uns, dass Sie kein Interesse haben, kürzen wir sie innerhalb von 30 Tagen; widersprechen Sie, sperren und kürzen wir sie sofort. Kürzen heißt: Wir löschen Titel, Beitragstext, Link, E-Mail-Adresse, Entwurf und unsere Nachricht an Sie und bereinigen die zugehörigen Einträge im Protokoll. Übrig bleiben nur pseudonymisierte Prüfwerte (Hash-Werte) des Beitrags und gegebenenfalls Ihrer E-Mail-Adresse sowie Ort, Plattform, Punktzahl, Status und die Zeitpunkte von Beitrag und Fund, jedoch ohne Titel, Text, Link und Kontaktdaten. Ein Hash-Wert ist ein Prüfwert, aus dem sich der ursprüngliche Inhalt nicht ablesen lässt; wir erkennen damit nur, wenn derselbe Beitrag oder dieselbe E-Mail-Adresse noch einmal auftaucht, und schreiben Ihnen dann nicht noch einmal. Haben Sie uns geantwortet und übernehmen wir Ihre Angaben als Kontakt oder Kunde in unsere Kundenverwaltung, gelten für diese Daten stattdessen Abschnitt 5 und, wenn ein Vertrag zustande kommt, die Abschnitte 7 und 9.
-Für Kopien außerhalb der Datenbank gilt Folgendes. Die Benachrichtigungs-E-Mails der Plattformen enthalten die Anzeigentexte und liegen in einem eigenen E-Mail-Postfach; eine Postfach-Regel löscht sie dort spätestens nach 90 Tagen. Eine E-Mail, die ein Mitarbeiter Ihnen sendet, liegt außerdem in unserem Postfach; dafür gilt Abschnitt 8 und, soweit die E-Mail ein Geschäftsbrief ist, die Frist aus Abschnitt 9. In den Datensicherungen unseres Servers (Abschnitt 12) können gekürzte Daten noch kurze Zeit enthalten sein, bis die Sicherungen überschrieben werden. Die Antwort, die wir auf einer Plattform abschicken, sehen auch deren Betreiber; dafür gelten dessen Datenschutzhinweise.
+Wir speichern die Daten auf unserem Server bei der Hetzner Online GmbH in Deutschland (Abschnitt 3). Für die Bewertung, den Entwurf und die Websuche setzen wir das Sprachmodell Claude der Anthropic Ireland, Limited, Dublin, Irland, als Auftragsverarbeiter ein. Anthropic erhält dafür den Titel, den Text und den Ort des Beitrags sowie bei der Websuche unsere Suchaufträge und die gefundenen Seiten. Dabei können Daten in die USA übermittelt werden, abgesichert durch Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO); eine Kopie erhalten Sie bei uns (support@webklar.com).
+Die KI sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht. Ob und was wir einer passenden Anfrage antworten, entscheidet ein Mitarbeiter (Abschnitt 14).
+Wie lange wir die Daten speichern: Wir kürzen Ihre Daten spätestens 90 Tage nach dem Fund. Passt Ihre Anfrage nicht zu uns, kürzen wir sie innerhalb von 30 Tagen nach dem Fund. Sagen Sie uns, dass Sie kein Interesse haben, kürzen wir sie innerhalb von 30 Tagen, nachdem wir Ihre Antwort gelesen haben. Widersprechen Sie, sperren und kürzen wir sie sofort, sobald wir Ihren Widerspruch lesen. Kürzen heißt: Wir löschen Titel, Beitragstext, Link, E-Mail-Adresse, Entwurf und unsere Nachricht an Sie und bereinigen die zugehörigen Einträge im Protokoll. Übrig bleiben nur pseudonymisierte Prüfwerte (Hash-Werte) des Beitrags und gegebenenfalls Ihrer E-Mail-Adresse sowie Ort, Plattform, Punktzahl, Status und die Zeitpunkte von Beitrag und Fund, jedoch ohne Titel, Text, Link und Kontaktdaten. Ein Hash-Wert ist ein Prüfwert, aus dem sich der ursprüngliche Inhalt nicht ablesen lässt; wir erkennen damit nur, wenn derselbe Beitrag oder dieselbe E-Mail-Adresse noch einmal auftaucht, und schreiben Ihnen dann nicht noch einmal. Diese Prüfwerte und die übrigen Angaben bewahren wir so lange auf, wie wir verhindern müssen, dass wir Sie wegen desselben Beitrags oder derselben E-Mail-Adresse erneut ansprechen. Haben Sie uns geantwortet und übernehmen wir Ihre Angaben als Kontakt oder Kunde in unsere Kundenverwaltung, gelten für diese Daten stattdessen Abschnitt 5 und, wenn ein Vertrag zustande kommt, die Abschnitte 7 und 9.
+Für Kopien außerhalb der Datenbank gilt Folgendes. Die Benachrichtigungs-E-Mails der Plattformen enthalten die Anzeigentexte und liegen in einem eigenen E-Mail-Postfach bei [OFFEN: Anbieter Alarm-Postfach]; eine Postfach-Regel löscht sie dort spätestens nach 90 Tagen. Eine E-Mail, die ein Mitarbeiter Ihnen sendet, liegt außerdem in unserem Postfach; dafür gilt Abschnitt 8 und, soweit die E-Mail ein Geschäftsbrief ist, die Frist aus Abschnitt 9. In den Datensicherungen unseres Servers (Abschnitt 12) können gekürzte Daten noch enthalten sein: [OFFEN: Aufbewahrung der Server-Sicherungen]. Die Antwort, die wir auf einer Plattform abschicken, sehen auch deren Betreiber; dafür gelten dessen Datenschutzhinweise.
Sie können der Verarbeitung Ihrer Daten jederzeit widersprechen, ohne Gründe anzugeben. Antworten Sie dazu auf unsere Nachricht oder schreiben Sie an support@webklar.com. Wir sperren Ihre Daten dann sofort, schreiben Ihnen nicht mehr und kürzen sie wie oben beschrieben. Weitere Rechte (Art. 15 bis 20 DSGVO) und das Beschwerderecht beschreibt Abschnitt 13.
+Sie können der Verarbeitung Ihrer Daten jederzeit widersprechen, ohne Gründe anzugeben. Antworten Sie dazu auf unsere Nachricht oder schreiben Sie an support@webklar.com. Sobald wir Ihre Nachricht lesen, sperren wir den Beitrag und, soweit wir sie gespeichert haben, Ihre E-Mail-Adresse, schreiben Ihnen dazu nicht mehr und kürzen Ihre Daten wie oben beschrieben. Wir erkennen Sie dabei nur am Beitrag und an der E-Mail-Adresse: Ein neues Gesuch, das Sie später einstellen, ist für uns ein neuer Beitrag; sagen Sie uns dann bitte noch einmal Bescheid. Weitere Rechte (Art. 15 bis 20 DSGVO) und das Beschwerderecht beschreibt Abschnitt 13.
Sie können sich außerdem bei einer Datenschutz-Aufsichtsbehörde beschweren. Für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz, Hintere Bleiche 34, 55116 Mainz, poststelle@datenschutz.rlp.de.
Für einen Vertrag mit uns brauchen wir die dafür nötigen Angaben. Ohne sie können wir den Vertrag nicht schließen. Darüber hinaus sind Sie nicht verpflichtet, uns Daten bereitzustellen. Entscheidungen, die Ihnen gegenüber rechtliche Wirkung entfalten oder Sie in ähnlicher Weise erheblich beeinträchtigen (Art. 22 DSGVO), treffen wir nicht automatisiert. Die Bewertung öffentlicher Anfragen durch einen KI-Assistenten (Abschnitt 11) bereitet nur eine Antwort vor; ob und was wir antworten, entscheidet ein Mitarbeiter, der die Antwort von Hand abschickt.
+Für einen Vertrag mit uns brauchen wir die dafür nötigen Angaben. Ohne sie können wir den Vertrag nicht schließen. Darüber hinaus sind Sie nicht verpflichtet, uns Daten bereitzustellen. Entscheidungen, die Ihnen gegenüber rechtliche Wirkung entfalten oder Sie in ähnlicher Weise erheblich beeinträchtigen (Art. 22 DSGVO), treffen wir nicht automatisiert. Die Bewertung öffentlicher Anfragen durch einen KI-Assistenten (Abschnitt 11) sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht. Für alle anderen bereitet sie nur eine Antwort vor; ob und was wir antworten, entscheidet ein Mitarbeiter, der die Antwort von Hand abschickt.
Wir passen diese Erklärung an, wenn sich unsere Verarbeitung oder die Rechtslage ändert. Es gilt die jeweils hier veröffentlichte Fassung.
diff --git a/scripts/datenschutz-hochladen.mjs b/scripts/datenschutz-hochladen.mjs index 91b338e..6e2545a 100644 --- a/scripts/datenschutz-hochladen.mjs +++ b/scripts/datenschutz-hochladen.mjs @@ -3,6 +3,9 @@ // legal-files, Collection legalVersions): neue Fassung "current", die bisherige gültige wird "superseded" mit validTo. Nur mit // --freigabe=JJJJ-MM-TT; dieser Tag muss der „Stand TT.MM.JJJJ“ in public/datenschutz.html entsprechen. Schon hochgeladene Stände (gleiche source, // also gleicher Commit) werden übersprungen. Den Ablauf teilt sich das Skript mit rechtliches-hochladen.mjs (Funktion hochladen). +// Zwei Sperren gegen eine unfertige Fassung (vor dem Schreiben): (1) steht im Text noch eine „[OFFEN: …]“-Markierung (offene Tatsache im +// Faktenblatt), (2) passt die Prüfsumme neben dem PDF (docs/WEBklar-Datenschutzerklaerung.pdf.sha256) nicht zur HTML, ist das PDF also nicht aus +// dieser HTML gedruckt (scripts/build-legal-pdfs.sh datenschutz), dann Abbruch. // // node scripts/datenschutz-hochladen.mjs --probe nur lesen: Schlüssel, Bucket, Karte datenschutz und ihre Versionen // node scripts/datenschutz-hochladen.mjs --freigabe=JJJJ-MM-TT lädt die Datenschutzerklärung hoch (validFrom = Freigabedatum); ohne Karte, @@ -12,6 +15,7 @@ // Schlüssel: kurzlebiger Appwrite-Schlüssel mit files.read/files.write/documents.read/documents.write in /root/.appwrite-rechtliches.key (chmod 600). // Der Import dieser Datei liest weder den Schlüssel noch nutzt er das Netz; beides geschieht nur in main (deps.schluessel, deps.fetch). // Dieses Skript wird nur geschrieben und mit Nachbau-deps getestet, nie von einem Agenten ausgeführt (auch nicht mit --probe). +import { createHash } from 'node:crypto' import { readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -21,6 +25,8 @@ const DOKUMENT_KEY = 'datenschutz' /** [PDF, HTML]: in dieser Reihenfolge hochgeladen. */ const DATEIEN = Object.freeze(['docs/WEBklar-Datenschutzerklaerung.pdf', 'public/datenschutz.html']) const QUELLE = 'public/datenschutz.html' +/** Neben dem PDF: SHA-256 der HTML, aus der es gedruckt wurde (scripts/build-legal-pdfs.sh datenschutz). */ +const PRUEFSUMME = 'docs/WEBklar-Datenschutzerklaerung.pdf.sha256' /** Für Kunden sichtbar (WOMS „Rechtliches“): darum weder „Workers“ noch „Hustler“. */ const AENDERUNG = 'Abschnitt 11 Interessenten (Antworten auf öffentliche Anfragen)' @@ -71,9 +77,21 @@ export async function main(argv, deps = {}) { freigabe, kartenHinweis: '(die Karte gehört zu WOMS „Rechtliches“; dieses Skript legt keine Karten an)', pruefeInhalt: (lies) => { - const stand = standIn(lies(QUELLE).toString('utf8')) + const html = lies(QUELLE) + const stand = standIn(html.toString('utf8')) if (!stand) return `Abbruch: „Stand TT.MM.JJJJ“ fehlt in ${QUELLE}` if (stand !== ddmmjjjj(freigabe)) return `Abbruch: Stand ${stand} in ${QUELLE} passt nicht zur Freigabe ${ddmmjjjj(freigabe)}` + // offene Tatsachen sind kein veröffentlichungsfähiger Text: die Markierung zuerst durch die geklärte Angabe ersetzen + const offen = [...new Set(html.toString('utf8').replace(//g, '').match(/\[OFFEN[^\]]*\]/g) ?? [])] + if (offen.length > 0) return `Abbruch: offene Markierung im Text von ${QUELLE}: ${offen.join(', ')} (erst klären und ersetzen, dann Stand/Pin setzen, PDF drucken, pushen)` + // das PDF muss aus genau dieser HTML gedruckt sein + let summe = '' + try { + summe = lies(PRUEFSUMME).toString('utf8').trim() + } catch { + return `Abbruch: ${PRUEFSUMME} fehlt oder ist nicht lesbar (PDF mit scripts/build-legal-pdfs.sh datenschutz drucken)` + } + if (summe !== createHash('sha256').update(html).digest('hex')) return `Abbruch: das PDF ist nicht aus dieser HTML gedruckt (Prüfsumme in ${PRUEFSUMME} passt nicht zu ${QUELLE}); scripts/build-legal-pdfs.sh datenschutz` return null }, }, deps) diff --git a/test/datenschutzHochladen.test.js b/test/datenschutzHochladen.test.js index 2212f1d..4074c3d 100644 --- a/test/datenschutzHochladen.test.js +++ b/test/datenschutzHochladen.test.js @@ -1,5 +1,6 @@ import { test } from 'node:test' import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' // Hochladen der Datenschutzerklärung nach WOMS "Rechtliches" (scripts/datenschutz-hochladen.mjs): die Tests rufen main nur mit Nachbau-deps // auf, nie mit dem echten Schlüssel oder dem Netz. Das globale fetch wirft und zählt, damit ein Weg am Nachbau vorbei auffällt. @@ -13,7 +14,8 @@ const m = await import('../scripts/datenschutz-hochladen.mjs') // der Import lie const frueher = ['v1', 'v2', 'v3', 'v4', 'v5'].map((v, i) => ({ $id: `s${i}`, version: v, validFrom: `2026-09-0${i + 1}`, status: 'superseded', source: `x${i}` })) const AKTUELL = { $id: 'alt', version: 'v6', validFrom: '2026-10-05', status: 'current', source: 'kundenbereich@aaaaaaa:public/datenschutz.html' } // stand = das Datum, das im Nachbau der HTML-Datei hinter "Stand" steht (die PDF-Datei ist beliebiger Inhalt) -function backend({ versionen = [...frueher, AKTUELL], karten = [{ $id: 'datenschutz', key: 'datenschutz' }], gitStatus = '', stand = '20.10.2026', vorhanden = () => true } = {}) { +// zusatz = zusätzlicher Inhalt der HTML (zum Beispiel eine offene Markierung); summe = Inhalt der .sha256 (null = passend zur HTML, 'fehlt' = nicht lesbar) +function backend({ versionen = [...frueher, AKTUELL], karten = [{ $id: 'datenschutz', key: 'datenschutz' }], gitStatus = '', stand = '20.10.2026', vorhanden = () => true, zusatz = '', summe = null } = {}) { const aufrufe = [], log = [] const antwort = (obj) => ({ ok: true, status: 200, text: async () => JSON.stringify(obj) }) const fetch = async (url, opt = {}) => { @@ -27,8 +29,12 @@ function backend({ versionen = [...frueher, AKTUELL], karten = [{ $id: 'datensch if (methode === 'PATCH' && pfad.includes('/collections/legalVersions/documents/')) return antwort({}) throw new Error(`unerwartet: ${methode} ${pfad}`) } - const html = () => Buffer.from(stand === null ? '' : ``) - return { aufrufe, log, deps: { fetch, schluessel: () => 'test-schluessel', log: (z) => log.push(z), git: (args) => (args[0] === 'status' ? gitStatus : 'abc1234\n'), dateien: { existsSync: vorhanden, readFileSync: (rel) => (rel.endsWith('.html') ? html() : Buffer.from('x')) } } } + const html = () => Buffer.from((stand === null ? '' : ``) + zusatz) + const pruefsumme = () => { + if (summe === 'fehlt') throw new Error('ENOENT') + return Buffer.from(`${summe ?? createHash('sha256').update(html()).digest('hex')}\n`) + } + return { aufrufe, log, deps: { fetch, schluessel: () => 'test-schluessel', log: (z) => log.push(z), git: (args) => (args[0] === 'status' ? gitStatus : 'abc1234\n'), dateien: { existsSync: vorhanden, readFileSync: (rel) => (rel.endsWith('.html') ? html() : rel.endsWith('.sha256') ? pruefsumme() : Buffer.from('x')) } } } } const schreibend = (b) => b.aufrufe.filter((a) => a.methode !== 'GET') @@ -91,6 +97,30 @@ test('Stand in der HTML muss der Freigabe entsprechen, sonst Abbruch vor jedem S const richtig = backend({ stand: '21.10.2026' }) assert.equal(await m.main(['--freigabe=2026-10-21'], richtig.deps), 0, 'mit passender Freigabe geht derselbe Stand durch') }) +test('offene Markierung im Text: Abbruch vor jedem Schreiben, mit Nennung der Markierung; Kommentare zählen nicht', async () => { + const offen = backend({ zusatz: 'bei [OFFEN: Anbieter Alarm-Postfach]
' }) + assert.equal(await m.main(['--freigabe=2026-10-20'], offen.deps), 1) + assert.ok(offen.log.some((z) => z.includes('offene Markierung') && z.includes('[OFFEN: Anbieter Alarm-Postfach]')), offen.log.join(' | ')) + const zweiMal = backend({ zusatz: '[OFFEN: Aufbewahrung der Server-Sicherungen] und noch einmal [OFFEN: Aufbewahrung der Server-Sicherungen]
' }) + assert.equal(await m.main(['--freigabe=2026-10-20'], zweiMal.deps), 1) + assert.equal(zweiMal.log.filter((z) => z.includes('offene Markierung'))[0].match(/Aufbewahrung der Server-Sicherungen/g).length, 1, 'jede Markierung nur einmal genannt') + for (const b of [offen, zweiMal]) assert.equal(schreibend(b).length, 0) + const kommentar = backend({ zusatz: '' }) + assert.equal(await m.main(['--freigabe=2026-10-20'], kommentar.deps), 0, 'eine Markierung im HTML-Kommentar ist nicht sichtbar') +}) +test('Prüfsumme neben dem PDF muss zur HTML passen, sonst Abbruch vor jedem Schreiben (veraltetes PDF)', async () => { + const veraltet = backend({ summe: 'a'.repeat(64) }) + assert.equal(await m.main(['--freigabe=2026-10-20'], veraltet.deps), 1) + assert.ok(veraltet.log.some((z) => z.includes('das PDF ist nicht aus dieser HTML gedruckt') && z.includes('build-legal-pdfs.sh datenschutz')), veraltet.log.join(' | ')) + const fehlt = backend({ summe: 'fehlt' }) + assert.equal(await m.main(['--freigabe=2026-10-20'], fehlt.deps), 1) + assert.ok(fehlt.log.some((z) => z.includes('WEBklar-Datenschutzerklaerung.pdf.sha256') && z.includes('fehlt')), fehlt.log.join(' | ')) + for (const b of [veraltet, fehlt]) assert.equal(schreibend(b).length, 0) + // passende Prüfsumme (Standard im Nachbau): der Upload geht durch + const passt = backend() + assert.equal(await m.main(['--freigabe=2026-10-20'], passt.deps), 0) + assert.equal(schreibend(passt).length, 4) +}) test('--probe liest nur: Bucket, Karte und Versionen, meldet eine fehlende Karte als Stopp', async () => { const b = backend() assert.equal(await m.main(['--probe'], b.deps), 0) diff --git a/test/legalTexts.test.js b/test/legalTexts.test.js index fecac16..e80c906 100644 --- a/test/legalTexts.test.js +++ b/test/legalTexts.test.js @@ -745,28 +745,37 @@ test('legal_datenschutz_anfragen: Abschnitt 11 beschreibt nur den Anfrage-Weg (K const s = between(strip(html), '11. Interessenten: Antworten auf öffentliche Anfragen', '12. Empfänger im Überblick') for (const t of ['Art. 14 DSGVO', 'KI-Assistent', 'Claude von Anthropic', 'von Hand', 'Automatische E-Mails verschicken wir nicht', 'Für E-Mails gilt Abschnitt 8', // Quellen: Alarm-E-Mails und Websuche; Plattformen mit Bot-Verbot ausgenommen - 'Benachrichtigungs-E-Mails', 'Kleinanzeigen, freelancermap und freelance.de', 'Websuche', 'Frage-Foren', 'Die Suche und den Abruf der Seiten führt Anthropic aus', 'Facebook, Instagram, LinkedIn, Xing, Reddit', + 'Benachrichtigungs-E-Mails', 'Kleinanzeigen, freelancermap und freelance.de', 'Websuche', 'Frage-Foren', 'Die Suche und den Abruf der Seiten führt Anthropic aus', + // feste Ausschlussliste (so wie BOT_VERBOT_DOMAINS im Dienst) statt eines Kriteriums + 'Von der Websuche und vom Abruf ausgenommen sind die Plattformen Kleinanzeigen, freelancermap, freelance.de, Facebook, Instagram, LinkedIn, Xing, Reddit, Upwork, Fiverr und MyHammer sowie Seiten, deren robots.txt den Abruf sperrt', 'nicht nach bestimmten Personen', // Daten 'Titel des Beitrags', 'Beitragstext (höchstens 4000 Zeichen)', 'den Link', 'Ort und Postleitzahl', 'die Plattform', 'Punktzahl mit kurzer Begründung', 'E-Mail-Adresse aus dem Beitrag', 'Ein eigenes Feld für Ihren Namen gibt es nicht', 'Telefonnummern entfernen wir beim Speichern automatisch', 'Faustregel', // Rechtsgrundlage, Empfänger - 'Art. 6 Abs. 1 lit. f DSGVO', 'Hetzner Online GmbH', 'Anthropic Ireland, Limited', 'als Auftragsverarbeiter', 'USA', 'Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO)', - // keine automatisierte Einzelentscheidung - 'entscheidet ein Mitarbeiter (Abschnitt 14)', - // Fristen und Reste - 'spätestens 90 Tage nach dem Fund', 'innerhalb von 30 Tagen', 'sperren und kürzen wir sie sofort', 'pseudonymisierte Prüfwerte (Hash-Werte)', 'Ort, Plattform, Punktzahl, Status', + 'Art. 6 Abs. 1 lit. f DSGVO', 'Hetzner Online GmbH', 'Anthropic Ireland, Limited', 'als Auftragsverarbeiter', 'USA', 'Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO); eine Kopie erhalten Sie bei uns (support@webklar.com)', + // keine automatisierte Einzelentscheidung: die KI sortiert aus, ein Mitarbeiter entscheidet über jede Antwort + 'Die KI sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht', 'Ob und was wir einer passenden Anfrage antworten, entscheidet ein Mitarbeiter (Abschnitt 14)', + // Hinweis in der ersten Nachricht (der Dienst setzt ihn in Claras Entwurf; Wortlaut in text-weg-b-aenderungen.md) + 'In unserer ersten Nachricht steht, dass eine KI-Assistentin von WEBklar antwortet', 'in unserer Datenbank speichern, dass Sie widersprechen können', + // Fristen und Reste: aussortierte Anfragen 29/30 Tage (Regel B), Absage und Widerspruch erst ab dem Lesen unserer Seite + 'spätestens 90 Tage nach dem Fund', 'Passt Ihre Anfrage nach der Bewertung der KI nicht zu uns, schreiben wir Ihnen nicht und kürzen die Daten innerhalb von 30 Tagen nach dem Fund', + 'innerhalb von 30 Tagen, nachdem wir Ihre Antwort gelesen haben', 'sperren und kürzen wir sie sofort, sobald wir Ihren Widerspruch lesen', 'pseudonymisierte Prüfwerte (Hash-Werte)', 'Ort, Plattform, Punktzahl, Status', 'jedoch ohne Titel, Text, Link und Kontaktdaten', 'bereinigen die zugehörigen Einträge im Protokoll', 'Abschnitt 5 und, wenn ein Vertrag zustande kommt, die Abschnitte 7 und 9', + // Dauer der Reste (Art. 14 Abs. 2 lit. a: Kriterium) + 'so lange auf, wie wir verhindern müssen, dass wir Sie wegen desselben Beitrags oder derselben E-Mail-Adresse erneut ansprechen', // Kopien 'spätestens nach 90 Tagen', 'Datensicherungen unseres Servers', 'Geschäftsbrief', // Widerspruch - 'Ihr Widerspruchsrecht gegen diese Verarbeitung (Art. 21 DSGVO)', 'Antworten Sie dazu auf unsere Nachricht oder schreiben Sie an support@webklar.com']) assert.ok(s.includes(t), t) + 'Ihr Widerspruchsrecht gegen diese Verarbeitung (Art. 21 DSGVO)', 'Antworten Sie dazu auf unsere Nachricht oder schreiben Sie an support@webklar.com', + 'Sobald wir Ihre Nachricht lesen, sperren wir den Beitrag und, soweit wir sie gespeichert haben, Ihre E-Mail-Adresse', + 'Wir erkennen Sie dabei nur am Beitrag und an der E-Mail-Adresse', 'Ein neues Gesuch, das Sie später einstellen, ist für uns ein neuer Beitrag; sagen Sie uns dann bitte noch einmal Bescheid']) assert.ok(s.includes(t), t) // der Widerspruch steht im Kasten (box widerspruch) im Abschnitt 11 const kasten = html.slice(html.indexOf(''), html.indexOf('