- Ada 62.3%
- HTML 14.3%
- Go 13.8%
- C 2.7%
- Handlebars 2.6%
- Other 4.3%
|
All checks were successful
continuous-integration/drone/push Build is passing
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.
|
||
|---|---|---|
| alire | ||
| smoke | ||
| src | ||
| static | ||
| tools | ||
| .dockerignore | ||
| .drone.yml | ||
| .gitignore | ||
| alire.toml | ||
| DOCKER_INSTALL.md | ||
| Dockerfile | ||
| INSTALL.md | ||
| README.md | ||
| wortwerk.gpr | ||
| wortwerk_spark.gpr | ||
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-stylecontent/pinned/<date>/<normalized_title>/index.mdsection. - Post image upload stores
md5(content).extensionin the post bundle and returns Toast-compatible JSON. alr buildpasses.gnatprove --mode=allpasses 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.