A readable, per-version log of what changed in Suprnova. Each version
section is that version's release record. A version is released when its
version commit and matching v<version> tag are pushed atomically. Newest first.
1.2.0 - 2026-08-05
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.
1.1.0 - 2026-08-02
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 globalfallback_locale-pt-PTfrompt-BR,en-AUfromen-GB, and so on, transitively.Lang::get/try_get/get_with/try_get_with/hasall walk the chain, current locale first, so this works for anyTranslatordriver, 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:
FluentTranslatornow 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 terminalfallback_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 fromlang/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).
1.0.0 - 2026-08-02
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.mdis 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 inlang/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 returnResult<(), ValidationMessage>; a custom rule'sErr("…".into())body still compiles and still renders verbatim, but the signature in yourimplneeds 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 aMessageKeyunion 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 notrans_choicehere. Behind a default-onlocalizationfeature;--no-default-featuresstill compiles and still validates, using the embedded English fallbacks. -
IntoInertiaScrollforPaginator. The trait was implemented forLengthAwarePaginatorandCursorPaginatorbut not for the simple paginator, sosimple_paginateresults could not feedInertia::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 theLIMIT 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.
0.9.1 - 2026-08-01
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: adocker 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 after
max_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 frominit_telemetry;queue:work,schedule:work,schedule:runandworkflow: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 inside
if 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_jobsis part of that driver's contract -queue:retryreads it andQueue::retry_failedcannot work without it - butbootstrap_from_envwired 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 with
CACHE_DRIVER=memoryand a single-server task is refused, naming the offending tasks, withSCHEDULE_ALLOW_MEMORY_LOCK_IN_PRODUCTION=truefor deployments that genuinely run one scheduler.
Changed
manual/deployment.mdno longer says "run exactly oneschedule:workprocess" 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.
0.9.0 - 2026-07-31
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, becauseAlice@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's
refreshhad no expiry predicate and unconditionally extendedexpires_at, andOpaqueSessionProvider::refresh_sessionskipped theis_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 same
csrf_statecould 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'sPasskeyService::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 andallowCredentialshands to anyone who can start a ceremony. Renamed tofind_user_by_credentialandcreate_session_for_verified_credential, both documenting that verification is the caller's job. Not reachable through Suprnova, which driveswebauthn-rsitself (seetorii_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 SeaORM
get_challengealso ignoredexpires_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,AzBlobConfigandGcsConfigno 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-v4takesreqsign-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 with
SMEMBERSand 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 untilSIGKILL- 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 a
cargo 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.mdandmanual/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 withouttesting- sevenStorage::fakeintra-doc links could not resolve, andlib.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 the
torii_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 19Index::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 returnedOk(())unconditionally, soinit_torii's mapping of the failure into aFrameworkErrorwas 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 tableapp_users. Torii's migration now warns at migrate time when an existinguserstable 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 replica. Both now use
/_suprnova/health/live, with the database probed by hand from the console. The legacy paths still resolve; nothing already deployed needs changing.
0.8.0 - 2026-07-30
Remediation of an external red-team audit. The audit returned 19 P1 findings and a NO-GO verdict for 1.0; this release closes all nineteen, plus a number of defects found while fixing them that the audit had not named.
Several fixes deliberately turn a silent misconfiguration into a refused boot. Read Upgrading before deploying - a production app that has been running happily may not start.
Upgrading
Three configurations that used to boot with a warning (or in silence) now fail closed in production. Each error names the variable that unblocks it, and each has an explicit override for the deployment where the risk is genuinely absent.
- A non-delivering mail driver.
MAIL_DRIVERunset,log,memory, or an unrecognised value all resolved to a transport that renders mail and discards it - so password resets reported success while nothing was sent. Override:MAIL_ALLOW_NON_DELIVERING_IN_PRODUCTION=true. - Cleartext SMTP. Three of the four credential combinations landed on
an unencrypted transport, and the both-unset case logged a warning and
sent anyway. Override:
MAIL_ALLOW_INSECURE_SMTP_IN_PRODUCTION=true. - The in-memory rate limiter. Its buckets live in one process's heap,
so behind N replicas every quota is really N× and each deploy resets
them. Point
RATE_LIMIT_DRIVERatredis, or setRATE_LIMIT_ALLOW_MEMORY_IN_PRODUCTION=trueif you genuinely run one process. An unrecognised driver value fails for the same reason, because it fell back to memory -RATE_LIMIT_DRIVER=Redis, capitalised, is the case most likely to reach production because it looks configured.
Development, testing and staging are unchanged in all three cases. Staging is deliberately not gated: hard-failing it pushes teams to set the override globally, which disarms the check where it matters.
Two behaviour changes that are not boot failures:
fillandfirst_or_newreject malformed values. A value that cannot decode into its field's type used to become that field'sDefaultand returnOk-fill(attrs!{ age: "abc" })setage = 0and reported success. It now returns aValidationErrornaming the field, and leaves the model untouched. Unknown columns are still skipped silently (Laravel parity), and numeric widening still works./_suprnova/health?db=trueno longer returns the driver error. The detail moves to the log; the body keeps"database": "error". Debug builds still include it. Dashboards parsingstatus/databaseare unaffected.url::signature_has_not_expirednow requires a valid signature, and is deprecated. It used to answertruefor a forged URL - a bad signature is not "expired", because it never had an expiry to miss - so any handler guarding on it alone accepted forgeries. It is now identical tohas_valid_signature. If you were using it to tell expired from invalid (to render "request a fresh link" rather than a 403), switch tourl::signature_verdict, which returns all three states. This diverges from Laravel'sURL::signatureHasNotExpired, deliberately.
Two additions that need something from you only if you opt in:
QueueDrivergainedsettleandrelease, both with default implementations, so existing driver impls keep compiling unchanged. Implementsettleif your backend can commit a follow-up write and an acknowledgement in one transaction; implementreleaseif it can requeue a reserved message in place.- Batch accounting can now be durable.
DatabaseBatchRepositoryneeds two new tables,job_batchesandjob_batch_settlements- add them to your migrations, as withjobsandfailed_jobs. The schema is inmanual/queues.md. Nothing changes if you stay onMemoryBatchRepository.
Security
-
Slowloris (SEC-07). hyper's header-read timeout was documented as 30s but inert - it only arms when a timer is installed on the connection builder, and none was. A client could hold a connection, and a
SERVER_MAX_CONNECTIONSpermit, indefinitely. Now armed and configurable viaSERVER_HEADER_READ_TIMEOUT. -
Multipart uploads (SEC-05). The cap applied to individual part payloads but not to the raw stream, so a body could exceed the limit in aggregate. Now capped at the stream.
-
Webhook HMAC with an empty key (SEC-08). Both payment adapters accepted a blank secret, which verifies anything. Refused on both.
-
Paddle signature parsing (P2-11). An odd-length or non-hex
paddle-signaturereached the pinned SDK and panicked inside it. Now validated first: a malformed signature is a 401. -
Passkey enrolment and reset tokens (SEC-01, SEC-02). Anonymous enrolment against an existing email, non-owner enrolment, and owner enrolment without recent reauth are each refused with distinct statuses. A password login now stamps the reauth window.
-
dev:tls(SEC-10). A project could choose the CA the command trusts. -
Generated Docker Compose (P2-12). Published Postgres and Redis on all interfaces with credentials committed in this repository. Now bound to loopback with per-scaffold generated passwords,
.envwritten 0600, and symlinked targets refused. -
Health endpoint (P2-01, CI-05). It decided whether to query the database with
query.contains("db=true")- a substring test, so?nodb=trueran the probe too. Now parsed properly. The 503 no longer embeds the driver error, which named hosts, ports, schemas and versions. -
Credential issuance throttling (P2-02). The four auth-issuance routes in the reference app carried no rate limit at all, and the one route that did keyed its bucket on the raw
x-forwarded-forheader - which any client can vary per request to get a fresh bucket. Both fixed; the issuance budget is shared across the four routes so rotating between them does not multiply it. -
A redelivered chain step re-pushed its successor under a new id (DATA-02b, partial). Settlement pushes the next chain link before acking, deliberately: acking first means a crash in that window loses the chain permanently, and a duplicate is recoverable where silent loss is not. But the successor's envelope got a fresh
Uuid::new_v4()on every push, so the duplicate produced by that trade was indistinguishable from a legitimate new step - to the driver, to an outbox, and to the handler.That last one is the real cost. The framework's delivery contract is at-least-once and its answer to duplicates is "handlers must be idempotent" - but a handler keyed on
env.id, the only identifier it receives, could not satisfy that contract for a chained job, because the duplicate arrived under a new id every time. The contract was unsatisfiable by construction.The successor's id is now a UUIDv5 derived from its predecessor's, which is stable across that predecessor's own redeliveries. A redelivered step re-pushes the id it pushed before. No schema change, no new field, no new dependency.
This makes the duplicate detectable, which is the primitive the rest of DATA-02b was missing. It does not make the push atomic with the ack (that needs the outbox), and nothing yet rejects the duplicate on the way in. Both remain open.
-
Signed URLs verified one URL and executed another (SEC-04). The canonical form collapsed query pairs into a map, so a repeated key kept only its last value - while
Request::query_paramreturned the first. A legitimately signed?user=victimcould therefore be replayed as?user=attacker&user=victimwith the original signature untouched: verification canonicalised overvictimand passed, and the handler acted onattacker.The canonical form now carries every pair, sorted by
(key, value), so the signature covers the exact multiset of parameters - adding, removing, or substituting any value breaks the HMAC. A repeatedsignatureorexpiresis refused outright, since two of either leaves no non-arbitrary answer to which one governs.Request::query_paramnow resolves a repeated key to its last value, matchingquery_paramsandContext::query_param; it was the only one of the three that disagreed, and that disagreement was the other half of the defect. Existing signed links keep working - with no repeated keys the payload bytes are unchanged, which a test pins, because a canonical-form change that silently invalidated every outstanding password-reset link would be worse than the bug.Six regression tests, including both attack orderings, a legitimately repeated key that must still sign and verify, and the reordering guarantee. Not changed:
signature_has_not_expiredstill reports a forged signature as "not expired". That is Laravel's behaviour, was settled deliberately as a documentation fix, and has its own test pinning it against a well-meaning "correction". -
RBAC under Postgres. Verified against a real Postgres rather than SQLite alone.
-
Four RustSec advisories eliminated, not renewed. The Pinecone driver was rewritten against Pinecone's REST API, dropping
pinecone-sdk 0.1.2- whose newest release dates from 2024-09-06 - and with ittonic 0.11 → rustls 0.22 → rustls-webpki 0.102and RUSTSEC-2026-0049 / -0098 / -0099 / -0104. All four were fixed upstream inrustls-webpki >= 0.103.13, which this workspace already resolved for its other TLS users; one abandoned crate held the tree on the vulnerable line..cargo/audit.tomlis down from five ignores to one. See Changed for what this means for the driver's API. -
Audit exceptions now expire. Every entry in
.cargo/audit.tomlcarries anOWNERand anEXPIRESdate, andscripts/check-audit.shfails the release gate on a missing owner, a missing or unparseable date, or a lapsed one.cargo audithas no notion of an expiring ignore, so one added "temporarily" stayed until somebody re-read the file. The remaining entry (RUSTSEC-2023-0071,rsa, which has no fixed release at all) is owned and dated. -
Reachability claims are checked, not asserted.
scripts/check-feature-matrix.shresolves real dependency trees and asserts that no build - including--all-features, which is whatcargo auditactually reads - containspinecone-sdk,rustls-webpki 0.102.xortonic 0.11.x. An exception justified by a comment nothing verifies stops being true the first time someone adds a dependency.
Fixed
- Every release on a database-backed queue was silently a no-op.
JobOutcome::Released- a busyWithoutOverlappinglock, a rate-limiter backoff - was implemented as "push a copy, then ack the original". The envelope id is thejobstable's primary key, so the copy collided with the row still holding the live reservation and the push failed withUNIQUE constraint failed: jobs.id. The worker then correctly declined to ack, so the requested delay was never applied, noJobReleasedevent fired, and the job simply parked until visibility expiry redelivered it. Releases are now one driver call, done in place. - A partial batch dispatch orphaned the jobs it had already queued
(DATA-02). When a
driver.pushfailed mid-loop,PendingBatch::dispatchdeleted the batch row - but the envelopes already in the queue were still stamped with that batch id, so each of them settled against a batch that no longer existed, returningErr(batch not found)on every delivery, forever. The batch is now settled instead: undispatched jobs are recorded as failures and the batch is cancelled, so the queued ones settle normally and the terminal callbacks still fire. - Nothing tested that
url::has_valid_signaturerejects a forged URL. Found while verifying the SEC-04 fix: the entire framework suite passed with the primary signed-URL guard rewritten to accept any signature. - A scaffolded app could not migrate its database or build its image
(REL-01b). Neither scaffold declared
default-run, so all nine CLI wrappers that shell out tocargo runfailed on a fresh project. The generated Dockerfile had five independent defects - a missing lockfile COPY,npm ciwithout a lock, a cache stage stubbing one of two declared binaries, a frontend build copied from a path vite never creates, and a missingfrontend/src/pagescopy thatinertia_response!validates at compile time. A stock scaffold's image could not build. docker:initemitted one Dockerfile for every project type. On an--apiproject its first instruction,COPY frontend/package.json, failed outright. API projects now get a frontend-free Dockerfile.- SQL placeholders (DATA-01). Rendered per backend rather than assuming one dialect.
- Queue settlement (DATA-02a, P2-06c). Follow-ups settle before the reservation is acked, and a lock-release error no longer converts an already-succeeded job into a retry.
- A cancelled batch fired
Catch, neverThen. Builder::clonesilently dropped the eager-load plan (P2-09a).User::query().with("posts")cloned anywhere - pagination,count(), any scope that clones - returned rows with no relations and no error.- Presence rosters lost members (P2-08). The roster was snapshotted before subscribing, so anyone joining in that window appeared in neither, permanently.
- Pinecone serialised every index acquisition (P2-14). The write lock
was held across two network round trips, and
tokio's fairRwLockmeant one cold index stalled every warm one. - The type watcher discarded bursts (P2-13). Leading-edge debounce regenerated on the first file of a burst and dropped the rest with no trailing run, so the last save never took effect.
ssr:checkcould hang, and tried one address (P2-13). DNS ran outside the timeout entirely, and only the first resolved address was tried - so a host with an AAAA record and no IPv6 route reported the worker down while it was listening on v4.suprnova serveinstalledcargo-watchunpinned (P2-13). Now--lockedwith a major-version bound.- The release bumper rewrote five READMEs and nothing else. Four manual chapters and a public doc comment pinned tags that no release ever updated - the doc comment was two releases stale. Discovery now replaces the hand-maintained list, and the smoke test greps the bumped tree independently rather than trusting the bumper's own verify step.
db:synctreated the database schema as trusted input (CLI-01).migrate:freshis gated behind--forceplus a typed confirmation (CLI-02), in the app binary as well as the CLI.- The
logmail driver now logs the whole message, as Laravel does, and no longer writes bearer links to the log in production.
Added
- Atomic terminal settlement (
QueueDriver::settle, DATA-02). The chain successor and the acknowledgement now commit together onDatabaseQueueDriver, closing the window where a crash between them either lost the rest of a chain or ran its next step twice. The reservation-keyed delete doubles as a fence: a worker whose visibility expired mid-run commits nothing and reportsSettled::Stale, so it cannot enqueue work for a message another consumer now owns. Drivers that cannot do this answerSettled::Unsupportedand keep the documented push-before-ack ordering. DatabaseBatchRepository(DATA-02). Batch accounting survives a restart, andpending_jobs/failed_jobsare derived from settlement rows keyed(batch_id, job_id)rather than stored and decremented - so a redelivered job cannot drive a batch to "finished" while its other jobs are still running, and the guard holds across processes rather than within one./_suprnova/health/liveand/_suprnova/health/ready. Liveness touches nothing; readiness probes dependencies. Wiring a database check into a liveness probe turns a database blip into a rolling restart of every replica, which the single previous endpoint invited./_suprnova/healthkeeps working exactly as documented.SERVER_HEALTH_READINESS_TOKEN. Optional shared secret for the readiness probe, compared in constant time. Without it, readiness answers 404 - indistinguishable from an unrouted path, because it is the router's own 404. Unset by default so existing probes keep working.MAIL_SMTP_ENCRYPTION-starttls|tls|none, withsslandnullaccepted as Laravel-compatible aliases. Unset derives from the credentials, reproducing the previous behaviour exactly. This also makes implicit TLS on port 465 reachable: the transport supported it, but no combination of environment variables could select it.SERVER_MAX_CONNECTIONSandSERVER_HEADER_READ_TIMEOUTdocumented inmanual/env-vars.md, where they had been missing entirely.
Changed
The audit's own conclusion was that the gate passed in 470s and caught none of the 19 P1s. Most of this release's test work is aimed at that.
- Postgres runs in the gate. Twelve tests across six files had never
executed. Two of them turned out to aim
DROP TABLEat whatever Postgres was onlocalhost:5432by default, and neither had ever initialisedCrypt, so both failed the first time they ran. - Scaffold assertions read the bytes a user receives, after
substitution, rather than the template source. Found an API project
shipping a doc comment naming a database literally
{package_name}, and a.env.exampleadvertising five mail keys the framework never reads. - Queue fault injection. ACK loss, redelivery, lease lapse and partial dispatch are driven by a decorator that fails a named operation on a named call, so every case is deterministic rather than a sleep race.
- Payment adapters have negative tests. Stripe's
verify()had never been exercised with a valid signature, so every rejection path that depends on reaching the HMAC comparison was unproven. - The Pinecone driver speaks REST. Breaking, behind the
off-by-default
vector-pineconefeature. Motivation is under Security; the surface changes are:client()is gone - there is noPineconeClientany more. Replacing it arecontrol_plane_get,control_plane_postanddata_plane_post, which reach any Pinecone endpoint with your own request and response types over the driver's authenticated, host-resolved transport. That is strictly more reach than the old trapdoor had.json_to_metadata→metadata_from_json, and metadata is nowserde_json::Maprather thanprost_types::Struct.decode_match_fields→decode_match, taking aPineconeMatch.namespace()returns&str.- New:
with_control_plane,with_api_version,with_index_host(pins a known host and skips the control-plane round trip),index_host, and thePineconeVector/PineconeMatchwire types. from_envstill readsPINECONE_API_KEYandPINECONE_CONTROLLER_HOST, and now alsoPINECONE_API_VERSION.- The REST API version is pinned, not floated -
2025-04, the version the driver's request and response shapes were written against. - Nothing serializes any more. The old driver cached one
Indexper name behind atokio::Mutexbecausepinecone-sdkexposed it only behind&mut self; the new one caches a host string and sharesreqwest's connection pool. - A host learned from the control plane is always contacted over
https, whatever scheme the response carries. Debugis implemented by hand with the API key redacted, so a#[derive(Debug)]on a struct holding a driver can't print it.
- Wire-contract tests for Pinecone. The live integration tests need a
PINECONE_API_KEYand so cannot run in the gate - which left a REST rewrite's field names (topK,includeMetadata,vectorCount) resting on nothing. Thirteen tests now drive the driver against a localwiremockfake and assert the exact method, path, headers and JSON body it puts on the wire, plus that a non-2xx is never decoded as a result and that an error message never carries the API key. They pin the driver to Pinecone's documented contract; only the#[ignore]d tests can confirm the documentation matches the live service.
0.7.2 - 2026-07-28
Fixed
generate-typesresolves nested prop structs without derives. 0.7.1's generator degraded any prop field whose type didn't deriveInertiaProps/Datatounknown- so re-running the generator (or thesuprnova servewatcher) over a project with a committed types file replaced real interfaces likeArray<AdminArticleRow>withunknownand broke type-checking across the app. Plain structs defined anywhere insrc/now resolve to their real interfaces, transitively from the prop roots;unknown(with a warning) is reserved for types the project genuinely doesn't define - external crate types, enums, tuple structs.
Changed
-
routes.tsgeneration is opt-in.generate-typesno longer dropsfrontend/src/types/routes.tsinto every project unasked; pass--routesto generate it. -
Frontend starter dependencies refreshed. New scaffolds from
suprnova newnow pin current versions: Vite ^8.1.5, Tailwind CSS ^4.3.3, Svelte ^5.56.8 (vite-plugin-svelte ^7.2.0, svelte-check ^4.7.4), React ^19.2.8 (plugin-react ^6.0.4), Vue ^3.5.40 (plugin-vue ^6.0.8, vue-tsc ^3.3.8), and@types/node^24 (the Node 24 LTS types line). TypeScript stays at ^6.0.3 deliberately: it is the latest 6.x, and svelte-check's peer range (^5 || ^6) does not yet admit TypeScript 7. All three starters were verified end to end (npm install+npm run build) against the refreshed set.
0.7.1 - 2026-07-27
A defect-fix pass over 0.7.0's queue routing, from a full post-release review.
Fixed
-
Chained jobs no longer lose their declared queue.
ChainLinkcaptured a job'smax_tries,timeout, andbackoffat chain-build time but not itsJob::queue(), so a job that landed on its declared queue when pushed directly landed ondefaultwhen dispatched as part of a chain - the "job" tier of the route → job → default resolution order silently vanished for chains. The declared queue is now captured on the link and resolved exactly like a direct push. Chain payloads written before this release decode unchanged (serde(default)), and a link with no declared queue serializes byte-identically to what 0.7.0 wrote. -
Failed-job records carry the queue the job died on. The worker's dead-letter path hardcoded
queue = "default"into everyFailedJobrecord, so failures of a routed job were invisible to an operator filtering the failed store by the pool that owns them. The record now carries the envelope's queue (defaultfor unrouted jobs). -
The 0.7.0 upgrade note understated the
jobsmigration. It read "unfiltered workers are unaffected and need no migration", butDatabaseQueueDriver::pushnames thequeuecolumn in itsINSERTwhether or not the job is routed - a 0.7.0 binary against an un-migrated table fails every push, filtered or not. The 0.7.0 section below andmanual/queues.mdare corrected: on the database driver theALTER TABLEis required for every deployment, and it must run before binaries roll (older binaries list their columns explicitly, so migrating first is safe). -
README no longer advertises a
#[job]macro. No such macro exists - jobs implement theJobtrait. The queues row now describes the real surface, including 0.7.0's queue routing.
Changed
- The release path now bumps README version references.
bump-workspace-version.pyrewrites the README's pinned install tag, the distribution-model example, and the MSRV line atomically with the manifests, and a reworded README that stops matching a pattern fails the release loudly. The README had advertised v0.6.0 since v0.7.0 shipped because nothing in the release path touched it. - Connection routing is documented as name-resolution only.
Job::connection()and the connection field ofQueue::routeresolve the connection name carried on theJobQueueing/JobQueuedlifecycle events; a single process-global driver still receives every push, so they do not select a different driver. The rustdoc andmanual/queues.mdpreviously implied driver selection that does not exist. The queue dimension is unaffected - it is honored end to end. Per-connection drivers remain future work. ChainLinkgained a publicqueue: Option<String>field, which breaks struct-literal construction of chain links. Links built throughChainLink::from_job- the normal path - are unaffected.
Upgrading
Coming from ≤ 0.6.x on the database queue driver, apply the 0.7.0 migration
below before rolling binaries; it is required for every deployment on
that driver, not just ones using --queue. 0.7.1 itself needs no migration.
0.7.0 - 2026-07-26
Security
- Upgraded
ammoniato 4.1.4 (RUSTSEC-2026-0213). Versions through 4.1.3 allow XSS via SVGanimateandsetanimation tags.ammoniais the sanitizer at the end of Suprnova's markdown pipeline (comrak→syntect→ammonia), so any app rendering user-supplied Markdown throughcontentwas exposed. The advisory was published 2026-07-21 - after v0.6.5 shipped - so every release up to and including v0.6.5 is affected. Upgrading the framework is the fix; no application code changes are required.
Added
- Queue routing. Jobs can be dispatched to a specific queue and connection,
and workers can be dedicated to specific queues - the Laravel 13
Queue::route(...)surface, typed. A job states its own home withJob::queue()/Job::connection(); an operator overrides it centrally withQueue::route::<SendInvoice>(Some("redis"), Some("billing"))inbootstrap::register(), without editing the job. Resolution is route, then job, then global default, and aNonefield in a route defers rather than clearing.queue:work --queue=billing,defaultdrains only those queues. Unrouted jobs belong todefault, so they are never stranded. Chained jobs resolve routes by name, since a chain link stores its job erased. QueueDriver::pop_from. Filtering pop, with a default implementation that rejects a filter it cannot honor rather than silently draining every queue - a worker told to drainbillingthat quietly drains everything is indistinguishable from a working deployment until the wrong pool eats the wrong jobs. The memory and database drivers filter natively. Custom drivers keep compiling and inherit the loud default.- Documented the
jobstable schema.manual/queues.mdnow carries the DDLDatabaseQueueDriveractually expects, which was previously only discoverable by reading the driver's SQL. - Documented Inertia's
serverHeadoption. Server-driven<head>elements (Inertia 3.5.0) need no framework support: the client reads them from an ordinary prop, so any handler can already supply them. Seemanual/frontend-inertia-responses.md.
Changed
Envelopegained aqueue: Option<String>field. It isserde(default)and skipped when absent, so an unrouted envelope serializes byte-identically to what previous versions wrote - the frozen wire-format test passes unchanged, there is noschema_versionbump, and mixed-version fleets interoperate during a rolling upgrade.WorkerConfiggained aqueues: Vec<String>field (empty = drain everything, the previous behaviour).- Removed
ROADMAP.md. Its design principles live inmanual/introduction.md, the working agreement inmanual/contributions.md, and the deployment and scale-out material inmanual/deployment.md; the shipped/planned checklists had gone stale.README.md's pointer to it for "the relationship to upstream" was already dangling - that attribution lives inLICENSE. - Scaffold frontends now pin
@inertiajs/{svelte,react,vue3}at^3.6.1(from^3.4.0). The 3.4.0 → 3.6.1 range is client-side only - audited against the upstream changelog and thePagecontract inpackages/core/src/types.ts, everyX-Inertia-*header the 3.6.1 client sends was already handled. scripts/release.shnow publishes the GitHub release itself, with notes taken from the version'sCHANGELOG.mdsection. Previously this was a manual "next step" that got skipped, which is why v0.5.10 and v0.6.1–v0.6.3 are tag-only and the Releases page sat on a stale version. Preflight runs before the gate so a missingghor changelog section fails in seconds, and publishing is skipped automatically unlessoriginis GitHub.
Upgrading
Existing jobs tables on the database queue driver must add the new
column - push names it in its INSERT whether or not the job is routed, so
an un-migrated table fails every push. Migrate first, then roll binaries
(older binaries list their columns explicitly and ignore the new one, so that
order is safe):
jobs ADD COLUMN queue TEXT NULL;
ON jobs(queue);
(Corrected in 0.7.1 - this note originally claimed unfiltered deployments needed no migration.)
0.6.5 - 2026-07-21
Added
- Hosted one-off Checkout in the Stripe adapter.
Checkout::start_sessionwithSessionMode::OneOffand non-emptyprice_refsnow creates a hosted Checkout Session (mode=payment, one line item per price ref,allow_promotion_codes=true) and returnsSessionPayload::StripeCheckoutRedirect. Theamount_hint-only Elements path is unchanged; the two shapes are picked per request. - Stripe Managed Payments (merchant-of-record) support.
StripeProvider::with_managed_payments(true)- orSTRIPE_MANAGED_PAYMENTS=trueinfrom_env()- sendsmanaged_payments[enabled]=trueon hosted one-off session creation. Off by default; the field is omitted entirely so non-enrolled accounts are unaffected. Checkout::session_status. New trait method (default:PaymentError::NotSupported) reporting a session's provider-side state as the new neutralCheckoutSessionState(Open/Complete { paid, payment_ref, amount_total }/Expired). The Stripe impl mapsGET /v1/checkout/sessions/{id};payment_refcarries the session's PaymentIntent id for mirror-table correlation. This is the server-side verification primitive for redirect return pages and reconciliation sweeps.Promotionscapability trait.create_promotion_codemints a customer-restricted, optionally expiring, redemption-capped code off a pre-created coupon. Queried via the newPaymentProvider::as_promotions()(defaultNone). Implemented for Stripe (POST /v1/promotion_codes) and the mock.MockPaymentProviderupgrades for the above. Records everystart_sessionrequest (recorded_sessions()), scriptssession_statusper session id (script_session_status()- unscripted known sessions reportOpen, unknown idsNotFound), and implementsPromotionswith recorded requests (recorded_promotion_requests()).
0.6.4 - 2026-07-17
Fixed
- Eloquent aggregates decode consistently across database backends. Generated
count,sum,avg,min, andmaxexpressions now use one stable internal result alias. PostgreSQL no longer returns false zeroes orNonebecause its driver labels aggregate columns differently from SQLite, and missing-column or incompatible-type errors now propagate instead of being silently defaulted. - Mass deletes cannot use caller-supplied table expressions. Executable
delete SQL always derives its target from the model's validated static
M::TABLE. The legacy public renderer argument remains source-compatible but cannot redirect or inject the delete target.
0.6.3 - 2026-07-15
Added
- Typed raw reads can stay on a transaction's pinned connection.
Transaction::backend()exposes the active backend andTransaction::query_all(Statement)executes typed aggregate or custom SQL through the transaction while preservingQueryExecutedinstrumentation. Applications no longer need a pool-level query or private executor access when a lock-scoped decision depends on computed result columns.
0.6.2 - 2026-07-15
Fixed
- Bound raw predicates are backend-neutral. Eloquent
filter_rawandwhere_rawnow accept portable?bind markers on every database backend; PostgreSQL rendering rebases them to monotonic$Npositions across prior predicates, relationship subqueries, HAVING clauses, and UNION arms. Existing numbered PostgreSQL fragments are normalized by their local marker order, while mixed styles and bind-count mismatches fail validation before I/O. The SQL-aware scanner preserves question marks inside quoted strings, identifiers, comments, and dollar-quoted bodies;??emits a literal question-mark operator in a bound raw fragment.
0.6.1 - 2026-07-15
Added
- Observable supervised session cleanup.
SessionMiddleware::installuses the configurableSESSION_GC_INTERVALcadence (one hour by default), whilesession_gc_metrics()exposes process-local run, success, failure, removed-row, and last-result timestamps for protected operations surfaces. - Bounded sliding-session touches.
SESSION_TOUCH_INTERVALcontrols the minimum activity-write cadence (five minutes by default) and is capped at half the session lifetime so active sessions cannot expire between touches.
Fixed
- State-free requests no longer create durable sessions. Requests without a valid session cookie perform no session-store read or write and receive no session cookie unless handling creates state. Existing clean sessions avoid unconditional upserts and cookie churn, legacy cookies migrate on their next request, and cookies whose backing rows have expired are cleared without recreating empty sessions.
0.6.0 - 2026-07-10
Added
- Opt-in framework subsystems with backward-compatible defaults. Filesystem
storage, SQLite/Postgres/MySQL database drivers, the MariaDB vector driver,
and Web Push now have explicit Cargo features. Existing default builds retain
all of these capabilities, while
default-features = falseconsumers can select zero drivers or only the storage/database/vector/push surface they use. The executable feature matrix verifies zero-driver, individual-driver, Nation X minimal, default, and all-feature profiles. - Raw P-256 VAPID private-key import.
VapidKey::from_bytesaccepts a validated 32-byte big-endian P-256 scalar alongside the existing PKCS#8 PEM import/export path.
Changed
- VAPID JWTs are signed directly with P-256. Web Push now serializes the
RFC 8292 ES256 header/claims and signs them with
p256, removing the generic JWT dependency while preserving generated keys, PEM round trips, public-key encoding, and the 24-hour lifetime bound. - Security dependency refresh. Updated vulnerable framework dependencies, including bcrypt and ammonia, and narrowed Comrak's enabled features while retaining syntax highlighting.
- Rust 1.91.1 is the release MSRV. Every workspace package declares the
same
rust-version, generated Dockerfiles pin the matching builder image, and the full release gate compiles the supported filesystem profile with the exact Rust 1.91.1 toolchain. - OpenDAL 0.58 security pin. The filesystem feature pins
eas4ai/opendalcommit88717391eb72c9839d3f8e79fccad9f22fc3a1b4, a minimal fork based exactly on official Apache OpenDAL commitae99a3b016e354a1b2bb2baf0c70f9f9e134970a. The fork changes only the Reqsign declarations used by OpenDAL core plus S3, GCS, and Azure Blob so downstream consumers resolve official Apache Reqsign commitb49cd2996b9d2d9944e84481f8835ff55b188b97andquick-xml0.41.0. A fork is required because a dependency repository's root Cargo patches do not propagate to consumers; the published graph could otherwise restore vulnerablequick-xml0.38/0.40.
Fixed
- Atomic release version metadata. The release bump now updates
workspace.package.versionand every versioned internal path dependency in one validated operation, stages every affected manifest, and proves a temporary0.6.0workspace withcargo check --workspacebefore release. Release versions are validated as strict SemVer 2.0, including the numeric prerelease leading-zero rule. Version-agnostic disposable bare-remote smokes derive a later patch release from both the current source and an already0.6.0source, reject staged/unstaged/untracked release trees before the gate, prove atomic commit/tag publication rolls both refs back when a tag is rejected, and prove the normal release sequence without touching the real remote. Release versions must increase by SemVer precedence, including prerelease transitions. Smoke build artifacts always stay inside their temporary workspace, ignoring any callerCARGO_TARGET_DIR. - Rustdoc covers every supported feature boundary. The OAuth module links
to public
OAuthAuth::complete, and the executable matrix builds zero-driver, default, and all-feature rustdoc with no dependencies. - Filesystem stream validation is session-scoped. Local filesystem writers, listers, and copiers resolve and confine their paths once before first I/O instead of once per chunk/item, while activated close/abort operations always reach the backend for cleanup. Existing traversal and symlink confinement remain enforced for a trusted filesystem; canonicalize-then-open checks do not eliminate races against a principal concurrently mutating the tree.
Security
- The release gate fails closed.
release.shdelegates to the canonical full gate before editing manifests or creating commits/tags; that gate always runscargo audit, treats a missingcargo-auditbinary as an error, and stops on any audit failure. It also builds and audits an isolated downstream filesystem consumer, asserting exact OpenDAL/Reqsign source revisions and noquick-xmlbelow 0.41. No new advisory ignores were added.
0.5.10 - 2026-07-03
Fixed
generate-typesno longer drops self-referencing structs. A struct with a field that references its own type (a tree node withchildren: Vec<Self>, e.g. a threaded-comment view) created a self-edge in the type-dependency graph, pinning its in-degree above zero so Kahn's topological sort never emitted it - leaving every interface that referenced it with a dangling type name that failedsvelte-check/tsc. Self-edges are now stripped before sorting, and any structs trapped in a reference cycle (mutual recursion) are emitted in arbitrary order rather than dropped, since TS interfaces may reference one another regardless of declaration order.
0.5.9 - 2026-07-01
Added
MAIL_FROM_NAME- optional display name on auth-flow emails. The email-verification, password-reset, and password-changed mailables now render theirFromheader as"Name <address>"whenMAIL_FROM_NAMEis set (read at send time so it survives the queue's serde round-trip).MAIL_FROMstays a bare address; leavingMAIL_FROM_NAMEunset or blank keeps the previous bare-address behavior. No change to any call site - the mailables read the env var themselves.
0.5.8 - 2026-06-30
Fixed
generate-typesroute helpers are always valid TypeScript. When several routes in a module share one handler (e.g. astatic_files::servewhitelist mapping many favicon/asset URLs), the first kept the handler name and the rest got a key derived from the route path - but the path was only partly sanitized (/ { } -→_), so a file extension leaked a.into the key:favicon_16x16.png: (...) => .... That is member access, not a property name, sotsc/svelte-checkrejected the generatedroutes.ts. Derived keys are now sanitized to legal identifiers - every non-alphanumeric character becomes_and a leading digit is prefixed - sofavicon-16x16.png→favicon_16x16_pngand2fa.json→_2fa_json. Unique handler names are untouched.
0.5.7 - 2026-06-30
Fixed
generate-typesno longer emits dangling type references. A prop field whose type is a struct that doesn't deriveInertiaProps/Data(or an external type the generator can't see) was emitted as a bare identifier - e.g.user: UserInfo- producing TypeScript that failstsc/svelte-checkbecause that interface is never written. Such references now degrade tounknown(user: unknown;Vec<T>→Array<unknown>;Option<T>→unknown | null), so generated output always type-checks, andgenerate-typesprints a warning naming the unresolved type and the field that references it, with the fix (deriveInertiaProps/Dataon it). Generic parameters and resolved nested InertiaProps/Data types are unaffected.
0.5.6 - 2026-06-29
Changed
- Sign in with Apple: RS256 JWKS verification. Bump
suprnova-apple-rsto v0.3.1 - Apple ID tokens are now verified against Apple's published JWKS (RS256) instead of being trusted structurally.
0.5.5 - 2026-06-28
Added
MagicLinktoken purpose. NewMagicLinkvariant on the auth-flowTokenPurposeenum, for passwordless magic-link sign-in tokens.
0.5.4 - 2026-06-28
Changed
- Composable OAuth completion. Split the generic OAuth completion into
verify_oauth_identity(verify + resolve the identity) and a thincomplete, so apps can verify an OAuth identity without triggering the full session-completion side effects.
0.5.3 - 2026-06-28
Fixed
- Correct workspace version metadata. v0.5.2 was tagged and pushed before
its
Cargo.tomlversion bump was staged, so the pushed v0.5.2 tag still readsversion = "0.5.1". v0.5.3 re-cuts the release with the correct workspace version - no code change (the v0.5.2 OAuth split is unaffected).
0.5.2 - 2026-06-28
Changed
- Composable Apple completion. Split Apple Sign-In completion into
verify_apple_identity+ a thincomplete_apple, mirroring the generic OAuth split. (Note: the pushed v0.5.2 tag carries a stale0.5.1version field - fixed in v0.5.3.)
0.5.1 - 2026-06-28
Changed
- Renamed Apple crate. Repoint the Apple dependency to the renamed
suprnova-apple-rsrepository.
0.5.0 - 2026-06-28
Added
- Sign in with Apple. OAuth token exchange + ID-token verification + user
upsert for Apple; Apple well-known endpoints and the
form_postresponse mode; Apple-specific fields onOAuthProviderConfig;AppleKeyPairre-exported so apps configure Apple Sign-In without a directappledependency.
Fixed
- Omit PKCE parameters from the Apple authorize URL (Apple rejects the request when they are present).
Dependencies
- Consume the
toriimagic-auth fix; addapple-rsv0.3.0.
0.4.1 - 2026-06-26
Performance
- Pre-size
MiddlewareChainto eliminate per-requestVecreallocations.
Fixed
- Make the maintenance down-file path collision-proof under parallel test runs.
Docs
- Compile-check the framework's doc examples (
ignore→no_run); reconcile the distribution notes with the tagged GitHub Releases; ignore the wholedocs/tree.
0.4.0 - 2026-06-22
Changed
- Distribution is git-tracked; you don't pin to tags. Scaffolded apps
depend on
suprnova = { git = "…/suprnova.git" }and track the default branch; pull updates withcargo update -p suprnova. Versions are published as tagged GitHub Releases (v0.4.0, …) for the changelog, butCargo.lockalready pins the exact resolved commit - so builds stay reproducible without hand-pinning atagorrev. The installation docs no longer present commit-pinning as the update path.
0.3.0 - 2026-06-21
Added
- Query instrumentation for Eloquent reads -
Builder::get,Model::find,find_many, andallnow emitQueryExecuted, so model SELECTs and eager-load queries surface inDB::listenand the in-memory query log alongside writes and raw queries. Adds the instrumentedExecutorChoice::statement_allread terminal. - Resource-route authorization -
ResourceRoutes::authorize_resource::<U, R>()attaches the conventional ability check to every generated resource route as per-route middleware (LaravelauthorizeResourceparity). The action→ability map isindex/show→view,create/store→create,edit/update→update,destroy→delete. One call gates the whole seven-action surface instead of relying on every controller body to remember aGate::authorize. - Atomic rate-limit hit -
RateLimiter::hit_and_check(key, max, decay)increments a fixed window and tests it in a single round-trip, returning whether the bucket is now over its limit (i64::MAXmeans unlimited). - Constant-time comparison helper -
constant_time_eq(a, b)(subtle-backed) for webhook signature verification;WebhookHandler::verifydocs now mandate constant-time digest comparison. - Inertia client to 3.4.0 - the Svelte/React/Vue scaffolds now pin
@inertiajs/{svelte,react,vue3}at^3.4.0(from3.1.1), picking uprouter.pollmodes, dynamicusePoll,Inertia.once, the InfiniteScroll cancel fix, and awaited FormonSuccess. The server already emits the full 3.4.0 page-object and header surface (once-props, the prepend/deep-merge scroll family,matchPropsOn, rescued/shared props), so this is a client-currency bump with no protocol change. - Optional connection cap -
SERVER_MAX_CONNECTIONS(and the programmaticServer::max_connections(n)) bounds concurrently active connections with a semaphore on the accept loop, applying back-pressure at the TCP level. Unset - or0- leaves connections unbounded (the default, unchanged). A backstop to pair with a reverse proxy andLimitNOFILE, not a replacement for upstream rate limiting. - Opt out of redirect-following -
RequestBuilder::no_redirects()routes a request through a non-following HTTP client so a3xxis returned as-is instead of chased. Use it when the request URL is influenced by untrusted input, to close a redirect-based SSRF vector (a hostile endpoint redirecting toward an internal or cloud-metadata host). The default client still follows redirects, matching general-client convention.
Security
- Resource routes fail closed on the authorization registry's type-erased
downcast instead of panicking, and
authorize_resourcedenials / unauthenticated requests are refused before the handler runs. - Rate limiter closes a fixed-window check-then-hit race by incrementing and
comparing atomically (
hit_and_check). - Queue
RateLimitedmiddleware now admits jobs through that atomichit_and_checkinstead of a separatetoo_many_attempts+hitpair, so concurrent workers can no longer all pass the budget check before any of them increments and over-admit pastmax_attempts. - Upload validators (
mimetypes/mime) content-sniff the uploaded bytes instead of trusting the client-suppliedContent-Type. - Filesystem path guard canonicalizes paths to catch symlink traversal out
of the storage root, beyond the prior lexical
..// absolute / UNC checks. - Auth closes a passwordless-login timing oracle - a matched-but-passwordless
account given a password now runs a fixed-cost verify, across both the Eloquent
and database user providers - and
dummy_verifydrives the configured hasher so the unmatched-user path is constant-time. - Eloquent validates column identifiers on the
pluck/value/pluck_keyed/sole_valueandsum/avg/min/maxprojection paths. - Payments - the mock provider's verifier fails closed outside a development
environment, and webhook source IPs resolve through
TrustedProxiesConfig(req.ip()) rather than a rawX-Forwarded-Forheader. - Filesystem path guard now walks to the nearest existing ancestor when a write target doesn't exist yet, closing a symlink escape where a planted intermediate symlink with a missing immediate parent slipped past the guard.
DB::init_withvalidates the environment before connecting (matchingDB::init), so the dev SQLite fallback can no longer boot silently in production through that entry point.- Static-file serving rejects dotfiles (
.env,.git/config,.htpasswd, any leading-.segment), not just./..traversal. - Payment webhooks serialize concurrent retries of the same unprocessed
event with a
FOR UPDATElock + re-check, and treat mirror-table unique violations as benign already-applied;payments_subscription_itemsgains aUNIQUE(subscription_id, provider_item_id). - RBAC defaults the model discriminator to the fully-qualified type name, so two authenticatable types sharing a leaf name can no longer inherit each other's roles/permissions.
invalidate_session()rotates the session id (not just flushes), closing a session-fixation gap; the queueWithoutOverlappingmiddleware releases its cache lock even when the job panics.- Mail providers cap error-response body reads (8 KiB), matching the web-push client, so a hostile endpoint can't drive sender memory.
- Web push disables HTTP redirect-following on the default client, so an
attacker-influenced push endpoint can no longer
3xx-redirect a notification POST toward an internal or cloud-metadata host (SSRF). A redirect now surfaces as a rejected push rather than a silently followed request. - Stripe adapter
Debugredacts the webhook signing secret and prints a placeholder for thestripe::Client(which carries the API secret key in its auth header), so neither secret can reach logs through a{:?}ofStripeProvider, regardless of the upstream client's ownDebug. - Stripe adapter
from_envrejects present-but-blank credentials, failing closed instead of constructing a client with an empty (and therefore forgeable) webhook HMAC secret. - OAuth email verification fails closed for unrecognised providers: a
userinfo payload carrying an
emailbut noemail_verifiedflag is no longer treated as verified. An unknown provider must now assertemail_verified: trueor expose a verified-emails endpoint, closing an account-link/takeover vector for apps that key accounts on email. Google (explicit-true-only) and GitHub (verified-by-the-/user-contract) are unchanged.
Fixed
- Nested eager loading (
with(["posts.comments"])) is now a constant number of queries - the tail segment loads in one batched IN query across all parents instead of one query per parent (N+1). where_has/where_doesnt_havequalify closure columns with the target table, so a column present on both pivot and target no longer produces an ambiguous-column error on many-to-many relations.- Soft-delete
delete/force_delete/touchand factorypersisthonor a model's#[model(connection = "…")]routing (matchingrestoreand the other write paths) instead of falling back to the primary pool. - JSON:API
Maybe::Missinguses a non-collidable wire sentinel, so user data shaped like{"__missing__": true}is no longer silently stripped. - Queued notifications honor
should_send(per-channel veto) andafter_sending, re-checked on the worker - previously only the synchronous path did. - Released jobs push the retry copy before acking the original, so a transient driver push error no longer drops the job.
- Paddle adjustment (refund) webhooks key the mirror update off the referenced
transaction id and read amounts from
data.totals, instead of inserting a zero-amount row under the adjustment id. - SQLite URLs carrying a query string (
sqlite://db.sqlite?mode=rwc) build a valid single-query connection URL and a clean on-disk filename. - HTTP clamps
Acceptq-values to[0,1]and enforces aFormRequest'smax_body_byteseven when the body was pre-buffered; WebSocket config rejectsmax_missed_pings < 2(1 closed every connection on its first ping). - Cron day-of-month and day-of-week use OR semantics when both are restricted
(Vixie/POSIX parity); Markdown
plain_text/excerpts preserve intentional spaced punctuation;CachedEvaluatorbounds its cache growth;SupervisorRegistry::start_allno longer double-spawns on a second call; the test container recovers in place from a poisoned lock. - Supervisor restart backoff resets to the 100 ms floor after a run that stays up at least the 60 s cap, so a daemon that ran healthily for a long stretch and then exits restarts promptly instead of inheriting backoff that climbed during an earlier failure burst. A crash loop whose runs never reach the threshold still ramps to the cap, so the reset never masks a flapping supervisor.
- Corrected stale docs on
filter_op(operators are allowlist-validated), signed URLs (not byte-compatible with Laravel's default absolute signatures),UniqueIdKind::is_valid(a caller helper, not auto-wired intofind), and the identifier length cap (128, not 64).
Documentation
- Documented resource-route authorization (
authorize_resource) in the routing and authorization chapters, and the atomichit_and_checkcounter in the rate-limiting chapter.
0.2.0 - 2026-06-21
Adds role-based access control, a Markdown content / docs-rendering pipeline, and native static-file serving.
Added
- Tier-2 RBAC -
HasRolestrait; roles + permissions with arole_has_permissionsjoin;PermissionMiddleware/RoleMiddleware(both fail-closed / default-deny); theCreateRbacTablesmigration; andcreate_role/create_permission/give_permission_to_rolehelpers. - Content rendering - Markdown rendering and a docs-build pipeline:
MarkdownRenderer,build_docs,DocsCatalog/DocsChapter, heading extraction andslugify_heading. Rendered HTML is sanitized (comrak + syntect + ammonia). - Native static-file serving -
StaticFiles::public()fallback handler for serving apublic/directory at the web root, replacing hand-rolled per-asset whitelist controllers in apps.
Fixed
- Freshly generated apps inherit a framework-level
time = 0.3.47compatibility pin, avoiding Rust 1.96 coherence conflicts fromtime 0.3.48in fresh scaffold dependency resolutions.
Documentation
- Documented the two shipped starter kits - Nebula (Breeze-tier auth) and Pulsar (product site + community) - across the manual, README, and roadmap; restructured the roadmap around the shipped surface; and reconciled version references throughout the docs.
0.1.0 - 2026-06-10
The initial Suprnova release. Suprnova is a Laravel-inspired web framework for Rust, forked from Kit and taken in its own direction. Today's parity target is Laravel 13.x.
This release uses the git distribution model: framework consumers depend
on suprnova = { git = "https://github.com/eas4ai/suprnova.git" },
and the CLI installs with cargo install --git.
Added
HTTP, routing, and middleware
Routerwith route groups, prefixes, parameter constraints, named routes- Compile-time-validated route registration via the
routes!macro - Resource routing (
Router::resource) producing the seven standard routes - Signed URLs (
url::signed_route/url::temporary_signed_routefree functions, plusRedirect::signed_route/Redirect::temporary_signed_route) - Redirect helpers -
Redirect::to,Redirect::back,Redirect::route,Redirect::with_input,Redirect::with_errors,with_flash - Middleware trait with global, group, and per-route layers
- Built-in middleware - CORS, CSRF, session, request timeout, request ID, throttle / login throttle, signed-URL verify, authenticated, email-verified, brute-force
- Abort helpers (
abort,abort_unless,abort_if) suprnova::handle_request(...)- public adapter to serve a single hyper request against a router + middleware chain
Inertia.js frontend bridge
#[derive(InertiaProps)]with TypeScript type emissioninertia_response!macro with compile-time component validation- Three first-class starter frontends - Svelte 5 (runes-on), React 19, Vue 3.5 - all on Inertia 3.1.1 + Vite 8 + Tailwind v4
- Partial reloads (
only/except), deferred props, persistent layout, encrypted history, scroll preservation Inertia::paginate(component, key, paginator)for paginator → Inertia prop wiring
Eloquent-style ORM (over SeaORM)
#[suprnova::model]attribute macro that emits a SeaORM entity and the user-facing Eloquent struct in one shot- Full
Modeltrait -create,find,find_or_fail,find_many,all,query,save,update,delete,force_delete,refresh,fresh,replicate,replicate_into,increment/decrement,destroy,is/is_not,to_array/to_json - Fillable / guarded mass-assignment with
Attrsenvelope - 22 attribute casts - booleans, integers, floats, dates, enums, hashed, encrypted, JSON, collections, money, datetime with timezone
- Accessors / mutators via
#[suprnova::model] - Auto-timestamps (
created_at,updated_at) - Soft deletes (
deleted_at) withforce_delete,restore,trashed,only_trashed,with_trashed - Eleven relation kinds -
HasOne,HasMany,BelongsTo,BelongsToMany,HasOneThrough,HasManyThrough,MorphOne,MorphMany,MorphTo,MorphToMany,MorphedByMany - Per-family morph enums + morph registry with
APP_KEY_PREVIOUSrotation - Eager loading via
.with(...),.with_count(...),.load_missing(...) - Correlated EXISTS engine for
has/where_has - Sixteen lifecycle events (retrieving, retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored, force-deleting, force-deleted, replicating, trashed)
Observer<M>trait with per-method auto-registration via inventory- Local scopes via
#[scopes(M)], global scopes viaGlobalScope Collection<M>Laravel surface -pluck,key_by,group_by,where_in,first_where,contains_where,partition, etc.- Three paginators -
paginate(length-aware),simple_paginate,cursor_paginate- all serializing to Laravel-shape JSON chunk/lazy/cursorfor bulk-row iteration without OOMlock_for_update/shared_lockrow-level lockingDB::table(...)query builder withDynamicRowfor ad-hoc queriesDB::transaction(...)with savepoints, retry-on-deadlock, multi-connection read/write splitDB::listen(...)+QueryExecuted/TransactionBegan/TransactionCommitted/TransactionRolledBackeventsPrunabletrait +model:pruneconsole commanddump/ddquery-helper methods#[model(unique_id="...")]for UUID / ULID primary keys
Auth
Authenticatabletrait +EloquentUserProvider<M>Auth::attempt,Auth::login,Auth::user,Auth::user_or_fail,Auth::user_as<T>,Auth::logout,Auth::check- Multiple named guards (web session, API token)
- Email verification flow -
EmailVerification,EnsureEmailVerifiedMiddleware, signed verification URLs,EmailVerificationMail - Password reset flow -
PasswordReset, throttled tokens,PasswordChangedMail,PasswordResetLinkSentevent - Two-factor TOTP - enroll, verify, recovery codes, replay protection
- Brute-force / login throttle - IP + identifier keyed,
LoginThrottleMiddleware - Remember-me cookies with stable opaque tokens
- Six auth events -
LoginAttempted,LoggedIn,Authenticated,LoggedOut,PasswordResetLinkSent,EmailVerified - Browser sessions backed by the Torii fork at
github.com/eas4ai/suprnova-torii-rs
Authorization
Gatefacade -define,allows,denies,authorize,any,none,check(sync + async variants)#[policy(Model)]macro for policy registration- Resource-route auto-authorization
Payments
- Provider-agnostic five-trait surface -
Checkout,Payment,Subscription,CustomerStore,WebhookHandler PaymentProviderumbrella trait + capability-querying viaas_payment()- DB mirror -
customers,subscriptions,subscription_items,payments,refunds,payment_webhook_events(UNIQUE for idempotency) - Flow-tagged
SessionPayloadenum (one-shot vs subscription) - Two reference adapters as workspace crates -
suprnova-payments-stripe(gateway, fullPaymentimpl),suprnova-payments-paddle(Merchant of Record, noPaymentimpl) - Mock provider for tests
Queue, jobs, batches, chains
Jobtrait -handle,max_tries,backoff,timeout,fail_on_timeoutQueue::push,Queue::push_later,Queue::push_unique,Queue::push_unique_later- Drivers -
sync,null,redis,database JobMiddlewaretrait - six built-in middleware- Batches and chains -
Queue::batch(jobs).dispatch(), fluent chain builder, cancellation, progress tracking - Failed-jobs store with replay
- Worker with graceful shutdown, configurable concurrency, panic
recovery via
catch_unwind, settlement metrics - Twelve queue events covering queueing, processing, failure, release, worker lifecycle
Broadcasting and WebSockets
ws!()macro +Router::wsfor typed WebSocket endpointsWsSocketSink/Stream split- Auto-restart supervisors via
Supervisortrait BroadcastHubwithChannel,Private,Presencechannels- JSON-envelope protocol, presence join/leave/here, configurable presence TTL with crash recovery
Broadcastablebridge toEventDispatcher- Close-on-no-pong heartbeat with configurable WS_TASKS drain
- Per-route WebSocket middleware
- 1 MiB / 64 KiB safer defaults +
WsConfig::generous()factory - Origin policy + 1011 close-on-protocol-violation
Notifications and mail
Notificationtrait +Notify::send(recipient, notification).await- Mailable + Markdown template rendering
- Database / mail / broadcast / web-push channels
- VAPID signing + RFC 8291 ECE payload encryption (via
suprnova-web-push) - VAPID subject validation, retry-after parsing, 8 KiB rejection-body cap
- Notifiable trait for recipient typing
Events
- Typed event dispatcher -
EventFacade::dispatch,EventFacade::listen<E, L>,EventFacade::forget - Cancellable saving/updating events (return
EventResult::cancel) - Queueable listeners
Filesystem
Storage::disk("name")with multi-driver support - local, S3, Azure, GCS via OpenDAL- Move, copy, exists, size, mime, last-modified, prepend/append
- Streaming uploads and downloads
Cache
Cache::store("name")+ driver registration- Drivers - memory, redis (with bounded connect-timeout), database, file
remember,forever,tags, atomic increment/decrement, locks
Vector DB
VectorDrivertrait with four drivers - in-memory, Qdrant (UUID-5 ID mapping), Pinecone (native string IDs), MariaDB nativeVECTOR(N)+ HNSW indexes (11.7+)- Cosine / dot / euclidean distance
Console binary and CLI
- Per-project
consolebinary - Rust analogue ofphp artisan, runs user-defined commands via#[suprnova::console::command] #[derive(Command)]for typed argumentssuprnovaCLI -new,serve,migrate,db:sync,generate-types,key:generate,make:{controller,middleware,action,error,inertia,migration,task,command},db:seed,model:prune--versionflag- Scaffold templates for backend + API starters across three frontends
Feature flags
DatabaseEvaluatorwith snapshot loadingCachedEvaluatorwith TTLFeatureMiddlewareextractor- Admin CRUD surface
FeatureSynctrait for sub-second propagation across processes
Schedule
- Cron expression parser
Schedule::task(...)with composable predicates- Single-server locks, overlap prevention, dispatch tracking
schedule:runconsole command
Validation
validator0.20 integration#[request]+#[derive(FormRequest)]macros#[form_request(max_body_bytes = N)]per-form size cap#[form_request(custom_hooks)]opt-out for user-writtenimpl FormRequest- Lifecycle hooks -
authorize,after_validation,after_validation_async
Database drivers
- SeaORM-backed support for SQLite, Postgres, MySQL, MariaDB
- URL-based driver detection
- Migration system +
migrate,migrate:rollback,migrate:status,migrate:fresh,migrate:refresh
HTTP client
Httpfacade -get/post/put/patch/deletereturning aRequestBuilder;.send().awaitproduces aClientResponse- rustls TLS, 30s default timeout,
suprnova/<version>user-agent json/form/body/header/bearer_token/basic_auth/timeoutchainable methodsRequestBuilder::retry(max_attempts, base_backoff)- exponential backoff for transient failures and 5xx; respectsRetry-AfterHttp::fake(|| async { ... }).awaittest guard withfake_response(method, url_substring, status, body)+assert_sent/assert_not_sent
Encryption
Cryptstatic facade +EncryptionKey(crypto::*); AES-256-GCM with 12-byte random noncesencrypt_string/decrypt_string/encrypt<T>/decrypt<T>CryptPurposeAAD binding preventing cross-protocol replayAPP_KEY_PREVIOUSrotationsuprnova key:generateCLI command for minting fresh keys
Testing
#[suprnova_test]async test macroTestDatabase::fresh::<Migrator>()with parallel-safe instancesTestContainer::bindfor per-test mocks- HTTP test helpers -
Test::get,Test::post, JSON / form / multipart - Queue / Mail / Notification / Event fakes
assert_emitted,assert_dispatched,assert_dispatched_times
Changed
- Auth verification and password-reset flows now operate through the configured user provider instead of Torii internals.
- Generated apps must implement
get_auth_password; scaffolded examples now fail loudly instead of allowing login to always fail silently. - The local release gate is wired into
scripts/release.sh, and the repo includes an enforced pre-push hook for fmt, clippy, tests, docs, and feature builds. - Scaffolded dev-port documentation moved to the current backend/frontend
defaults (
8765/5765), withdev:tlsand--with-portlessdocumented. MAIL_FROMis validated before verification or reset tokens are issued, avoiding orphaned auth-flow rows when mail configuration is invalid.
Fixed
- React scaffold template drift from the released starter.
- Root route groups no longer generate duplicate
//paths. - Literal-path redirects now dispatch through the intended routing path.
- Broadcasting fanout tests now handle
track/untrackresults. - The mail log driver emits the rendered text body, so verification and password-reset links surface in local development logs.
- Password-reset coverage pins session and remember-me revocation behavior.
Notes
- Distribution model: git-based end-to-end.
suprnova = { git = "https://github.com/eas4ai/suprnova.git" }; CLI viacargo install --git. Nothing is published to crates.io.
