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:
4 files changed
+173
-2
No files matched your search
@@ -2,6 +2,78 @@
|
||||
|
||||
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
|
||||
|
||||
### Fixed
|
||||
|
||||
Reference in new issue
Block a user