404 lines
22 KiB
Markdown
404 lines
22 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to **volumen** are documented in this file.
|
||
|
||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
||
> Versions ≤ 0.3.0 were implemented in Ruby. The current codebase is Python
|
||
> (FastAPI). Earlier release notes are kept verbatim as historical record.
|
||
|
||
## [development]
|
||
|
||
## [0.4.2] — 2026-07-27
|
||
|
||
### Fixed
|
||
|
||
- **`web/templates/*.jinja` missing from distribution wheel** — added `web/templates/*.jinja` to `[tool.setuptools.package-data]` in `pyproject.toml`. After a fresh install from PyPI the admin panel returned 500 because `Jinja2Templates` could not find any templates.
|
||
- **Duplicate `script-src` / `style-src` in admin CSP** — the nonce-augmented directives used to be *appended* after the base ones, so the browser (which honours the first occurrence) ignored the nonce and blocked every inline JS/CSS. They are now *replaced*: `script-src` and `style-src` are filtered out of the base CSP and the nonce versions are appended as the only occurrence.
|
||
- **CSP nonce generated after template rendering** — `request.state.csp_nonce` is now set in `GlobalSecurityHeadersMiddleware.dispatch()` *before* `call_next(request)` so templates see it during rendering. Previously it was generated afterwards, so `csp_nonce` was an empty string in the template context and every inline script was blocked.
|
||
- **Missing `nonce` attribute on `<script>` in `list.html.jinja` and `settings.html.jinja`** — inline scripts in these templates now carry `nonce="{{ csp_nonce | default('') }}"` so they match the nonce in the CSP header.
|
||
- **Bootstrap `admin.password_hash` ignored when `users.toml` is empty** — `Users.all` now falls back to the bootstrap admin even when `users.toml` exists but is empty (e.g. right after `volumen init`, which creates the file via `Path.touch()`). Previously the first login after a clean install failed.
|
||
- **`admin_context` did not pass `users_exist`** — the login page showed the “No users configured” warning even when authentication worked. Added `"users_exist": users_obj.any` to the admin template context.
|
||
- **Inline `style="..."` blocked by strict admin CSP** — the admin templates used inline `style="..."` attributes (and one `card.style.display = ...` JS assignment) which the new strict `style-src 'self' 'nonce-…'` directive blocks. Replaced every offending attribute with a utility class in the `<style>` block (`page-head-actions`, `form-options`, `form-inline`, `form-hidden`, `form-spaced`, `flex-spacer`, `btn-static`, `btn-danger-text`, `text-fg-strong`, `badge-inline`, `h3-section`, `mt-3`, `is-hidden`); the dynamic `background-image: url(...)` on `.post-cover` now flows through a `--cover-url` custom property (`style="--cover-url: …"`) which CSP3 does not treat as an inline style application.
|
||
- **`/favicon.ico` returned 404** — browsers auto-request `/favicon.ico` even when the page declares an explicit `<link rel="icon">`. Added a tiny route in `create_app()` that serves the packaged `web/static/volumen-icon.svg` with `Content-Type: image/svg+xml` and a 1-day `Cache-Control`; added the matching `<link rel="icon" type="image/svg+xml" href="/favicon.ico">` to the admin layout so the tab gets the volumen icon instead of a generic placeholder.
|
||
- **Sidebar version label rendered as bare `v`** — `admin_context` did not include the package `VERSION`, so the footer `<div class="sidebar-version">v{{ version }}</div>` rendered as just `v` with no number behind it. Added `"version": VERSION` to the context.
|
||
|
||
## [0.4.1] — 2026-07-27
|
||
|
||
### Fixed
|
||
|
||
- **Missing CSP nonce for `style-src` on admin routes** — added `style-src 'self' 'nonce-{nonce}'` to the CSP header on `/admin` routes. Without a nonce the browser blocked every inline `<style>` tag in the admin templates.
|
||
- **`web/static/` SVG assets missing from distribution wheel** — added a `[tool.setuptools.package-data]` section to `pyproject.toml` so `volumen-icon.svg` and `volumen-logo.svg` ship inside the installed wheel. The admin panel previously returned 500 on `/admin/icon.svg` and `/admin/logo.svg`.
|
||
|
||
## [0.4.0] — 2026-07-26
|
||
|
||
### Added
|
||
|
||
- **`volumen init`** — a new `init` subcommand is the *operational*
|
||
bootstrap (config, data dirs, admin password, optional systemd unit). Run
|
||
as root for a system install (default `--systemd`); pass `--local` for a
|
||
per-user install (`~/.config/volumen/`, `~/.local/share/volumen/`). The
|
||
command renders the production-hardened config from the same commented
|
||
TOML template (`host = "::1"`, `env = "production"`, `trust_proxy = true`,
|
||
`cookie_secure = true`), prompts for an admin password (`getpass`), hashes
|
||
it via the existing scrypt helper, generates a 64-byte hex session key,
|
||
creates the system user (`useradd --system`) and the `/etc/volumen/` /
|
||
`/var/lib/volumen/` layout, and (with `--systemd`) installs a hardened
|
||
`/etc/systemd/system/volumen.service` and runs
|
||
`systemctl daemon-reload && systemctl enable --now`.
|
||
- **PyPI publish metadata** — `pyproject.toml` gains `readme`, `keywords`,
|
||
a complete `classifiers` array (Development Status, Framework :: FastAPI,
|
||
supported Python versions, OS, Topic, Typing), and a `[project.urls]`
|
||
block (Homepage, Repository, Issues, Changelog, Documentation). PyPI now
|
||
renders the README on the project page and links back to sourcedock.dev.
|
||
- **`volumen status`** — read-only inspection of an installation. Prints
|
||
human output by default or `--json` for tooling; reports `config_exists`,
|
||
`data_dir_exists`, `users_file_exists`, `password_hash_set`,
|
||
`session_key_ok`, `systemd_unit_installed`, `service_active`,
|
||
`admin_user_present`, and any issues found. Exit code is `1` when any
|
||
issue is reported.
|
||
- **`volumen check-update`** — compares the installed version (read via
|
||
`importlib.metadata`) with the latest release on PyPI. Uses only
|
||
`urllib.request` (stdlib, 5 s timeout); exit code is `0` up-to-date,
|
||
`1` update available (suggests `uv tool upgrade volumen`), `2` PyPI
|
||
unreachable / offline / malformed response. `--json` returns the same
|
||
fields plus `checked_at` for cron-friendly alerting.
|
||
- **`src/volumen/installer.py`** — a new module exposing `system_install`,
|
||
`user_install`, `inspect`, plus the private `_mutate_config_text` helper
|
||
used to apply line-anchored substitutions to the rendered TOML template
|
||
(preserves comment blocks byte-for-byte).
|
||
- **Tests** — `tests/test_installer.py` adds 15 tests covering both install
|
||
flows, the config-template mutator, the systemd-unit rendering,
|
||
idempotency / backup behaviour, the `inspect` round-trip, and the
|
||
`--systemd` + `--local` CLI guard.
|
||
|
||
### Changed
|
||
|
||
- **First-run setup is now `volumen init`** — replaces the legacy
|
||
`install.sh`. The CLI subcommand renders config, creates data dirs,
|
||
prompts for an admin password, generates a session key, and (with
|
||
`--systemd`) installs and enables a hardened systemd unit. No bash
|
||
required.
|
||
- **Publishing now targets PyPI** — `release.yml` runs
|
||
`uv publish --token "$PYPI_TOKEN"` after `python -m build` for `v*`
|
||
tags. Wheel + sdist continue to be uploaded to sourcedock.dev Releases as a
|
||
secondary mirror. End users get `uv tool install volumen` and
|
||
`pipx install volumen` as the one-line install path; no checkout
|
||
required.
|
||
- **Distribution path is `uv tool install`** (or `pipx install`) — the
|
||
CLI binary is installed globally; the operational bootstrap is delegated
|
||
to `volumen init`. The legacy `uv sync` + project-local venv at
|
||
`/opt/volumen/.venv` is gone.
|
||
- **`Config.load` round-trips config-file `content_dir` / `users_file`** —
|
||
the rendered template places these two keys after the `[server]` header,
|
||
so TOML table folding puts them inside `[server]`. `Config.load` now
|
||
hoists them to the root so files produced by `volumen init` (and any
|
||
hand-edited config) round-trip through the canonical accessors without
|
||
relying on CLI overrides.
|
||
- **Migrated from Ruby to Python**: the runtime has been rewritten on top of
|
||
FastAPI + Uvicorn, replacing Sinatra with FastAPI, kramdown with
|
||
python-markdown + pymdown-extensions, Puma with Uvicorn, Bundler with uv,
|
||
minitest with pytest, RuboCop with Ruff, and the TOML parser
|
||
(`toml-rb`) with the standard library `tomllib` (plus `tomli-w` for
|
||
round-tripping edits). The public API (`/api/volumen/*`) and the
|
||
server-rendered admin (`/admin/*`) keep the same shape and HTTP semantics.
|
||
- **Installer** — `install.sh` now bootstraps `uv` instead of Bundler, runs
|
||
`uv sync --frozen` to provision `/opt/volumen/.venv`, and points the
|
||
systemd `ExecStart` at the venv-resident `volumen` console script. The
|
||
Python runtime is auto-installed on Fedora/openEuler; other distros get a
|
||
heredoc with the equivalent `apt` / `pacman` / `zypper` / `apk` lines.
|
||
- **CI** — Forgejo Actions moved to `setup-uv` + Ruff + pytest on a single
|
||
Python 3.12 job (was: Ruby 3.3/3.4 matrix with Bundler, RuboCop, and
|
||
`bundle exec rake test`). The release workflow builds wheel + sdist via
|
||
`python -m build` and uploads them with the Forgejo Releases API.
|
||
- **Migration guard** — new `.forgejo/workflows/migration-guard.yml` greps
|
||
the tracked tree for Ruby-only terms (`bundle exec`, `RuboCop`, `Sinatra`,
|
||
`Puma`, `kramdown`, `gem build`, `Volumen::`, `Rack::Session::Cookie`,
|
||
`OpenSSL::KDF`, `lib/volumen/`) and fails the build if any of them leak
|
||
back into production paths.
|
||
- **Configuration** — `config.toml` gains six new keys surfaced in
|
||
`config.toml.example`: `[server].env` (`"production"` enables strict
|
||
startup), `[server].trust_proxy` (honour `X-Forwarded-Proto` from
|
||
nginx), `[admin].min_password_length` (default `10`),
|
||
`[admin].max_password_length` (default `1024`, caps scrypt work),
|
||
`[admin].max_upload_bytes` (default `10485760`, 10 MB), and
|
||
`[admin].cookie_secure` (default `false` in dev, set `true` behind
|
||
HTTPS). See `docs/configuration.md` for the full schema.
|
||
- **Default bind address** — the installer and `config.toml.example`
|
||
default to `host = "::"` (IPv6 with automatic IPv4 fallback) so the
|
||
service is reachable on both stacks out of the box. Bind to `"::1"`
|
||
when running behind a reverse proxy.
|
||
- **Test client now uses `httpx2`** — dev dependencies replace
|
||
`httpx>=0.28` with `httpx2>=2.7` (the actively maintained fork by
|
||
Pydantic). Starlette 1.3.x prefers `httpx2` in `TestClient` and emits
|
||
`StarletteDeprecationWarning` when only legacy `httpx` is installed;
|
||
shipping `httpx2` keeps the test suite warning-free. The legacy
|
||
`httpx` package is still pulled in via `fastapi[standard]` for the
|
||
`fastapi` CLI and remains API-compatible. The unused
|
||
`ignore::DeprecationWarning:starlette.testclient` filter is removed
|
||
from `[tool.pytest.ini_options]` (it targeted the wrong warning
|
||
class and never matched).
|
||
|
||
### Removed
|
||
|
||
- `install.sh` — the bash installer is deleted. Use `uv tool install
|
||
volumen` (or `pipx install volumen`) followed by `volumen init`.
|
||
- `pkg/` build output and `volumen.gem` (Ruby gem packaging is gone; the
|
||
project now ships as a wheel + sdist built with `python -m build`).
|
||
- `Gemfile`, `volumen.gemspec`, `Rakefile`, `.rubocop.yml` and the
|
||
Sinatra-era `lib/` tree.
|
||
- `.editorconfig` — Ruff (`pyproject.toml`'s `[tool.ruff]`) is the single
|
||
source of truth for Python formatting; modern editors default to UTF-8,
|
||
LF, and a trailing newline. Markdown files in this repo use blank lines
|
||
between paragraphs (no ` \n` line-break idiom), so trimming trailing
|
||
whitespace is safe.
|
||
|
||
### Security
|
||
|
||
- **scrypt password hashing** now uses `hashlib.scrypt` and constant-time
|
||
verification via `secrets.compare_digest` (was: Ruby's `OpenSSL::KDF`).
|
||
- **Session cookies** are signed by Starlette's `SessionMiddleware`
|
||
(`itsdangerous`) with `HttpOnly` and `SameSite=Strict`; `Secure` is added
|
||
by `SecureCookieMiddleware` when the request was forwarded over HTTPS
|
||
(only when `[server].trust_proxy = true`).
|
||
- **CSRF tokens** are generated with `secrets.token_hex(32)` and verified
|
||
with `secrets.compare_digest` on every state-changing admin POST.
|
||
- **Markdown sanitisation policy** — the renderer still passes inline HTML
|
||
through (authors are the trusted admin); document the trust model
|
||
explicitly in `docs/security.md` so deployments that ingest untrusted
|
||
content know to add a sanitiser.
|
||
|
||
> Note: refine the Security bullets above once the Python agent confirms the
|
||
> final cryptographic controls (e.g. whether `image/*` upload signatures get
|
||
> re-validated server-side and whether the CSRF token is rotated on login).
|
||
|
||
## [0.3.0] — 2026-07-12
|
||
|
||
### Added
|
||
|
||
**Content & authoring**
|
||
|
||
- **Fediverse attribution:** a `fediverse_creator` frontmatter field (e.g.
|
||
`@user@mastodon.social`) plus a site-wide `[site].fediverse_creator` default
|
||
in `config.toml`. The value is exposed as `fediverse_creator` on the site
|
||
and post payloads, rendered as `<dc:creator>` in the RSS feed, as
|
||
`authors[].name` in the JSON feed, and can be consumed by a front-end to
|
||
emit `<meta name="fediverse:creator">` on rendered pages. Per-post values
|
||
override the site default.
|
||
- **Cover caption:** a `cover_caption` post field rendered next to the cover
|
||
image so authors can credit photos or add a short blurb.
|
||
- **Titled images as `<figure>`:** Markdown images with a title
|
||
(``) are wrapped in a `<figure>` with a `<figcaption>`
|
||
and a small `i` info icon. Images without a title render exactly as before.
|
||
|
||
**Public API**
|
||
|
||
- `has_next` and `has_prev` boolean flags in the paginated
|
||
`/api/volumen/posts` response so clients can navigate pages without
|
||
computing boundaries themselves.
|
||
- Distinct `"draft"` error code in `/api/volumen/posts/:slug` when the post
|
||
exists but is a draft, replacing the generic `"not_found"`.
|
||
|
||
**Admin**
|
||
|
||
- **Modern admin UI:** redesigned layout with a sticky top bar, breadcrumbs
|
||
on every page, and a unified design language across login, post list,
|
||
post form, and settings.
|
||
- **Volumen icon:** a dedicated SVG icon shown in the sidebar and on the
|
||
login page (served at `/admin/icon.svg`).
|
||
- **Version in sidebar footer** so admins always see which release is
|
||
running.
|
||
- **Per-user display name** on the user record and admin header.
|
||
- **Per-user fediverse handle** editable from the settings page (independent
|
||
of the post-level `fediverse_creator`).
|
||
- **Profile photo upload:** each admin user can upload a WebP/AVIF avatar
|
||
that is stored alongside posts in the content directory and shown in the
|
||
settings page.
|
||
- **Tabbed settings page** separating account, profile, and admin sections.
|
||
- **Restricted uploads:** only WebP (`image/webp`) and AVIF (`image/avif`)
|
||
are accepted, with a 415 error message that points users to `cwebp` /
|
||
`avifenc` for conversion. Reflects the engine's WebP/AVIF-only posture
|
||
end-to-end.
|
||
- **Polished native inputs** for `<input type="date|time|file">` so they
|
||
match the rest of the admin's design language.
|
||
|
||
**Tooling**
|
||
|
||
- `just lint` recipe for standalone RuboCop checks and `just fmt` for
|
||
auto-fixing lint issues.
|
||
- `config.toml.example` template for local development; `config.toml` is
|
||
now gitignored.
|
||
|
||
### Changed
|
||
|
||
- Memoize `published_posts` within a single request so repeated calls
|
||
(posts list, tags, feeds, sitemap) reuse the cached result.
|
||
- `Store#all` cache now invalidates on individual file mtime changes, not
|
||
just on the content directory mtime. Manual edits and `git pull` are
|
||
detected without a restart.
|
||
- `Cache-Control: public, max-age=60` header on all public API responses
|
||
for browser and CDN caching.
|
||
- Extract feed and sitemap generation into a dedicated `FeedHelpers` module,
|
||
reducing `Server` from ~500 to ~420 lines.
|
||
|
||
### Fixed
|
||
|
||
- Admin post save/delete now correctly clears the in-memory posts cache, so
|
||
the post list reflects edits without a manual reload.
|
||
- The post form's date field defaults to today when the underlying post has
|
||
no date, preventing the picker from showing an invalid empty value.
|
||
|
||
### Security
|
||
|
||
- **Content-Security-Policy** header on all `/admin/*` responses:
|
||
`default-src 'self'`, `script-src 'self' 'unsafe-inline'`,
|
||
`style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`,
|
||
`font-src 'self'`, `connect-src 'self'`, `form-action 'self'`,
|
||
`frame-ancestors 'none'`.
|
||
- File uploads are rejected (HTTP 413) when exceeding **10 MB**
|
||
(`MAX_UPLOAD_BYTES`).
|
||
- File uploads are restricted to `image/webp` and `image/avif`
|
||
(`ALLOWED_UPLOAD_TYPES`). Anything else is rejected with a 415 and a
|
||
conversion hint.
|
||
|
||
## [0.2.0] — 2026-06-25
|
||
|
||
### Added
|
||
|
||
- `<pubDate>` in RSS feed items, derived from the post date.
|
||
- `<lastmod>` in sitemap entries for SEO.
|
||
- In-memory rate limiting on the login endpoint: 10 attempts per 60 seconds per IP.
|
||
- `<language>` element in the RSS feed channel.
|
||
- Periodic cleanup of stale `login_attempts` entries to prevent unbounded memory growth.
|
||
- Slug format validation (`[a-z0-9._-]`) preventing path traversal and XSS in templates.
|
||
- Username validation in `change_username` matching the create-user regex.
|
||
- Escape-on-output for slug and username in admin template attributes.
|
||
- Memoization of `Store#all` with mtime-based invalidation, eliminating N×read+parse
|
||
per request.
|
||
- Atomic file writes (tmp + rename) for posts and `users.toml`.
|
||
- Post language directory routing: non-default-language posts now land in
|
||
`content_dir/<lang>/` instead of overwriting root-level files.
|
||
- CLI test coverage (version, help, unknown command, option parsing, config bootstrap).
|
||
- Test coverage for RSS feed and sitemap endpoints.
|
||
- CORS preflight (`OPTIONS /api/volumen/*`) so browsers can reach the JSON API.
|
||
- `Users#all` memoization with mtime-based invalidation, matching Store#all.
|
||
- Session invalidation: deleted users can no longer use existing session cookies.
|
||
- CSRF protection on `POST /admin/preview`.
|
||
- Symlink-safe containment in `Store#media_file` via `File.realpath`.
|
||
|
||
### Added
|
||
|
||
- **Scheduled publishing:** a `publish_at` frontmatter field hides posts from the
|
||
public API and feeds until their publication date arrives. The admin form
|
||
includes a Publish at date picker.
|
||
- **JSON Feed:** `GET /api/volumen/feed.json` serves a jsonfeed.org v1.1 feed
|
||
with `content_html` and `date_published` per item.
|
||
- **Fulltext search:** `GET /api/volumen/posts?q=…` filters posts by
|
||
case-insensitive match in title and body.
|
||
|
||
### Changed
|
||
|
||
- Split `lib/volumen/server.rb` into `api_routes.rb` and `admin_routes.rb` modules,
|
||
keeping the server class focused on configuration and shared helpers.
|
||
|
||
### Fixed
|
||
|
||
- `admin.session_ttl` was defined but never read; it now controls
|
||
`Rack::Session::Cookie` expiration via `expire_after`.
|
||
- Derived post excerpts now strip HTML tags before stripping markdown markup,
|
||
avoiding raw HTML leaks in auto-generated excerpts.
|
||
- Draft posts no longer leak through the public detail endpoint (`/api/volumen/posts/:slug`).
|
||
- `Config::DEFAULTS` inner hashes are now frozen and `deep_merge` builds fresh
|
||
copies, preventing mutation from silently poisoning later `Config.load` calls.
|
||
- A warning is emitted when `admin.session_key` is configured but shorter than
|
||
64 bytes, instead of silently falling back to a random secret.
|
||
|
||
## [0.1.0] — 2026-06-20
|
||
|
||
First public release: a small, file-based Markdown blog engine in CRuby. Posts
|
||
are plain files, the engine exposes them as a JSON API and ships a self-hosted
|
||
admin — no database, no external services.
|
||
|
||
### Added
|
||
|
||
**Content & storage**
|
||
|
||
- File-based store: each post is a `.md` file with a TOML frontmatter block
|
||
delimited by `+++`. No database.
|
||
- Post fields: `title`, `slug`, `date`, `lang`, `author`, `tags`, `draft`,
|
||
`excerpt`, `cover`, `all_langs`, and `translations` (a `lang → slug` map).
|
||
- Drafts (`draft = true`) are hidden from the public API.
|
||
- Excerpt falls back to the first paragraph (~200 chars) when none is set.
|
||
- Estimated reading time (~200 words/min) computed per post.
|
||
- Multilingual posts: per-language slugs via `translations`, plus `all_langs`
|
||
to surface a single post under every language.
|
||
|
||
**Markdown rendering**
|
||
|
||
- GFM rendering via kramdown: tables, fenced code blocks, task lists, and
|
||
strikethrough.
|
||
- Automatic heading IDs for in-page anchors.
|
||
- Raw HTML in post bodies is passed through (authors are the trusted admin).
|
||
|
||
**Public JSON API (`/api/volumen`)**
|
||
|
||
- Read-only endpoints: `site`, `posts`, `posts/{slug}`, `tags`, `feed.xml`
|
||
(RSS), and `sitemap.xml`.
|
||
- `posts` supports pagination (`page`, `limit`, returning `total` and
|
||
`page_size`) and filtering by `lang` and `tag`.
|
||
- Permissive CORS so a separate front end can consume the API.
|
||
|
||
**Admin (`/admin`)**
|
||
|
||
- Server-rendered admin with a sidebar layout and full post CRUD.
|
||
- Dual-mode editor: Markdown source (primary) and a visual WYSIWYG, switchable
|
||
per post, with a toggleable live preview persisted across sessions.
|
||
- Image upload from the editor toolbar into `content_dir/media`; any image
|
||
format is accepted (WebP and AVIF included) and served with the correct
|
||
`Content-Type`.
|
||
- Cover image field with upload, URL, clear, and preview.
|
||
|
||
**Multi-user & roles**
|
||
|
||
- Multiple admin users stored in `users.toml`, managed from a settings page.
|
||
- Two roles: `admin` (full access incl. user management) and `author` (posts
|
||
only).
|
||
- Change password and username; last-admin and self-deletion safeguards.
|
||
|
||
**Configuration & CLI**
|
||
|
||
- TOML configuration, auto-generated with a commented template on first run.
|
||
- Configurable `host`/`port` (defaults to `9090`), `content_dir`,
|
||
`users_file`, and `site` metadata.
|
||
- CLI: `serve`, `hash-password`, `version`, `help`, with `--config`,
|
||
`--content`, `--host`, and `--port` overrides.
|
||
|
||
**Deployment**
|
||
|
||
- `install.sh` installs the Ruby toolchain from distro packages on Fedora and
|
||
openEuler (and prints guidance on other distros), creates the service user
|
||
and directories, and installs a systemd unit.
|
||
- Deployment docs for systemd and FreeBSD (`rc.d`), plus an nginx reverse-proxy
|
||
setup that runs the admin same-origin, with brute-force rate-limiting and an
|
||
optional IP allowlist on the login endpoint.
|
||
|
||
**Project & tooling**
|
||
|
||
- Documentation set: architecture, configuration, API reference, deployment,
|
||
and security.
|
||
- `just` task set (`install`, `run`, `dev`, `build`, `test`, `uninstall`) and
|
||
Forgejo Actions CI (RuboCop + tests on Ruby 3.3/3.4, gem build on release).
|
||
- `CONTRIBUTING.md` and a minitest suite; clean RuboCop.
|
||
|
||
### Security
|
||
|
||
- Passwords hashed with scrypt and verified in constant time.
|
||
- Per-form CSRF tokens on every state-changing admin request.
|
||
- Session cookies are `HttpOnly` and `SameSite=Strict`, and gain the `Secure`
|
||
attribute automatically on HTTPS requests (detected via `request.ssl?` /
|
||
`X-Forwarded-Proto`), so the cookie never travels over plain HTTP in
|
||
production.
|
||
- Configurable, stable `session_key` so sessions survive restarts.
|
||
- Generic login error message to avoid username enumeration.
|