No description
  • Ada 62.3%
  • HTML 14.3%
  • Go 13.8%
  • C 2.7%
  • Handlebars 2.6%
  • Other 4.3%
Find a file
LowEel efcb19d9e3
All checks were successful
continuous-integration/drone/push Build is passing
Serve the dashboard's scripts as files, not as inline code
The statistics dashboard was the last page allowed to run inline
script, and it was allowed because GoAccess ships as one 681KB file
with its libraries written into the page.  Nothing was ever fetched
from anywhere: that code is vendored here and copied into the image.
It still is -- it is served from /static/admin/ now instead of from
inside the page, which is the same bytes from the same origin, and
script-src 'self' forbids any other.

Seven blocks were executable; thirteen more are templates, which a
browser does not run and a policy does not stop, so they stay where
they are.  Six of the seven are constant -- d3, topojson, hogan, the
GoAccess application and its charts, the translations -- and each is
now a file, verified byte-identical to the block it came from.  The
seventh is the pair of values the page is handed per request: those
are inert <script type="application/json"> blocks now, read back by a
bootstrap of ours.  Data, not code.

So the dashboard keeps one allowance instead of two.  It still needs
'unsafe-eval', because that is how it compiles its table templates; it
no longer needs 'unsafe-inline', and no page of this blog does.

The theme moves into that bootstrap.  It used to be substituted into
the vendored shell by matching "var html_prefs={};var user_interface="
from Ada, which stopped matching the moment those two lines became a
file -- and would have silently returned the dark dashboard on a white
page, which was a bug fixed only this morning.  The bootstrap is ours,
so the preference is simply written in it, and one piece of string
surgery leaves the Ada side.

That marker appears twice in the shell, and splitting on the first
occurrence quietly dropped 107KB of world-map data.  The byte counts
caught it; the tests would not have.

Incidentally the browser can now cache 489KB that were re-sent with
every opening of the page.

One test asked the local clock for "today" while the series is filed by
UTC day, so the suite failed for the two hours a night between the two
midnights.  It asks UTC now, as the panel's own label says it should.
2026-09-21 00:11:46 +02:00
alire Replace AWS with libmicrohttpd and libcurl 2026-09-17 12:22:21 +02:00
smoke Serve the dashboard's scripts as files, not as inline code 2026-09-21 00:11:46 +02:00
src Serve the dashboard's scripts as files, not as inline code 2026-09-21 00:11:46 +02:00
static Serve the dashboard's scripts as files, not as inline code 2026-09-21 00:11:46 +02:00
tools Serve a file without putting it on the stack, and read the markdown under a <br> 2026-09-18 23:37:31 +02:00
.dockerignore Remove ActivityPub and add Docker runtime assets 2026-08-18 22:31:38 +02:00
.drone.yml Take the smoke tests out of the image build 2026-09-17 14:07:20 +02:00
.gitignore Write the archive's permissions into the archive 2026-09-18 22:31:44 +02:00
alire.toml Replace AWS with libmicrohttpd and libcurl 2026-09-17 12:22:21 +02:00
DOCKER_INSTALL.md Add a logout, stop the password from breaking the boot, match the reader type 2026-09-17 22:19:33 +02:00
Dockerfile Shrink stored images at the one door they all pass 2026-09-19 22:53:28 +02:00
INSTALL.md Centralize browser auth and reader workflow 2026-08-19 12:13:34 +02:00
README.md Centralize browser auth and reader workflow 2026-08-19 12:13:34 +02:00
wortwerk.gpr Give the request tasks a stack that fits an image 2026-09-19 22:53:14 +02:00
wortwerk_spark.gpr Clean imported feed markup at the door 2026-09-20 21:20:02 +02:00

Wortwerk

Wortwerk is a single-user blog engine written in Ada, with SPARK used for every pure decision layer that can be proved.

The target is a WriteFreely-like publishing experience with a smaller and more explicit implementation: local files only, Hugo-compatible Markdown layout, deterministic asset storage, and isolated adapters for HTTP, email, zip handling, themes, newsletter, and RSS.

Status

Early project skeleton.

