docs: sharing, mail and provenance in changelog, README and llms.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
tillandClaude Opus 5 committed 2026-09-01 03:43:04 +00:00
1 parent 14dae8e550
commit 5a438e2c19
4 files changed
+173 -2

No files matched your search

+72
View File
@@ -2,6 +2,78 @@
All notable changes to Klarbild are documented here. Newest first. All notable changes to Klarbild are documented here. Newest first.
## 2026-09-01 — Sharing, e-mail, password reset, and a security pass
### Added
- **Share links** (`/s/<slug>`) for a single image or a collection: someone without an
account can view them and download at full resolution. These are photos of children,
so the defaults lean safe — every link expires by itself (30 days, configurable), can
be revoked and un-revoked, optionally carries a passphrase, download can be switched
off entirely, and pages and files are noindex. Access is bound to the link, not the
image: swapping in another item id on a valid link returns 404. Private-session
results can never be shared.
- **Passing it on** by e-mail (several recipients, no attachment — a link can be
withdrawn later, an attachment cannot) or via `wa.me`, which opens WhatsApp with the
text prepared. The WhatsApp Business API needs a Meta account, approved templates and
per-conversation fees; this works today for free and the API can slot in behind it.
- **E-mail through the account's own mailbox** (SMTP, credentials AES-256-GCM encrypted
like every other secret). Powers password reset, optional job-done notifications, and
mailing share links. nodemailer has zero dependencies and no native postinstall —
checked before adding it, given the history.
- **Password reset**: single-use token, only its hash stored, one hour, identical
response whether or not the address exists, rate limited per address and per sender.
- **`/konto`** for the e-mail address and notifications, **`/teilen`** for managing links
with view and download counts.
- **IPTC/XMP provenance** on AI results (`trainedAlgorithmicMedia`, or
`compositeWithTrainedAlgorithmicMedia` for edited photos) plus the model, written by
hand into PNG iTXt and JPEG APP1 rather than pulling in a sharp upgrade. exiftool reads
it back from both formats; pixels, dimensions and dpi provably untouched.
- **CSP with real hashes** for every inline script, plus nosniff, referrer-policy,
X-Frame-Options and a permissions-policy. `style-src` stays open because React sets
styles as attributes and attributes cannot be hashed — measured, the hash-only policy
breaks the print preview.
- `mem_limit: 3g` in compose. sharp can spike; without a limit an outlier takes the whole
container down instead of just the process.
### Fixed
- **Sessions never expired server-side and survived a password reset.** `Max-Age` is a
request to the browser, not a rule for the server. A leaked cookie stayed valid
indefinitely, and the very reset meant to rescue a compromised account left the
intruder logged in; a demoted admin also kept admin, because the role came from the
cookie. Cookies now carry a session epoch, checked against the database on every
request, and the epoch is bumped on password reset and on any role change.
- **"Allow download: off" did nothing for images with transparency** — exactly the
cutouts and stickers this tool exists for. Verified: now 1500x2000 instead of the
2400x3200 original, and the download route returns 403.
- **The e-mail settings card saved nothing** while reporting success. Found by the design
critique an hour after shipping.
- **The public page explained how to bypass "view only"**, printing the long-press hint
regardless of the setting.
- The share title went into outgoing mail HTML unescaped while the message was escaped.
- argon2 ran on every request for protected links, including every image (130 ms, 64 MiB
each). Checked once now, then a short-lived signed cookie scoped to that link. The
passphrase moved from the query string to POST.
- `days` accepted nonsense and quietly produced an unlimited link (`Number('abc')` is
NaN, `NaN > 0` is false; `[]` coerces to 0).
- Rate limits keyed on the *first* X-Forwarded-For value, which the caller sets. Counting
from the right now, trusted-proxy count configurable.
- **The path-escape hole in delivery** the wiki had flagged twice as open.
- The crop maths lived twice with nothing holding the two copies together. One module
now, 60000 randomised cases asserting both entry points agree — which immediately
found a real bug: the zoom limits also clamped the *initial* crop, so an extreme format
silently broke the aspect ratio.
- Smaller: expired links could still be mailed; missing PUBLIC_BASE_URL sent dead links
while reporting success; the reset response time revealed whether an account existed;
`share_views` grew without bound; double provenance marking appended a second packet
with the older model; the colour flag covered the selection circle in the library; the
seventh nav item pushed logout off screen at 820px; `/konto` was unreachable on a
phone; network failures rendered as empty states.
### Changed
- 42 new tests (97 total). `shares`, `mail` and `provenance` had none, and that is where
both reviews found the most.
- `PRODUCT.md` and `DESIGN.md` added, written from the code and the wiki.
## 2026-08-21 — Unique, chronologically sortable filenames ## 2026-08-21 — Unique, chronologically sortable filenames
### Fixed ### Fixed
+28 -2
View File
@@ -57,13 +57,39 @@ Migrationen und Seeds laufen beim Serverstart automatisch. Erststart-Passwörter
chronologisch; Kürzel aus der Item-ID → zwei Bilder können sich beim Ausliefern **nicht mehr überschreiben**. chronologisch; Kürzel aus der Item-ID → zwei Bilder können sich beim Ausliefern **nicht mehr überschreiben**.
Zeitzone `KLARBILD_TZ` (Standard `Europe/Berlin`). Nachbenennung der Bibliothek und erneute Auslieferung Zeitzone `KLARBILD_TZ` (Standard `Europe/Berlin`). Nachbenennung der Bibliothek und erneute Auslieferung
im Admin unter „Dateinamen" (`src/lib/renames.ts`), idempotent. im Admin unter „Dateinamen" (`src/lib/renames.ts`), idempotent.
- **Admin:** Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, Dateinamen, Picdrop-Diagnose. - **Teilen (`/s/<slug>`, öffentlich):** ein Bild oder eine Sammlung ohne Konto zugänglich machen —
ansehen und in voller Auflösung herunterladen. Voreinstellungen bewusst vorsichtig: **Ablauf**
(Standard 30 Tage), **widerrufbar**, optionales **Zugangswort** (argon2), **Download abschaltbar**,
`noindex`. Zugriff hängt am **Link**, nicht am Bild (fremde `item`-Kennung ⇒ 404); private Sitzungen
lassen sich nie teilen. Verwaltung unter `/teilen` mit Aufrufen und Downloads. Gezählt wird *wann*
und *womit*, nicht *wer*: IP nur gehasht, gesalzen mit Server-Geheimnis **und** Link-Kennung, eine
Zeile je Besucher und Stunde, Löschung nach 90 Tagen. Kern: `src/lib/shares.ts`.
- **Weitergeben:** per E-Mail aus Klarbild heraus (mehrere Empfänger, **kein Anhang** — ein Link lässt
sich zurückziehen), über das eigene Mailprogramm oder per `wa.me`-Link, der WhatsApp mit fertigem
Text öffnet. (Server-seitiger WhatsApp-Versand bräuchte die Business-API: Meta-Konto, genehmigte
Vorlagen, Gebühren je Konversation.)
- **E-Mail (`src/lib/mail.ts`):** SMTP über das **eigene Postfach** — SPF/DKIM stimmen dadurch von
selbst und kein Versanddienst sieht mit. Zugang AES-256-GCM in `settings`. Trägt Passwort-Reset
(Einmal-Token, nur Hash gespeichert, 60 min, gleiche Antwort für unbekannte Adressen),
Auftragshinweise (je Nutzer, `/konto`) und den Versand von Teilen-Links.
- **Sitzungen:** signiertes Cookie **plus** serverseitige Prüfung (`session_epoch`). Passwortwechsel und
Rollenänderung entwerten alle offenen Anmeldungen; die Rolle kommt aus der Datenbank, nicht aus dem
Cookie.
- **KI-Kennzeichnung (`src/lib/provenance.ts`):** IPTC `DigitalSourceType` als XMP direkt in der Datei
(`trainedAlgorithmicMedia` bzw. `compositeWithTrainedAlgorithmicMedia`) samt Modell — von Hand in
PNG-`iTXt` und JPEG-`APP1` geschrieben, ohne neue native Abhängigkeit. Mit exiftool prüfbar.
- **Sicherheits-Header:** CSP mit echten Hashes für jedes inline-Skript (`experimental.csp`), dazu
`nosniff`, `referrer-policy`, `X-Frame-Options`, `permissions-policy`. `style-src` bleibt offen, weil
React Stile als Attribute setzt und Attribute sich nicht hashen lassen.
- **Admin:** Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, E-Mail, Teilen-Links,
Dateinamen, Picdrop-Diagnose.
## Datenmodell / Migrationen ## Datenmodell / Migrationen
Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichtige Spalten: Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichtige Spalten:
`items.has_alpha`, `jobs.status` (inkl. `failed`), `jobs.finished_at`, `items.has_alpha`, `jobs.status` (inkl. `failed`), `jobs.finished_at`,
`settings.output_ext` / `recipes.output_ext`, `settings.*_metadata_sidecar`, `delivery_targets.*`, `settings.output_ext` / `recipes.output_ext`, `settings.*_metadata_sidecar`, `delivery_targets.*`,
`print_presets` (Bogen-Vorlagen, `013`). `print_presets` (Bogen-Vorlagen, `013`), `shares`/`share_items`/`share_views` + `settings.smtp_*` +
`users.email`/`notify_jobs` (`014`), `users.session_epoch` (`015`).
## Tests ## Tests
`npm test` — Node-Testrunner mit nativem Type-Stripping (`node --experimental-strip-types`). `npm test` — Node-Testrunner mit nativem Type-Stripping (`node --experimental-strip-types`).
+32
View File
@@ -9,6 +9,38 @@ import Base from '../layouts/Base.astro';
</div> </div>
<p class="lead">Was in Klarbild neu ist. Neueste Änderungen oben.</p> <p class="lead">Was in Klarbild neu ist. Neueste Änderungen oben.</p>
<section>
<div class="ver"><span class="tag">01.09.2026</span><h2>Bilder teilen, E-Mail, Passwort vergessen</h2></div>
<ul>
<li><b>Teilen-Links.</b> In der Bibliothek auf <b>Teilen</b> — einzeln oder für mehrere markierte
Bilder auf einmal. Es entsteht ein Link, den auch jemand <b>ohne Klarbild-Konto</b> öffnen kann;
von dort lässt sich das Bild in voller Auflösung herunterladen.</li>
<li><b>Weitergeben per WhatsApp</b> (öffnet WhatsApp mit fertigem Text), über dein eigenes
Mailprogramm, oder direkt aus Klarbild heraus per E-Mail an mehrere Empfänger auf einmal.</li>
<li><b>Sicher voreingestellt:</b> Jeder Link <b>läuft von selbst ab</b> (Standard 30 Tage, im Admin
änderbar), lässt sich jederzeit <b>abschalten</b> und wieder freigeben, auf Wunsch mit einem
<b>Zugangswort</b> schützen, und Herunterladen kann man ganz sperren. Suchmaschinen bekommen die
Seiten nie zu sehen. Bilder aus privaten Sitzungen lassen sich gar nicht teilen.</li>
<li><b>Übersicht unter <a href="/teilen">Geteilte Links</a>:</b> Aufrufe, Downloads und die Zahl der
Besuchenden je Link. Festgehalten wird <b>wann</b> und mit welcher Art Gerät geöffnet wurde —
nicht <b>wer</b>. Nach 90 Tagen wird das gelöscht.</li>
<li><b>E-Mail-Versand</b> über dein eigenes Postfach (Admin → E-Mail). Damit funktionieren:
<b>Passwort vergessen</b>, ein <b>Hinweis wenn ein Auftrag fertig ist</b> (unter
<a href="/konto">Mein Konto</a> einschaltbar) und der Versand von Teilen-Links.</li>
<li><b>Passwort vergessen?</b> Auf der Anmeldeseite unten. Der Link aus der Mail gilt eine Stunde
und lässt sich nur einmal verwenden. Dafür muss unter <b>Mein Konto</b> eine E-Mail-Adresse
hinterlegt sein.</li>
<li><b>Behoben:</b> Ein neues Passwort beendet jetzt auch alle noch offenen Anmeldungen. Vorher
blieb eine bestehende Sitzung bis zu 30 Tage gültig — wer sein Passwort zurücksetzte, weil
jemand anderes hineingekommen war, hatte davon also nichts.</li>
<li><b>Behoben:</b> Bei freigestellten Motiven wirkte „Herunterladen sperren" nicht.</li>
<li><b>KI-Bilder tragen jetzt eine maschinenlesbare Kennzeichnung</b> (IPTC/XMP) samt verwendetem
Modell — direkt in der Bilddatei.</li>
<li>Kleinigkeiten: farbige Markierung liegt nicht mehr auf dem Auswahlkreis; die Kopfleiste passt
wieder aufs Tablet; der Admin-Bereich ist am Handy bedienbar.</li>
</ul>
</section>
<section> <section>
<div class="ver"><span class="tag">21.08.2026</span><h2>Eindeutige Dateinamen — Bilder überschreiben sich nicht mehr</h2></div> <div class="ver"><span class="tag">21.08.2026</span><h2>Eindeutige Dateinamen — Bilder überschreiben sich nicht mehr</h2></div>
<ul> <ul>
+41
View File
@@ -19,6 +19,7 @@ const BODY = `# Klarbild
Basis-URL: https://klarbild.heidrich-digital.de Basis-URL: https://klarbild.heidrich-digital.de
Öffentliche Vorstellung des Druckmoduls (ohne Login, reine Beschreibung): /vorstellung Öffentliche Vorstellung des Druckmoduls (ohne Login, reine Beschreibung): /vorstellung
Öffentliche Teilen-Links: /s/<slug> (ohne Login, nur was der Link freigibt)
Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · pg-boss. Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · pg-boss.
## Authentifizierung (für Agenten) ## Authentifizierung (für Agenten)
@@ -120,6 +121,46 @@ Werkzeug \`klarbild\` (mcp/klarbild-mcp.mjs), nutzt denselben API-Token:
- exact_size — OHNE KI: ein Bild exakt auf ein Maß bringen, Datei lokal speichern. - exact_size — OHNE KI: ein Bild exakt auf ein Maß bringen, Datei lokal speichern.
- print_sheet — OHNE KI: Druckbogen (mehrere Bilder × Stückzahl, Schnittmarken) als PDF lokal speichern. - print_sheet — OHNE KI: Druckbogen (mehrere Bilder × Stückzahl, Schnittmarken) als PDF lokal speichern.
## Teilen-Links (öffentlich, ohne Konto)
Ein Bild oder eine Sammlung unter \`/s/<slug>\` zugänglich machen — der Empfänger braucht kein Konto.
- Voreinstellungen sind bewusst vorsichtig (auf den Bildern sind Kinder): **Ablauf** (Standard 30 Tage,
\`settings.share_default_days\`), jederzeit **widerrufbar** und wieder freigebbar, optionales
**Zugangswort** (argon2), **Herunterladen abschaltbar**, Seiten und Dateien tragen \`X-Robots-Tag: noindex\`.
- Der Zugriff hängt am **Link**, nicht am Bild: Eine fremde \`item\`-Kennung an einem gültigen Link
liefert 404. Bilder aus privaten Sitzungen lassen sich gar nicht teilen.
- Zustände: 200 (gültig) · 401 (Zugangswort nötig oder falsch) · 404 (unbekannt) · 410 (abgelaufen oder
widerrufen). Das Zugangswort geht per **POST**, danach quittiert ein kurzlebiges Cookie je Link.
- \`GET /api/s/<slug>/file?item=<uuid>\` — Ansichtsfassung (auf 2000 px verkleinert).
\`&download=1\` liefert das Original; bei gesperrtem Download 403. \`&thumb=1\` für die Übersicht.
- \`GET|POST /api/shares\` — eigene Links auflisten bzw. anlegen. Body: \`{ itemIds[], title?, note?,
days?, allowDownload?, password? }\`. \`days\` muss eine Zahl von 0–3650 sein (0 = unbegrenzt);
alles andere wird **abgelehnt**, nicht als „unbegrenzt" gedeutet.
- \`GET|PATCH|DELETE /api/shares/<id>\` — Zugriffe ansehen, ändern (\`action: 'revoke'|'unrevoke'\`,
\`title\`, \`note\`, \`days\`, \`allowDownload\`, \`password\`), löschen.
- \`POST /api/shares/send\` — Link per Mail verschicken: \`{ shareId, to, message? }\`, höchstens 10
Empfänger je Anfrage, 50 Mails je Stunde und Absender. **Kein Anhang** — ein Link lässt sich
zurückziehen, ein Anhang nicht.
- WhatsApp läuft über einen \`wa.me\`-Link im Browser (kein Server-Versand; die WhatsApp-Business-API
bräuchte Meta-Konto, genehmigte Vorlagen und Gebühren).
- Gezählt wird **wann** und mit welcher Art Gerät geöffnet wurde, nicht **wer**: von der IP nur ein
Hash, gesalzen mit Server-Geheimnis **und** Link-Kennung (also nicht über Links hinweg verknüpfbar),
höchstens eine Zeile je Besucher und Stunde, Löschung nach 90 Tagen.
## E-Mail
Versand über das eigene Postfach (SMTP, Zugang verschlüsselt in \`settings\`; Admin → E-Mail).
Gebraucht für: Passwort zurücksetzen, Hinweis bei fertigen Aufträgen (je Nutzer schaltbar unter
\`/konto\`), Teilen-Links verschicken. Ohne eingerichteten Versand melden die betroffenen Routen 503.
- \`POST /api/auth/reset { email }\` — Rücksetz-Link anfordern. Antwortet **immer gleich**, egal ob es
die Adresse gibt. \`POST /api/auth/reset { token, password }\` — neues Passwort setzen; Token gilt
60 Minuten, genau einmal, und entwertet alle offenen Sitzungen des Kontos.
- \`GET|PATCH /api/me\` — eigenes Konto: \`email\`, \`notify_jobs\`.
## KI-Kennzeichnung
Ergebnisse tragen IPTC \`DigitalSourceType\` als XMP in der Datei:
\`trainedAlgorithmicMedia\` (aus Text erzeugt) bzw. \`compositeWithTrainedAlgorithmicMedia\`
(vorhandenes Foto verändert), dazu \`xmp:CreatorTool\` mit dem verwendeten Modell.
Auslesbar mit exiftool. Das Druckmodul erzeugt nichts mit KI und kennzeichnet folglich nichts.
## Dateinamen der Ergebnisse ## Dateinamen der Ergebnisse
Schema: \`JJJJ-MM-TT_HHMMSS_<motiv>_<format>_<kürzel>.<ext>\` — z. B. \`2026-08-20_143207_lea-badge_the-frame_a3f9.jpg\`. Schema: \`JJJJ-MM-TT_HHMMSS_<motiv>_<format>_<kürzel>.<ext>\` — z. B. \`2026-08-20_143207_lea-badge_the-frame_a3f9.jpg\`.
- Der Zeitstempel steht vorn, deshalb sortiert jede Dateiliste (Picdrop, NAS, Finder) von allein chronologisch. - Der Zeitstempel steht vorn, deshalb sortiert jede Dateiliste (Picdrop, NAS, Finder) von allein chronologisch.