-
released this
2026-07-27 08:38:11 +00:00 | 0 commits to main since this releaseFixed
web/templates/*.jinjamissing from distribution wheel — addedweb/templates/*.jinjato[tool.setuptools.package-data]inpyproject.toml. After a fresh install from PyPI the admin panel returned 500 becauseJinja2Templatescould not find any templates.- Duplicate
script-src/style-srcin 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-srcandstyle-srcare 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_nonceis now set inGlobalSecurityHeadersMiddleware.dispatch()beforecall_next(request)so templates see it during rendering. Previously it was generated afterwards, socsp_noncewas an empty string in the template context and every inline script was blocked. - Missing
nonceattribute on<script>inlist.html.jinjaandsettings.html.jinja— inline scripts in these templates now carrynonce="{{ csp_nonce | default('') }}"so they match the nonce in the CSP header. - Bootstrap
admin.password_hashignored whenusers.tomlis empty —Users.allnow falls back to the bootstrap admin even whenusers.tomlexists but is empty (e.g. right aftervolumen init, which creates the file viaPath.touch()). Previously the first login after a clean install failed. admin_contextdid not passusers_exist— the login page showed the “No users configured” warning even when authentication worked. Added"users_exist": users_obj.anyto the admin template context.- Inline
style="..."blocked by strict admin CSP — the admin templates used inlinestyle="..."attributes (and onecard.style.display = ...JS assignment) which the new strictstyle-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 dynamicbackground-image: url(...)on.post-covernow flows through a--cover-urlcustom property (style="--cover-url: …") which CSP3 does not treat as an inline style application. /favicon.icoreturned 404 — browsers auto-request/favicon.icoeven when the page declares an explicit<link rel="icon">. Added a tiny route increate_app()that serves the packagedweb/static/volumen-icon.svgwithContent-Type: image/svg+xmland a 1-dayCache-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_contextdid not include the packageVERSION, so the footer<div class="sidebar-version">v{{ version }}</div>rendered as justvwith no number behind it. Added"version": VERSIONto the context.
Downloads
-
v0.4.1 Stable
released this
2026-07-27 07:03:44 +00:00 | 3 commits to main since this releaseFixed
- Missing CSP nonce for
style-srcon admin routes — addedstyle-src 'self' 'nonce-{nonce}'to the CSP header on/adminroutes. 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 topyproject.tomlsovolumen-icon.svgandvolumen-logo.svgship inside the installed wheel. The admin panel previously returned 500 on/admin/icon.svgand/admin/logo.svg.
Downloads
- Missing CSP nonce for
-
v0.4.0 Stable
released this
2026-07-26 16:19:36 +00:00 | 5 commits to main since this releaseAdded
volumen init— a newinitsubcommand is the operational
bootstrap (config, data dirs, admin password, optional systemd unit). Run
as root for a system install (default--systemd); pass--localfor 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.serviceand runs
systemctl daemon-reload && systemctl enable --now.- PyPI publish metadata —
pyproject.tomlgainsreadme,keywords,
a completeclassifiersarray (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--jsonfor tooling; reportsconfig_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 is1when 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 is0up-to-date,
1update available (suggestsuv tool upgrade volumen),2PyPI
unreachable / offline / malformed response.--jsonreturns the same
fields pluschecked_atfor cron-friendly alerting.src/volumen/installer.py— a new module exposingsystem_install,
user_install,inspect, plus the private_mutate_config_texthelper
used to apply line-anchored substitutions to the rendered TOML template
(preserves comment blocks byte-for-byte).- Tests —
tests/test_installer.pyadds 15 tests covering both install
flows, the config-template mutator, the systemd-unit rendering,
idempotency / backup behaviour, theinspectround-trip, and the
--systemd+--localCLI 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.ymlruns
uv publish --token "$PYPI_TOKEN"afterpython -m buildforv*
tags. Wheel + sdist continue to be uploaded to sourcedock.dev Releases as a
secondary mirror. End users getuv tool install volumenand
pipx install volumenas the one-line install path; no checkout
required. - Distribution path is
uv tool install(orpipx install) — the
CLI binary is installed globally; the operational bootstrap is delegated
tovolumen init. The legacyuv sync+ project-local venv at
/opt/volumen/.venvis gone. Config.loadround-trips config-filecontent_dir/users_file—
the rendered template places these two keys after the[server]header,
so TOML table folding puts them inside[server].Config.loadnow
hoists them to the root so files produced byvolumen 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 librarytomllib(plustomli-wfor
round-tripping edits). The public API (/api/volumen/*) and the
server-rendered admin (/admin/*) keep the same shape and HTTP semantics. - Installer —
install.shnow bootstrapsuvinstead of Bundler, runs
uv sync --frozento provision/opt/volumen/.venv, and points the
systemdExecStartat the venv-residentvolumenconsole script. The
Python runtime is auto-installed on Fedora/openEuler; other distros get a
heredoc with the equivalentapt/pacman/zypper/apklines. - 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 buildand uploads them with the Forgejo Releases API. - Migration guard — new
.forgejo/workflows/migration-guard.ymlgreps
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.tomlgains six new keys surfaced in
config.toml.example:[server].env("production"enables strict
startup),[server].trust_proxy(honourX-Forwarded-Protofrom
nginx),[admin].min_password_length(default10),
[admin].max_password_length(default1024, caps scrypt work),
[admin].max_upload_bytes(default10485760, 10 MB), and
[admin].cookie_secure(defaultfalsein dev, settruebehind
HTTPS). Seedocs/configuration.mdfor the full schema. - Default bind address — the installer and
config.toml.example
default tohost = "::"(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.28withhttpx2>=2.7(the actively maintained fork by
Pydantic). Starlette 1.3.x prefershttpx2inTestClientand emits
StarletteDeprecationWarningwhen only legacyhttpxis installed;
shippinghttpx2keeps the test suite warning-free. The legacy
httpxpackage is still pulled in viafastapi[standard]for the
fastapiCLI and remains API-compatible. The unused
ignore::DeprecationWarning:starlette.testclientfilter is removed
from[tool.pytest.ini_options](it targeted the wrong warning
class and never matched).
Removed
install.sh— the bash installer is deleted. Useuv tool install volumen(orpipx install volumen) followed byvolumen init.pkg/build output andvolumen.gem(Ruby gem packaging is gone; the
project now ships as a wheel + sdist built withpython -m build).Gemfile,volumen.gemspec,Rakefile,.rubocop.ymland the
Sinatra-eralib/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\nline-break idiom), so trimming trailing
whitespace is safe.
Security
- scrypt password hashing now uses
hashlib.scryptand constant-time
verification viasecrets.compare_digest(was: Ruby'sOpenSSL::KDF). - Session cookies are signed by Starlette's
SessionMiddleware
(itsdangerous) withHttpOnlyandSameSite=Strict;Secureis added
bySecureCookieMiddlewarewhen the request was forwarded over HTTPS
(only when[server].trust_proxy = true). - CSRF tokens are generated with
secrets.token_hex(32)and verified
withsecrets.compare_digeston 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 indocs/security.mdso 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. whetherimage/*upload signatures get
re-validated server-side and whether the CSRF token is rotated on login).Downloads
-
v0.3.0 Stable
released this
2026-07-12 12:03:22 +00:00 | 12 commits to main since this releaseAdded
Content & authoring
- Fediverse attribution: a
fediverse_creatorfrontmatter field (e.g.
@user@mastodon.social) plus a site-wide[site].fediverse_creatordefault
inconfig.toml. The value is exposed asfediverse_creatoron the site
and post payloads, rendered as<dc:creator>in the RSS feed, as
authors[].namein 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_captionpost 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 smalliinfo icon. Images without a title render exactly as before.
Public API
has_nextandhas_prevboolean flags in the paginated
/api/volumen/postsresponse so clients can navigate pages without
computing boundaries themselves.- Distinct
"draft"error code in/api/volumen/posts/:slugwhen 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-levelfediverse_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 tocwebp/
avifencfor 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 lintrecipe for standalone RuboCop checks andjust fmtfor
auto-fixing lint issues.config.toml.exampletemplate for local development;config.tomlis
now gitignored.
Changed
- Memoize
published_postswithin a single request so repeated calls
(posts list, tags, feeds, sitemap) reuse the cached result. Store#allcache now invalidates on individual file mtime changes, not
just on the content directory mtime. Manual edits andgit pullare
detected without a restart.Cache-Control: public, max-age=60header on all public API responses
for browser and CDN caching.- Extract feed and sitemap generation into a dedicated
FeedHelpersmodule,
reducingServerfrom ~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/webpandimage/avif
(ALLOWED_UPLOAD_TYPES). Anything else is rejected with a 415 and a
conversion hint.
Downloads
- Fediverse attribution: a
-
v0.2.0 Stable
released this
2026-06-25 20:14:38 +00:00 | 39 commits to main since this releaseAdded
<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_attemptsentries to prevent unbounded memory growth. - Slug format validation (
[a-z0-9._-]) preventing path traversal and XSS in templates. - Username validation in
change_usernamematching the create-user regex. - Escape-on-output for slug and username in admin template attributes.
- Memoization of
Store#allwith 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#allmemoization 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_fileviaFile.realpath.
Added
- Scheduled publishing: a
publish_atfrontmatter 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.jsonserves a jsonfeed.org v1.1 feed
withcontent_htmlanddate_publishedper item. - Fulltext search:
GET /api/volumen/posts?q=…filters posts by
case-insensitive match in title and body.
Changed
- Split
lib/volumen/server.rbintoapi_routes.rbandadmin_routes.rbmodules,
keeping the server class focused on configuration and shared helpers.
Fixed
admin.session_ttlwas defined but never read; it now controls
Rack::Session::Cookieexpiration viaexpire_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::DEFAULTSinner hashes are now frozen anddeep_mergebuilds fresh
copies, preventing mutation from silently poisoning laterConfig.loadcalls.- A warning is emitted when
admin.session_keyis configured but shorter than
64 bytes, instead of silently falling back to a random secret.
Downloads
-
v0.1.0 Stable
released this
2026-06-21 11:15:06 +00:00 | 62 commits to main since this releaseFirst 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
.mdfile with a TOML frontmatter block
delimited by+++. No database. - Post fields:
title,slug,date,lang,author,tags,draft,
excerpt,cover,all_langs, andtranslations(alang → slugmap). - 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, plusall_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), andsitemap.xml. postssupports pagination (page,limit, returningtotaland
page_size) and filtering bylangandtag.- 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) andauthor(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 to9090),content_dir,
users_file, andsitemetadata. - CLI:
serve,hash-password,version,help, with--config,
--content,--host, and--portoverrides.
Deployment
install.shinstalls 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. justtask set (install,run,dev,build,test,uninstall) and
Forgejo Actions CI (RuboCop + tests on Ruby 3.3/3.4, gem build on release).CONTRIBUTING.mdand 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
HttpOnlyandSameSite=Strict, and gain theSecure
attribute automatically on HTTPS requests (detected viarequest.ssl?/
X-Forwarded-Proto), so the cookie never travels over plain HTTP in
production. - Configurable, stable
session_keyso sessions survive restarts. - Generic login error message to avoid username enumeration.
Downloads
- File-based store: each post is a