Current baseline:

  • Alire crate initialized.
  • GNAT 16.1.0 and GPRbuild 26 configured through Alire.
  • GNATprove 16.1.0 and SPARKlib 16.1.0 added.
  • Minimal verifiable echo command added: wortwerk echo <text>.
  • AWS-based HTTP layer added: wortwerk serve.
  • Initial storage/path/slug helpers created.
  • Content and private keystore state use separate filesystem roots.
  • Authenticated post lifecycle APIs write Hugo page bundles: content/posts/<date>/<normalized_title>/index.md; pinned posts are moved to the Hugo-style content/pinned/<date>/<normalized_title>/index.md section.
  • Post image upload stores md5(content).extension in the post bundle and returns Toast-compatible JSON.
  • alr build passes.
  • gnatprove --mode=all passes for the SPARK proof project.

Runtime Roots

Use WORTWERK_CONTENT_ROOT for Hugo-compatible blog files and assets. Use WORTWERK_KEYSTORE_ROOT for private state such as the credential wallet.

If those are not set, WORTWERK_DATA_ROOT is used only as a base and expands to WORTWERK_DATA_ROOT/blog and WORTWERK_DATA_ROOT/keystore.

Use WORTWERK_LISTEN_ADDR for the HTTP bind endpoint, for example 0.0.0.0:8080. The older WORTWERK_LISTEN_HOST and WORTWERK_LISTEN_PORT variables remain as compatibility fallback.

Public Routes

/ is the public blog route. /login is the explicit single-user login entrypoint for the future administration/editor UI. /admin is the authenticated administration shell. /newpost is the editor entrypoint for creating a new draft post.

Application static assets are served from WORTWERK_APP_STATIC_ROOT, defaulting to static. Editor assets live under static/toast. Administration panel assets live under static/admin. They are separate roots so the HTTP adapter and future asset serving rules can distinguish editor UI assets from administration UI assets without path ambiguity.

The default public theme is Ghost Journal, vendored under static/themes/journal. The Docker image copies the whole static/ tree to /usr/share/wortwerk/static, so the default theme is available without runtime downloads.

Static files are always served with no-cache revalidation semantics. Textual static files are served with gzip when the client advertises gzip support; binary formats such as images, fonts, and zip files are left uncompressed.

Administration and editor assets are not public CDN assets: /static/admin/... and /static/toast/... are served only after OAuth login. The keystore secret is never an HTTP authentication token; it is only key material for opening private keystore state.

The /newpost editor uses Toast UI from static/toast and calls Wortwerk APIs: /api/posts/create, /api/posts/save, /api/posts/publish, and /api/images. It sends raw text/markdown and raw image bodies matching the current API contract.

Admin APIs

GET /api/posts/export returns a Hugo-style content zip. POST /api/posts/import accepts a raw zip body with Content-Type: application/zip and imports it into the content root. POST /api/posts/create?date=YYYY-MM-DD&title=Title creates a draft post from a raw text/markdown body up to 50,000 bytes. POST /api/posts/save?slug=YYYY-MM-DD/title saves a draft from a raw text/markdown body up to 50,000 bytes. POST /api/posts/draft?slug=YYYY-MM-DD/title keeps a post as draft from a raw text/markdown body up to 50,000 bytes. POST /api/posts/publish?slug=YYYY-MM-DD/title saves a published post from a raw text/markdown body up to 50,000 bytes. DELETE /api/posts/delete?slug=YYYY-MM-DD/title deletes the post bundle. POST /api/images?slug=YYYY-MM-DD/title and POST /api/posts/assets/upload?slug=YYYY-MM-DD/title accept a raw image body, with Content-Type: image/png, image/jpeg, image/gif, or image/webp; they store it as md5(content).extension and return {"data":{"url":...}}. POST /api/posts/distribute?slug=YYYY-MM-DD/title&go=true queues newsletter distribution for the post. The actual SMTP sender adapter is still separate runtime work; this endpoint already has the authenticated contract and queue wrapper.

These endpoints require the OAuth session cookie issued by POST /login. WORTWERK_KEYSTORE_SECRET is not accepted on HTTP requests and must never be compared with request data.

Feeds

Atom 1.0 is exposed publicly at /feed.atom; /rss serves the same feed for compatibility with older reader links.

License

Wortwerk is licensed under the European Union Public Licence, version 1.2 (EUPL-1.2).

Credits

Wortwerk vendors TOAST UI Editor under static/toast for the Markdown editor. TOAST UI Editor is licensed under the MIT License; the bundled license text is in static/toast/Toast-UI-MIT.txt.

Wortwerk vendors Ghost Journal under static/themes/journal as the default theme. Journal is copyright Ghost Foundation and released under the MIT License; the bundled license text is in static/themes/journal/LICENSE.