Releases: eas4ai/suprnova
Release list
Suprnova v1.3.1
Fixed
- Provider-backed applications can reset verified users again. When no Magnetar engine is installed,
PasswordResetuses an explicitly reset-capableUserProviderand frameworkauth_flow_tokensfor already verified accounts.EloquentUserProvider<M>opts in whenMimplementsMustVerifyEmail + CanResetPassword; noapp_usersmigration is required. - The published framework line now contains both post-release repair sets. The translated 1.3.0 changelog layout and headings, CJK wrapping, localized anchors, glossary terms, and prose punctuation are reconciled instead of split across divergent local and remote branches.
- Post-tag CLI and Magnetar hardening is included. Development-process cleanup uses the completed process-group fallback, and the local qualification contracts cover the released refs and plugin-SDK SQLite lanes.
Security
- The provider fallback never treats password reset as first mailbox proof. Unknown and unverified addresses receive the same no-mail response. Install Magnetar when an unverified account must prove mailbox ownership through reset so credential cleanup, auth-epoch advancement, and revocation remain atomic. Provider fallback completion reports framework session and remember revocation failures through
PasswordResetOutcome.
Upgrading
- Move every
v1.3.0Git dependency tov1.3.1. Applications with their ownuserstable keep their configuredUserProvider; they do not initialize the defaultapp_usersengine merely to reset an already verified account. Applications that use Magnetar credentials or unverified-account first proof continue to initialize Magnetar.
Suprnova v1.2.4
Security
-
The maintenance-mode bypass secret is compared in constant time.
MaintenanceMiddlewarematched the secret URL with a plain string
compare, which returns at the first differing byte. Because the secret is
a bearer credential carried in the request path, that timing difference
told an attacker how long a prefix they had guessed correctly. The
compare now runs over the full byte length viasubtle::ConstantTimeEq,
short-circuiting only on a length mismatch - the same shape as the
bypass-cookie compare next to it. -
rules::Urlnow rejects script URIs. The rule accepted any scheme
url::Urlcould parse,javascript:andvbscript:included, so a
validated URL could still be a script-execution sink when rendered into
anhref. It now applies Laravel'surlrule shape
(Illuminate\Support\Str::isUrl's^(PROTOCOLS)://HOSTpattern): the
scheme must be on Laravel's allowlist, be followed by://, and be
followed by a non-empty host - Laravel's host group has no?, so an
absent or empty host never matches even with a listed scheme. The scheme
list and the://-plus-host requirement are Laravel's verbatim; the host
itself is parsed by theurlcrate rather than Laravel's regex, so a few
edge cases still differ - an out-of-range port is rejected here and
accepted there, and IDN hosts normalise differently. New
Url::protocols(&[...])mirrors Laravel'surl:http,https;HttpUrl
is now literal sugar for it and keeps its own message. Behaviour
change: a URL with an unlisted scheme that used to validate now
fails - name the scheme withUrl::protocols(&["myapp"])if you meant
to accept it. Two more behaviour changes:mailto:,data:, and
tel:are on Laravel's allowlist by name but don't carry an authority
component, so they now fail; andfile:///etc/passwd-style paths -
scheme://with nothing between the last two slashes - now fail too,
since an empty string isn't a host either. Both follow from Laravel's
own://-plus-host rule. -
Inertia responses now advertise
Vary: X-Inertiaeverywhere. The
header was set only on the page-object responses themselves. Redirects,
404s, 422s, and static responses carried none, so a shared cache keyed on
the URL alone could serve the JSON page object to a hard browser
navigation, or the HTML shell to an Inertia XHR. The new
InertiaHeadersMiddleware- registered byInertia::installas the
outermost of the three - sets it on every response, and turns an empty
200on an Inertia visit into a303back rather than a response the
client rejects as non-Inertia.InertiaVersionMiddlewarenow re-flashes
the session before its409, so a flashed error survives the client's
follow-up full-page GET. -
Three Inertia response fixes.
InertiaResponse::location_for(&req, url)
returns409+X-Inertia-Locationfor an Inertia XHR and a plain302Locationfor a hard navigation, so an OAuth or SSO bounce entered
outside the SPA no longer dead-ends on a body-less409. The existing
location(url)keeps its always-409shape. NewApp::clear_history()
flashes the history-clear flag into the session so it survives the logout
redirect and lands on the page that actually renders - the per-response
.clear_history()marked only the redirect the browser throws away,
leaving the previous session's encrypted history decryptable. And a
onceprop is now skipped only on a full Inertia visit: an explicit
router.reload({ only: ['stats'] })re-resolves it instead of returning
nothing.
-
The SES transport now sends custom message headers.
Mail::to(..) .header("List-Unsubscribe", ...)andMailable::headers()were dropped
silently underMAIL_DRIVER=ses: theContent.Simplerequest body had no
Headersfield and the raw-MIME builder never readOutgoingMessage:: headers, even though every other transport forwards them. Both SES paths
now carry them -Headersas SES v2's{Name, Value}list, raw MIME as
real header lines - so unsubscribe links, threading headers and routing
hints survive a driver swap. Header names are validated up front on both
paths - CR, LF and NUL (the injection bytes, as the Mailgun transport
already refuses) and anything that is not a valid RFC 5322 field name
(spaces, colons, non-ASCII) - so attaching a file never changes whether a
message is accepted.
Fixed
-
Nested validation failures now reach the 422 body.
#[validate(nested)]
failures on a nested struct or on an element of a validatedVec<T>were
dropped between the validator and the response: the request was correctly
rejected with 422, but theerrorsmap came back empty, so no message
rendered and the client could not tell which field was at fault. Nested
failures are now flattened into Laravel's dotted notation -
address.street,items.1.name,order.items.2.sku- alongside the
top-level ones. -
The Inertia page object's
urlkeeps the query string.page.urlwas
the request path only, so the client recorded/usersfor a visit to
/users?page=2&sort=name. Every back/forward navigation and every
router.reload()then replayed the page without its pagination cursor,
sort, or filters. It is now path plus query - the same derivation
InertiaVersionMiddlewarealready used forX-Inertia-Location, so by
default the two agree byte for byte. New
InertiaConfig::url_resolver(...)overrides how the page object names
the page (Laravel'sInertia::resolveUrlUsing); the version bounce keeps
naming the URL that arrived, because that is the URL the browser has to
fetch. -
Inertia::installnow applies its config to every response. The
config handed toInertia::installwas read for three fields and then
dropped, so everyInertiaResponsebuilt without an explicit
.with_config(...)rendered fromInertiaConfig::default(). An app
scaffolded with--frontend reactserved the Svelte entry point and no
React refresh preamble unlessSUPRNOVA_FRONTENDwas set in the
environment; SSR enabled on the config never reached a response; and the
page object's asset version came from a different config than the
version middleware's resolver. The installed config is now retained on
the container's Inertia registry and is whatInertiaResponse::new
starts from. Per-response.with_config(...)still overrides, apps that
never callInertia::installare unchanged, and a failed (fail-closed)
install retains nothing. As a side effect the production Vite manifest
is now parsed once per process rather than once per response. -
Scaffolded apps now install the Inertia protocol middlewares. The
bootstrap.rswritten bysuprnova newregistered the session, locale,
CSRF and include middlewares but never calledInertia::install, so a
generated app had neitherInertiaVersionMiddlewarenor
Inertia303Middleware: a browser still running the previous bundle was
never told to reload after a deploy, and aPUT/PATCH/DELETEthat
redirected stayed on a302the client could follow with the original
verb. The call now lands afterSessionMiddleware- where the version
middleware's session re-flash works - with a namedINERTIA_VERSION
constant to bump when assets change, and it pins the frontend the
project was generated with (.frontend(Frontend::React)for
--frontend react), so the HTML shell loads that framework's Vite entry
point instead of falling back to Svelte's. The generated.envnow sets
SUPRNOVA_FRONTENDto match. The--apistarter is unchanged; it has
no frontend. -
Queue::push_uniqueno longer reports a queued job as skipped. The
return value was computed withmatches!(outcome, Idempotent::Fresh(())),
which foldedIdempotent::FreshUnfencedintofalse- the outcome where
the envelope was pushed but the dedupe lease was lost mid-push. Callers
branching on that boolean were told a job that was about to run had been
suppressed as a duplicate. All three outcomes are now matched exhaustively:
a lost lease returnstruewith awarnnaming the job and its unique
key, and only a real duplicate returnsfalse.push_unique_laterand
later_uniqueshare the path and are fixed with it.
Changed
- Parity baseline moved to Laravel 13.25.0. The 13.23.0, 13.24.0 and
13.25.0 release notes were traced item by item to the framework's own
surface. Everything that reached a Suprnova code path is either fixed in
this release or has a row inmanual/parity.mdmarked
not yetorby design no.
Upgrading
Two changes can alter a running app without any code change on your side.
-
Settings on the config you pass to
Inertia::installnow take effect.
They were read for three fields and dropped. If your install config sets
.ssr(...), SSR is now on: start the worker (suprnova ssr:start) before
deploying, or drop the.ssr(...)call..entry_point,
.assets_base_url,.default_titleand.encrypt_history(...)set there
also reach the page now. -
rules::Urlrejects more. Values that used to pass and no longer do:
any scheme outside Laravel's allowlist,javascript:andvbscript:
among them;mailto:,data:andtel:, which are on the allowlist but
carry no://host; andscheme://with an empty host, such as
file:///path. If you meant to accept a scheme, name it:
Url::protocols(&["myapp"]).
Suprnova v1.2.3
Fixed
- Datetime casts now read database-native
CURRENT_TIMESTAMPtext.
AsDateTime,AsImmutableDateTime, andAsOptionalDateTimecontinue to
write canonical RFC-3339, while reads also accept PostgreSQL's
timezone-bearing text and timezone-free SQLite/MySQL text. Timezone-free
values are interpreted as UTC, matching the framework's UTC timestamp
contract.
Suprnova v1.2.2
Fixed
- Correctly writes nullable non-text values through PostgreSQL attribute-based update, upsert, model-less table, and pivot APIs.
- Rejects malformed multi-row upserts with inconsistent column sets.
- Binds automatic pivot timestamps as typed UTC datetimes.
Security
- Strengthens release-gate proof for dormant lockfile dependencies and records the time-bounded RustSec exception described in the changelog.
v1.2.1
GitHub account migration: every repository URL (manual in all seven languages, README, Cargo repository fields, git dependency sources, scaffold templates) now points at github.com/eas4ai instead of the renamed github.com/entrepeneur4lyf. Scaffolded projects also carry a monitored author email. No behavior changes.
Suprnova v1.2.0
Added
-
The manual ships in seven languages.
manual/es/,manual/fr/,
manual/de/,manual/pt-BR/,manual/ja/andmanual/zh-Hans/each
carry the full 104-chapter manual - every chapter, the table of
contents, and this changelog - translated from the English source.
English remains canonical: chapter structure, code blocks, identifiers,
CLI commands and environment variables are held byte-identical to the
source, so a translated chapter can never disagree with the English
about what the framework does, only say it in the reader's language.The translations were produced and reviewed for suprnova.app, which
renders this manual as its/docs. Every section carries a review
ledger there: verdicts are recorded against content hashes of both the
English and the translation, two independent reviewers must pass the
exact bytes for a section to count as approved, and per-locale
glossaries pin the terminology rulings (which terms stay English,
which take the native word, and why). Corrections are welcome in
either repo - a fix here reaches the site on its next sync.
Suprnova v1.1.0
Added
-
Per-locale fallback chains.
LocalizationConfiggainsparents
(APP_LOCALE_PARENTS, comma-separatedchild=parentpairs, or the
chainable.parent(child, parent)builder): a locale can inherit from a
configured sibling before falling further back to the global
fallback_locale—pt-PTfrompt-BR,en-AUfromen-GB, and so
on, transitively.Lang::get/try_get/get_with/try_get_with/has
all walk the chain, current locale first, so this works for any
Translatordriver, not just the bundled one. A malformed pair, an
invalid locale, a child named twice, or a cycle (including a locale
naming itself as its own parent) fails loudly at config load rather
than degrading at request time.Served catalogs stay chain-flattened ahead of time:
FluentTranslator
now builds each locale's/_suprnova/lang/<locale>.ftlcatalog as a
fold — the embedded framework catalog at the bottom foren/en-*
locales, then the locale's configured parent chain, then its own
*.ftlfiles — so a chained locale is still one self-contained file
the browser fetches once, with no client-side chain awareness needed.
Flattening covers configured parents only; the terminal
fallback_localeis still aLang-facade-level fallback, not baked
into the served bytes.This makes delta-style catalogs practical: a
lang/pt-PT/directory
can hold only the handful of strings that actually differ from
lang/pt-BR/, rather than a full duplicate catalog. The merge that
makes it possible works at the Fluent AST level — a child's value
replaces the parent's, attributes merge by name (an override that
doesn't mention an attribute no longer loses it), select expressions
replace whole (CLDR plural categories are locale-dependent, so
variant-by-variant merging isn't coherent), and child-only entries
append. Seemanual/localization.md's new "Fallback chains" section
for the full contract.
Changed
LocalizationConfiggained theparentsfield.from_env()and
the builder are unaffected; a literal struct constructor (tests
building aLocalizationConfigby hand) needs one more field.- Served catalog text is now serializer-normalized for every locale,
and intra-locale multi-file merging (several.ftlfiles in one
locale directory) now goes through the same AST-level merge as parent
chains rather than simple bundle-overriding. Resolved translations are
unchanged except for the two strict improvements below; the
underlying bytes rotate regardless —ETag/?v=<hash>rotates once
on upgrade. The improvements: an override no longer silently drops
the attributes it doesn't mention, and an attributes-only override no
longer strips the message's own value (previously an error or a
fallback resolution; it now resolves to the earlier override's
value).
Suprnova v1.0.0
Added
-
Localization. Message catalogs in
lang/<locale>/*.ftl
(Fluent), aLangfacade with the
__!("key", name: value)macro, per-request locale detection
(LocaleMiddleware: session → cookie →Accept-Language→
APP_LOCALE), and locale-aware formatting for numbers, currency,
dates, times, lists, and relative times over ICU4X.manual/localization.md
is the chapter.The built-in validation rules stop hardcoding English. Each returns a
keyed message (validation-minplus its arguments and an English
fallback), translated once at the serialization boundary — so a Spanish
app gets Spanish validation errors by dropping in
lang/es/validation.ftl, with no rule wrapping and no forked copy of
the framework's messages. Field names humanize through afield-<name>
lookup.Rule::passes(andContextualRule/AsyncRule) now return
Result<(), ValidationMessage>; a custom rule'sErr("…".into())body
still compiles and still renders verbatim, but the signature in your
implneeds the new type.The browser gets the same bytes the server resolved: the merged catalog
is served at/_suprnova/lang/<locale>.ftlwith an ETag and an
immutable?v=<hash>form, the three starter kits parse it with
@fluent/bundle, andsuprnova generate-typesemits aMessageKey
union so renaming a message points the TypeScript compiler at every
call site.Fluent rather than Laravel-style PHP arrays because one format has to
serve both the server and the browser, and because CLDR plural
categories are what gets Russian, Polish, and Arabic right —
trans_choice's integer ranges cannot, which is why there is no
trans_choicehere. Behind a default-onlocalizationfeature;
--no-default-featuresstill compiles and still validates, using the
embedded English fallbacks. -
IntoInertiaScrollforPaginator. The trait was implemented for
LengthAwarePaginatorandCursorPaginatorbut not for the simple
paginator, sosimple_paginateresults could not feed
Inertia::paginateat all — despitesimple.rs's own module docs
pointing at it as the URL-generation path. That left offset-paginated
Inertia collections with a choice between aCOUNT(*)per request and
hand-rolling the scroll metadata.next_pagecomes from the
LIMIT n+1overflow probe rather than a computed last page, there
being no total to compute one from.
Fixed
-
suprnova generate-typesemitted a different file on every run.
The topological sort seeded its work queue by iterating aHashMap,
and Rust randomises hash iteration order per process, so consecutive
runs ordered the same interfaces differently. The output is a
checked-in artifact, so every run produced a diff — and a generated
file that churns for no reason is one people stop regenerating, after
which it quietly stops describing the Rust it claims to. The directory
walk is sorted too, so the output no longer depends on filesystem
order either. Two runs of the same source are now byte-identical. -
topological_sortdid the opposite of its doc comment, emitting
dependents before dependencies. Harmless — a TypeScript interface may
reference one declared later in the same file — so the comment is
corrected rather than the order, which would have reshuffled a tracked
file for no benefit.
Suprnova v0.9.1
Three defects, all found by running the dogfood app under a containerised
harness rather than by reading the code. Every one of them is invisible to
a test suite that never stops a process the way production stops it.
They compound in a specific order: a rolling deploy SIGKILLs a worker
mid-job (the first), and that job then takes a reclaim path that never
counted the attempt (the second).
Fixed
-
schedule:work,queue:workandworkflow:workignored SIGTERM.
Each selected ontokio::signal::ctrl_c()alone, which installs a
SIGINT handler — so SIGTERM had no handler anywhere in the process, and
SIGTERM is whatdocker stop, Coolify, systemd and Kubernetes send. All
three already had a careful bounded drain behind thatselect!; none of
it had ever executed under a supervisor. Measured before the fix: a
docker stopon aqueue:workcontainer burned its whole 40s grace
window and exited 137 with the in-flight job destroyed. As PID 1 — which
is what a container runs — the kernel discards an unhandled SIGTERM
outright, so the process did not die badly; it did not die at all until
SIGKILL.Server::runalready handled both signals correctly and its
listener is now shared, which also closes a missed-signal window in the
scheduler's loop. -
A job that killed its worker could never be dead-lettered. A job
whose handler fails is nacked and its attempt counted, so it
dead-letters aftermax_tries. A job that kills its worker — OOM,
abort, segfault, or the SIGKILL above — settles nothing; its reservation
merely lapses, and every driver used to redeliver it byte-identical.
Such a job is immortal: it kills each worker that claims it, comes back
unchanged, and kills the next one, for as long as anything restarts
workers. All three drivers now charge the attempt where they learn a
worker died, because swappingQUEUE_DRIVERmust not change whether a
poison job can be stopped.attemptsnow means "deliveries to a worker"
rather than "handler failures" — documented inmanual/queues.md,
because a worker lost for unrelated reasons burns an attempt too. -
…and the exhausted job is now dead-lettered before it is dispatched.
Counting the attempt was necessary and not sufficient. Every
dead-letter decision lived in the worker's settlement path, which
assumes the handler returns — so it never ran for exactly the jobs that
could not return. With the driver fix alone the counter climbed
(measured: 0 → 1 → 2 across three killed workers) and nothing acted on
it. The budget is now spent before the handler runs. Caught only by
re-running the container experiment after the first fix looked correct. -
The daemons had no tracing subscriber.
servegets one from
init_telemetry;queue:work,schedule:work,schedule:runand
workflow:workcome through a different boot path and got nothing, so
everytracing::line they emit went nowhere andLOG_LEVELwas inert
for them. That is most of what they have to say — a worker
dead-lettering a job, a scheduler skipping a tick it lost, a lock it
could not release. In a container the only visible output was the
startup banner, and the process looked idle while doing all of it. Two
of the defects in this release were invisible until this was fixed. -
A dead-letter with no failed-jobs store bound was a silent deletion.
The persist step sat insideif let Some(store) = .., so with no store
the arm did not match and execution fell through to the ack — quieter
than the failure path directly above it, which at least leaves the
reservation intact. An absent store was treated as more successful than
a broken one. It now logs the full envelope at ERROR, because that is
whatqueue:retryre-pushes: the difference between work recoverable by
hand and work that ceased to exist. -
QUEUE_DRIVER=databasenow binds a failed-jobs store.failed_jobs
is part of that driver's contract —queue:retryreads it and
Queue::retry_failedcannot work without it — butbootstrap_from_env
wired the driver and left the store unset, so a database-backed queue
dead-lettered into nothing unless the app bound one by hand. Configurable
viaQUEUE_FAILED_DB_TABLE. Only for this driver:memoryis ephemeral
by construction andredishas no table to write to. -
Redis reclaim latency now follows
--visibility-timeout. The flag
sets XAUTOCLAIM's idle threshold, but a separate clock governs how often
a consumer looks, and the driver left it at sea-streamer's 30s default —
so--visibility-timeout 5really meant "up to 35 seconds". The
interval now tracks the configured timeout, clamped to 1s..=30s so a
short timeout cannot become an XAUTOCLAIM storm and a long one can only
make reclaim faster than before.
Added
-
TaskBuilder::on_one_server()/on_one_server_for(ttl)— run a
scheduled task exactly once per due tick across replicas. Without it
nothing elects a leader for a tick: eachschedule:workprocess
evaluates the schedule independently, and three replicas were measured
running every due task three times, every minute, with no variance. A
nightly billing job on three replicas billed every customer three times.without_overlapping()does not cover this and cannot: its lock is
keyed on the task and released when the handler returns, so a fast task
frees it before a second replica looks.on_one_serverkeys on the task
and the tick and holds the lock past the handler, letting it expire on
TTL. The two compose.Opt-in, matching Laravel. Diverges from Laravel in failing closed: the
election is only as shared as the cache behind it, so a production boot
withCACHE_DRIVER=memoryand a single-server task is refused, naming
the offending tasks, withSCHEDULE_ALLOW_MEMORY_LOCK_IN_PRODUCTION=true
for deployments that genuinely run one scheduler.
Changed
manual/deployment.mdno longer says "run exactly oneschedule:work
process" as the only option, and gains a Stopping cleanly section
covering the drain windows per subsystem, how to size a platform's
termination grace above them, and why PID 1 makes a missing signal
handler worse than it sounds.
Suprnova v0.9.0
Security
-
Auth issuance could only be throttled per caller, never per
recipient. An address-keyed limit answers "is one client noisy"; it
cannot answer "is one mailbox being flooded". An attacker spread across
a botnet or a single IPv6/64stayed under every per-IP budget while
filling one victim's inbox with password-reset mail, and nothing in the
framework could express the limit that would have stopped it — a key
function could read the path, headers, and query string, but not a
form-encoded body, so the address was invisible on exactly the route
that carries it.identity_keykeys a bucket on the account being acted on. It reads the
query string first and then a buffered form body, so one key function
covers both shapes; the value is trimmed and lowercased, because
Alice@Example.comreaches the same mailbox asalice@example.comand
a limit bypassed by holding down shift is not a limit; and it is hashed,
because a rate-limit backend is frequently a shared Redis with weaker
access control than the primary database.Two new middleware builders support it.
key_reads_body(cap)buffers
the body before keying — opt-in, because buffering is work an
unauthenticated caller gets to make you do, and a body over the cap is
refused with 413 rather than passed through unkeyed.only_when(pred)
skips a limiter entirely for requests it has nothing to say about,
which is what keeps a stacked per-recipient budget from silently
becoming the binding limit on routes that name no recipient.The dogfood app now stacks both on its issuance group: 10 per 5 minutes
per address, 3 per 15 minutes per recipient.
A review of Torii's session, password, OAuth, and passkey paths turned up
eight defects, all fixed in the pinned fork (suprnova-torii-rs 968b0be).
- Expired sessions could be refreshed back to life. The SeaORM session
repository'srefreshhad no expiry predicate and unconditionally extended
expires_at, andOpaqueSessionProvider::refresh_sessionskipped the
is_expired()check thatget_sessionperforms. A token held past its
expiry could be renewed indefinitely. Fixed at both layers. Not reachable
through Suprnova's own surface — neitherToriinor the framework exposes
session refresh — but it is public API of both crates. - The login form leaked which accounts exist, by timing. Authentication
returned as soon as the email missed, skipping Argon2 entirely: measured at
54µs for an unknown address against 719ms for a wrong password, a ~13,000x
gap readable over a network. Both failure paths now verify against a dummy
hash so they cost the same. This one was reachable through Suprnova's
password login. - The JWT
issclaim was written but never verified. Algorithm pinning
was already correct —alg: noneand HS/RS confusion were never possible —
but the issuer was decoration, so two services sharing a signing key would
accept each other's sessions. Now enforced when an issuer is configured. - A single-use PKCE verifier could be claimed twice. Consumption was a
read followed by a delete, so two OAuth callbacks for the samecsrf_state
could both read it before either delete landed. Now claimed in one
operation —DELETE ... RETURNINGon Postgres, a primary-key delete whose
affected-row count picks the winner on SeaORM. - Expired sessions were listed as active.
find_by_user_idhad no expiry
filter, and expired rows survive until cleanup runs, so a "devices you're
signed in on" screen offered users dead sessions to revoke while saying
nothing about the live one. - A passkey lookup was named
authenticate. Torii's
PasskeyService::authenticate_credentialtook a credential ID and returned
the owning user, andPasskeyAuth::authenticateminted a session from it.
Torii stores passkeys — it carries no WebAuthn dependency and cannot verify
an assertion, so the only thing those calls proved was that the caller knew
a credential ID: a value the browser sends in the clear and
allowCredentialshands to anyone who can start a ceremony. Renamed to
find_user_by_credentialandcreate_session_for_verified_credential, both
documenting that verification is the caller's job. Not reachable through
Suprnova, which driveswebauthn-rsitself (see
torii_integration::passkey) and reaches Torii only for credential storage. - A WebAuthn challenge was replayable for its whole TTL. Neither backend
consumed a challenge on read, and the SeaORMget_challengealso ignored
expires_atentirely, returning expired challenges as live. Reads now
exclude expired rows on both backends, and a newtake_challengeclaims one
exactly once — the same delete-decides-the-winner shape as the PKCE fix.
Breaking
-
Azure Blob Storage and Google Cloud Storage moved behind the new
filesystem-azureandfilesystem-gcsfeatures.Storage::register_azblob,
register_azblob_with,register_gcs,register_gcs_with,AzBlobConfig
andGcsConfigno longer exist unless you enable the matching feature. If
you use either backend, add it to your dependency:suprnova = { git = "…", tag = "v…", features = ["filesystem-gcs"] }
You get a compile error naming the missing item, not a runtime failure.
Both opendal service crates pull
rsa, which carries RUSTSEC-2023-0071
(the Marvin timing attack) with no fixed release upstream. They were the
only crates enablingreqsign-core/jwt, the featurereqsign-core's
optionalrsasits behind, so gating them severs all three opendal paths
to it at once.rsais now avoidable:--no-default-features --features filesystem,database-postgresresolves without it and still has the
storage subsystem. Previously no feature combination could shed it while
keeping storage at all.A stock default build still carries
rsa—database-mysqlis a default
feature andsqlx-mysql 0.8.6depends on it non-optionally — so the audit
exception stays open. S3 is deliberately not gated:reqsign-aws-v4
takesreqsign-corewithoutjwt, so the S3 driver never contributed a
path, and gating it would break the most-used cloud backend while removing
nothing.
Added
suprnova --version, with-vas well as clap's default-V. Asking a
CLI its version with the flag every other CLI uses should not print a usage
error.
Fixed
- Two Redis operations had no upper bound. The cache's tag flush read a
tag's whole member set withSMEMBERSand deleted key by key, so a tag with
a large membership stalled the connection and a concurrent write could be
lost between the read and the delete; tags are now generation-based, flushed
atomically, and scanned with a boundedSSCAN. The delayed-queue promotion
pass moved every due job in one unboundedZRANGEBYSCORE, so a backlog that
came due together produced a single enormous script; it now promotes in
batches. - Two shutdown drains waited forever.
schedule:workon Ctrl-C and the
workflow worker after cancellation both awaited every in-flight task with no
deadline, so one task that never returned held the process open until
SIGKILL— an operator sees a daemon that "doesn't stop". Both now wait a
bounded grace, then abort what remains and report the count. - The release version-pin sweep only recognised one of the two pin
syntaxes, so every file carrying acargo install --tag vX.Y.Zline and
no dependency snippet was never discovered.suprnova-cli/README.mdhad
been telling readers to install v0.6.0 for three releases;manual/cli.md
andmanual/cli-new.mdsat at v0.7.2;manual/installation.mdcarried
both forms and had one bumped while the other froze. Discovery and rewrite
now read from one pattern table, and a file's rules are derived from its
content. cargo docfailed for any build withfilesystembut without
testing— sevenStorage::fakeintra-doc links could not resolve, and
lib.rsdenies broken links.testingis a default feature, so no gate
step had ever built that combination;check-feature-matrix.shnow does.- Torii's migrations could not be replayed over their own schema, so a
database holding it without thetorii_migrationstracking table — restored
from a dump that skipped it, or migrated by hand — could not be brought under
management. EveryTable::create()carried.if_not_exists(); none of the 19
Index::create()calls did, nor did theADD COLUMN locked_atalter, so
replay sailed through the tables and died on the firstCREATE INDEX. Fixed
in the pinned fork (suprnova-torii-rsa0f956d) viahas_index/
has_columnrather thanIF NOT EXISTS, which sea-query silently drops for
MySQL — the syntactic fix would have left a default-featured build broken. - A failed Torii migration aborted the process instead of returning an
error.SeaORMStorage::migrateunwrapped the migrator and returned
Ok(())unconditionally, soinit_torii's mapping of the failure into a
FrameworkErrorwas unreachable code. - An app's own
userstable silently suppressed Torii's, because
.if_not_exists()cannot tell "already mine" from "already somebody
else's". The migration reported success and authentication failed later on
a missing column — the reason the--apistarter names its table
app_users. Torii's migration now warns at migrate time when an existing
userstable lacks columns it requires, naming the columns and the remedy.
It stays a warning rather than a hard failure so existing deployments keep
booting. - The Railway and DigitalOcean deployment guides pointed the platform
health check at a path that could probe Postgres. Both platforms restart
the container when that check fails, so following the advice turned a
database blip into a restart loop across every...