The version that makes plugin-first real. Until now every extensible interface had exactly one
implementation compiled into the core. v0.3 adds an out-of-process plugin loader: a third party
registers ADDITIONAL implementations - an OIDC login, a scoring curve, a Discord notifier, a new
challenge type - as standalone executables the core discovers, launches, supervises, and calls
over a versioned gRPC ABI. A plugin author edits nothing in core and opens no PR against it.
A no-plugin deployment behaves exactly as v0.2 did. Plugins are strictly additive, the migrations
are additive, and /api/v0 keeps answering.
API v1 and API tokens
- /api/v1 is the canonical, semver-governed surface. /api/v0 remains as a deprecated alias
serving the identical handlers - there is no second handler set that could drift - and carries
Deprecation/Sunset headers. - Scoped API tokens for clients that are not a browser. The plaintext is shown once and only its
hash is stored; scope is intersected with the owner's role, so an admin scope on a non-admin
account grants nothing. - Every operation the dashboard performs is reachable with a token and NO session cookie, driven
end to end and asserted request-by-request.
The plugin system
- A loader with discovery, strict manifest parsing, an eight-state supervisor with restart
backoff and quarantine, a two-level in-flight budget, and a cancel-then-kill drain. Boot is
asynchronous: the core serves whether or not plugins come up. - A versioned ABI (1.1). A major mismatch is refused at the handshake; minor is forward-
compatible. An Info cross-check at ready quarantines a mispackaged manifest/binary pair at load
rather than surfacing it mid-event. - Four plugin types wired into real requests: challenge-type (verdict computed before the
transaction; deleted or swapped challenges fail closed inside the row lock), scoring (locked at
solve and recorded per solve, so the scoreboard stays recomputable with every plugin down),
notification (a non-blocking bus with bounded queues where every drop is counted), and auth. - POST /admin/plugins/{name}/reload hot-reloads one plugin. A failed reload is not destructive:
the old instance is retained and keeps serving.
External login, and what an auth plugin is allowed to mean
The core owns the CSRF state: it mints it, hands it to the provider, and refuses to start a login
whose authorize URL does not carry it verbatim. The state is single-use and bound to an HttpOnly
cookie, so a captured callback can neither be replayed nor replayed into someone else's browser.
An auth plugin asserts an identity; the core decides what it means, and enforces that field by
field. Provisioning always creates the lowest role. A login attaches to an existing account only
through a binding the core minted, or an email the provider says it VERIFIED. Claims carrying
role/admin/user_id are rejected outright rather than ignored. A malformed claim fails closed.
Installing an auth plugin remains a trust decision on the level of replacing the core binary: it
sits inside the authentication trust boundary, and one with the password capability receives the
plaintext credentials your users type. The checks bound the blast radius; they do not contain a
hostile plugin.
The author kit
plugin/sdk, the shared plugin/abi, and plugin/sdk/contract - VerifyScoring, VerifyNotification,
VerifyChallengeType, VerifyAuth - which boot a built plugin through the real loader and assert the
contract. Five reference plugins ship under plugins/, each its own module: a template, first-blood
(scoring), webhook (notification), regex-flag (challenge type), and oidc (auth).
The exit gate - build a plugin from a clean checkout, against the published SDK, with no replace
and no core source on disk - passes for all five.
Upgrade
No action required for a deployment running no plugins; the migrations are additive and existing
rows are untouched. All new configuration is documented in .env.example and every option has a
working default. If you enable plugins, mount the plugins directory READ-ONLY: a writable one
lets a compromised core drop a binary for the next boot to launch as the platform, and the loader
now warns about exactly that.
There are no API stability promises before v1.0. See CHANGELOG.md for the full entry.