diff --git a/AGENTS.md b/AGENTS.md index 9eb7be2..f9faf70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Pagerite is a CMS. See `docs` for the full design and implementation details. Ke - `pagerite/` — Python backend package (hatchling build target). - `app.py` — thin FastAPI assembly: lifespan, `FastAPI(...)`, router includes (route ordering: api/tracking/messages/files routers, then `frontend.route(app, "/")`, then the pages catch-all last). - `state.py` — shared core, no routes: env-derived site constants, `data`/`kanta`, `analytics_store`, the fastapi-vue `frontend`, the render cache (`_html_response`, `_invalidate_pages`), the translator `dispatcher`, slug helpers, `@kanta.bootstrap` hooks. - - `files.py` — `FileStore` (content-addressed, RAM-cached), image derivative helpers (`store_image`), file routes (`/_api/files`, `/_f/` — incl. `/_f/{name}/{filename}` forced-download attachments, `/_themes/`, `/_fonts/`, favicon settings). + - `files.py` — `FileStore` (content-addressed, RAM-cached), image derivative helpers (`store_image`; `store_attachment` keeps non-media uploads as inert `{hash}.file` blobs), file routes (`/_api/files`, `/_f/` — media by hash, attachments only via `/_f/{hash}/{filename}`, all forced-download + nosniff, `/_themes/`, `/_fonts/`, favicon settings). - `api.py` — editor REST + WS: `/_api/pages`, `/_api/structure`, `/_api/settings`, `/_api/toggle-task`, `/_api/translations`, `/_api/ws/editor`, `/_translate/{key}`. - `tracking.py` — visit analytics: GeoIP, client enrichment, favicon fetch, `/_ws`, `/_api/ws/analytics`, the `/_a` page (docs/analytics.md). Both sockets also carry the contact-message protocol (docs/messages.md): `/_ws` routes token-bearing visitor frames, the analytics socket the admin inbox. - `messages.py` — visitor contact threads (docs/messages.md): strict message rendering (`html=False` markdown-it — threads never carry user markup), thread store ops, the visitor/admin socket protocols, and the public `/_contact/files` upload route. diff --git a/docs/messages.md b/docs/messages.md index 3239c85..388b77f 100644 --- a/docs/messages.md +++ b/docs/messages.md @@ -92,18 +92,25 @@ injection point, fed only by the strict renderer. ## Uploads `PUT /_contact/files/{name}` (public, like the messaging socket) stores -panel uploads in the content-addressed file store: raster images through -the usual AVIF/WebP/JPEG derivative pipeline, **anything else as-is**, -rendering as a download chip in the thread. Every file type is allowed -(html, exe, svg…) — safety comes from enforced downloads rather than -type filtering, verified twice: the chip anchor carries a `download` -attribute, and the attachment link is `/_f/{name}/{original-filename}`, -which the file server answers with `content-disposition: attachment` -naming the original file (the name rides in the URL; there is no -filename registry). The plain `/_f/{name}` form additionally forces a -download whenever the stored type is a script carrier (html/xml/svg/ -js), so stripping the filename segment cannot turn an attachment into -an in-origin page; all `/_f/` responses carry `nosniff`. Gates today: a +panel uploads in the content-addressed file store. Images (raster and +SVG) go through the usual AVIF/WebP/JPEG derivative pipeline and inline +in the message — SVGs included: the extension-less link negotiates +raster derivatives, and even an as-is-served `.svg` is script-inert +inside an ``. Everything else (videos, archives, html, exe…) is +stored as an inert `{hash}.file` blob — the original extension is +dropped on disk, so the store can never be double-click-executed — and +linked as `/_f/{hash}/{original-name}`; the `.file` suffix exists only +on the filesystem and is rejected in URLs. Serving is strictly +separated: `/_f/{name}` serves media only (hash with or without +extension), `.orig.` originals stay blocked, and the filename-carrying +route serves only `{name}.file` blobs (never media, never by hash). +Every `/_f/` response carries `content-disposition: attachment` (no +filename needed — the URL's last segment names the download) and +`x-content-type-options: nosniff`, so a store file can never be +rendered or sniffed into something executable in the site's origin; +subresource fetches (``, `