Six keys, and the parser's first list, bool and word-valued ones:
registration, registrations_per_hour, info_replies_per_hour, admins,
admin_notify, register_hosts.
Lists drop empty items, because `admins =` is exactly what the Nix module
renders for an unset option, and addresses are validated at load rather than
at first use — a typo would otherwise pass the start and then silently never
match an admin command. Validated values are normalised through
Address::short(), so a spelling carrying the default port collapses to the one
the daemon will compare against.
registration defaults to silent, so adding these keys changes nothing for
anyone who writes in.
Verified the generator and the parser agree by feeding the module's rendered
output to the binary, in both the fully-set and all-defaults shapes — which
matters because config.rs hard-errors on an unknown key.
The bucur half waits for the lock: that host pins this flake at the rev before
these options existed, so setting them there now would break its eval. It
lands with the deploy.
Mode is persisted rather than read from config alone, so an admin can flip
registration by mail instead of needing a redeploy; config supplies only the
initial value. It defaults to Silent, which is today's behaviour byte for
byte, so landing this cannot start mailing strangers on its own.
Limits holds the two daemon-wide hourly counters. Identity keys are free to
mint, so a per-key budget does not bind an attacker and these are what
actually do; each vector is pruned to the hour on read and so is bounded by
its own cap.
Also fixes reply_allowed's ledger growth, which stops being a wart here and
becomes a vulnerability: until now only accepted authors reached it, but once
a stranger can draw a reply any throwaway keypair appends a permanent row.
Ledgers idle for an hour are dropped and the count is capped, with a
regression test that drives it past the cap.
drained carries the one-time backlog guard: a live mail.db holds every
requests-tier mail ever received, and the first poll after upgrading must not
mail all of them.
A remote author's feed needs a namespaced path or two servers' `alice` collide
on one file. valid_slug accepts `local`, `local@host` and `local@host:port`;
local authors keep their bare path, so the published feed keeps its URL and
nothing migrates.
valid_username is untouched and is now only the nick rule: a nick reaches
`# nick =` metadata that other twtxt clients parse, so widening it with the
path rule would have leaked `@` and `:` in there. A test pins the two apart.
The traversal guard changes character here. A path could not contain a dot
before, which is why `/twtxt/../etc/passwd` 404'd for free; dots are legal now,
so the rejections are explicit and path() also asserts the join stayed inside
the feed directory. username_of rejects `/` before decoding and decodes only a
literal `%40`, since some twtxt clients encode the `@`.
Incidentally fixes a legal smolmail user never being able to get a feed: the
old rule banned `.` and `_`, so `admit alice.b@smol.place` failed at create.
dispatch and apply_commands returned English prose that became the reply
subject, so the pure layers owned author-facing copy. They now return
reason::Reason and the mail-building edge resolves it through the catalogue.
core::rules needed no change: RuleError was already a typed enum, and its
Display stays for author-side tools. The daemon maps the variants itself
rather than borrowing that prose.
Error-path tests now assert on variants, so rewording main.ftl cannot break
them, and PruneModeUnknown carries the value the author actually sent.
Reason::every_variant_resolves_to_copy walks the enum, so a variant added
without a catalogue entry fails in tests rather than mailing someone
"???reason-…???".
Ports carrier's src/i18n.rs and its locales/en-US/main.ftl layout, same
fluent-bundle and unic-langid versions, so the two catalogues stay portable
between them. Only mail the daemon sends an author moves; operator CLI output
stays inline, since the operator runs the binary and reads English.
The error-reply reasons from dispatch and ApplyError are deliberately left
behind: dispatch is pure by contract, so those become reason identifiers
resolved at the mail-building edge in a separate change rather than having the
pure layer reach for a bundle.
Adds a test carrier lacks. FluentResource::try_new and add_resource both panic
on a malformed or duplicate-id catalogue, and nothing touches the bundle until
the daemon first mails someone, so without it a bad catalogue surfaces inside
poll() on a live instance.
Sentences are verbatim; the bodies are now hard-wrapped where they were single
long lines, which reads better in a plain-text client.
packages.static is a static musl binary so a low-power deploy target can
fetch a prebuilt one rather than compile Rust; apps.release-static publishes
it to a Forgejo release, guarded on HEAD carrying the version's tag.
fumi-core is a path dependency on ../fumi, which escapes the flake source, so
fumi is vendored as its own input and re-laid beside the source tree in the
store. Over https, not a local path: a flake importing this one has to resolve
this input too when it locks.
services.smolboid runs as a real user rather than DynamicUser, because admit,
block and the fumi bootstrap are operator commands against the same master and
store and need a stable uid to sudo into.