Skip to content

Releases: eas4ai/suprnova

Suprnova v1.3.2

Choose a tag to compare

@eas4ai eas4ai released this 25 Aug 15:54

Added

  • OAuth providers can now be registered through MagnetarConfig::oauth. Suprnova re-exports the OAuthProvider contract, all five first-party provider and configuration types, and the HTTP, revocation, abuse-limiter, authorization, and auto-link types an application needs. Custom providers no longer require a direct suprnova-magnetar dependency or a hand-retained MagnetarHostEngine.

  • A production OAuth transport and framework limiter adapter now ship at the crate root. ReqwestOAuthTransport implements token, userinfo, and revocation I/O with redirects disabled by default, a 30-second timeout, a default User-Agent, and a 1 MiB response cap. FrameworkAbuseLimiter reuses the configured RateLimiterDriver; apps no longer hand-write either adapter.

Fixed

  • init_magnetar now publishes OAuth with password and passkey services as one reserved installation. The OAuth service is built before publication, and all three engine slots remain hidden while the reservation is active. A failed or duplicate OAuth configuration cannot leave password and passkey state visible without the configured OAuth registry.

  • Custom providers can supply userinfo headers. OAuthProvider::userinfo_headers is merged with the host-owned bearer header, enabling requirements such as GitHub's User-Agent and media-type Accept headers without allowing a provider to replace Authorization.

Upgrading

  • The Magnetar cutover in 4faaa933 removed Torii's OAuth installation path without wiring its replacement into the default initializer. The old workaround required constructing a custom host engine, calling oauth_service, and installing the adapter separately. Replace that workaround with MagnetarConfig::from_sea_orm(database).oauth(oauth_config) and one init_magnetar call.

  • GitHub community providers must handle verified email explicitly. GitHub /user usually omits non-public email, while the verified primary address requires /user/emails. Return email: None to use the email-completion ceremony, or point userinfo_endpoint at a host adapter that combines both responses; never treat a public but unverified address as ownership.

Suprnova v1.3.1

Choose a tag to compare

@eas4ai eas4ai released this 25 Aug 00:14

Fixed

  • Provider-backed applications can reset verified users again. When no Magnetar engine is installed, PasswordReset uses an explicitly reset-capable UserProvider and framework auth_flow_tokens for already verified accounts. EloquentUserProvider<M> opts in when M implements MustVerifyEmail + CanResetPassword; no app_users migration 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.0 Git dependency to v1.3.1. Applications with their own users table keep their configured UserProvider; they do not initialize the default app_users engine 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

Choose a tag to compare

@eas4ai eas4ai released this 18 Aug 11:16

Security

  • The maintenance-mode bypass secret is compared in constant time.
    MaintenanceMiddleware matched 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 via subtle::ConstantTimeEq,
    short-circuiting only on a length mismatch - the same shape as the
    bypass-cookie compare next to it.

  • rules::Url now rejects script URIs. The rule accepted any scheme
    url::Url could parse, javascript: and vbscript: included, so a
    validated URL could still be a script-execution sink when rendered into
    an href. It now applies Laravel's url rule shape
    (Illuminate\Support\Str::isUrl's ^(PROTOCOLS)://HOST pattern): 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 the url crate 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's url: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 with Url::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; and file:///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-Inertia everywhere. 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 by Inertia::install as the
    outermost of the three - sets it on every response, and turns an empty
    200 on an Inertia visit into a 303 back rather than a response the
    client rejects as non-Inertia. InertiaVersionMiddleware now re-flashes
    the session before its 409, so a flashed error survives the client's
    follow-up full-page GET.

  • Three Inertia response fixes. InertiaResponse::location_for(&req, url)
    returns 409 + X-Inertia-Location for an Inertia XHR and a plain 302

    • Location for a hard navigation, so an OAuth or SSO bounce entered
      outside the SPA no longer dead-ends on a body-less 409. The existing
      location(url) keeps its always-409 shape. New App::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
      once prop 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", ...) and Mailable::headers() were dropped
    silently under MAIL_DRIVER=ses: the Content.Simple request body had no
    Headers field and the raw-MIME builder never read OutgoingMessage:: headers, even though every other transport forwards them. Both SES paths
    now carry them - Headers as 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 validated Vec<T> were
    dropped between the validator and the response: the request was correctly
    rejected with 422, but the errors map 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 url keeps the query string. page.url was
    the request path only, so the client recorded /users for 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
    InertiaVersionMiddleware already used for X-Inertia-Location, so by
    default the two agree byte for byte. New
    InertiaConfig::url_resolver(...) overrides how the page object names
    the page (Laravel's Inertia::resolveUrlUsing); the version bounce keeps
    naming the URL that arrived, because that is the URL the browser has to
    fetch.

  • Inertia::install now applies its config to every response. The
    config handed to Inertia::install was read for three fields and then
    dropped, so every InertiaResponse built without an explicit
    .with_config(...) rendered from InertiaConfig::default(). An app
    scaffolded with --frontend react served the Svelte entry point and no
    React refresh preamble unless SUPRNOVA_FRONTEND was 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 what InertiaResponse::new
    starts from. Per-response .with_config(...) still overrides, apps that
    never call Inertia::install are 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.rs written by suprnova new registered the session, locale,
    CSRF and include middlewares but never called Inertia::install, so a
    generated app had neither InertiaVersionMiddleware nor
    Inertia303Middleware: a browser still running the previous bundle was
    never told to reload after a deploy, and a PUT/PATCH/DELETE that
    redirected stayed on a 302 the client could follow with the original
    verb. The call now lands after SessionMiddleware - where the version
    middleware's session re-flash works - with a named INERTIA_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 .env now sets
    SUPRNOVA_FRONTEND to match. The --api starter is unchanged; it has
    no frontend.

  • Queue::push_unique no longer reports a queued job as skipped. The
    return value was computed with matches!(outcome, Idempotent::Fresh(())),
    which folded Idempotent::FreshUnfenced into false - 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 returns true with a warn naming the job and its unique
    key, and only a real duplicate returns false. push_unique_later and
    later_unique share 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 in manual/parity.md marked
    not yet or by 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::install now 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_title and .encrypt_history(...) set there
    also reach the page now.

  • rules::Url rejects more. Values that used to pass and no longer do:
    any scheme outside Laravel's allowlist, javascript: and vbscript:
    among them; mailto:, data: and tel:, which are on the allowlist but
    carry no :// host; and scheme:// with an empty host, such as
    file:///path. If you meant to accept a scheme, name it:
    Url::protocols(&["myapp"]).

Suprnova v1.2.3

Choose a tag to compare

@eas4ai eas4ai released this 17 Aug 01:00

Fixed

  • Datetime casts now read database-native CURRENT_TIMESTAMP text.
    AsDateTime, AsImmutableDateTime, and AsOptionalDateTime continue 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

Choose a tag to compare

@eas4ai eas4ai released this 17 Aug 01:01

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

Choose a tag to compare

@eas4ai eas4ai released this 10 Aug 00:05

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

Choose a tag to compare

@eas4ai eas4ai released this 08 Aug 19:00

Added

  • The manual ships in seven languages. manual/es/, manual/fr/,
    manual/de/, manual/pt-BR/, manual/ja/ and manual/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

Choose a tag to compare

@eas4ai eas4ai released this 02 Aug 20:03

Added

  • Per-locale fallback chains. LocalizationConfig gains parents
    (APP_LOCALE_PARENTS, comma-separated child=parent pairs, or the
    chainable .parent(child, parent) builder): a locale can inherit from a
    configured sibling before falling further back to the global
    fallback_localept-PT from pt-BR, en-AU from en-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
    Translator driver, 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>.ftl catalog as a
    fold — the embedded framework catalog at the bottom for en/en-*
    locales, then the locale's configured parent chain, then its own
    *.ftl files — 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_locale is still a Lang-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. See manual/localization.md's new "Fallback chains" section
    for the full contract.

Changed

  • LocalizationConfig gained the parents field. from_env() and
    the builder are unaffected; a literal struct constructor (tests
    building a LocalizationConfig by hand) needs one more field.
  • Served catalog text is now serializer-normalized for every locale,
    and intra-locale multi-file merging (several .ftl files 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

Choose a tag to compare

@eas4ai eas4ai released this 02 Aug 14:32

Added

  • Localization. Message catalogs in lang/<locale>/*.ftl
    (Fluent), a Lang facade 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-min plus 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 a field-<name>
    lookup. Rule::passes (and ContextualRule / AsyncRule) now return
    Result<(), ValidationMessage>; a custom rule's Err("…".into()) body
    still compiles and still renders verbatim, but the signature in your
    impl needs the new type.

    The browser gets the same bytes the server resolved: the merged catalog
    is served at /_suprnova/lang/<locale>.ftl with an ETag and an
    immutable ?v=<hash> form, the three starter kits parse it with
    @fluent/bundle, and suprnova generate-types emits a MessageKey
    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_choice here. Behind a default-on localization feature;
    --no-default-features still compiles and still validates, using the
    embedded English fallbacks.

  • IntoInertiaScroll for Paginator. The trait was implemented for
    LengthAwarePaginator and CursorPaginator but not for the simple
    paginator, so simple_paginate results could not feed
    Inertia::paginate at all — despite simple.rs's own module docs
    pointing at it as the URL-generation path. That left offset-paginated
    Inertia collections with a choice between a COUNT(*) per request and
    hand-rolling the scroll metadata. next_page comes from the
    LIMIT n+1 overflow probe rather than a computed last page, there
    being no total to compute one from.

Fixed

  • suprnova generate-types emitted a different file on every run.
    The topological sort seeded its work queue by iterating a HashMap,
    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_sort did 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

Choose a tag to compare

@eas4ai eas4ai released this 01 Aug 11:57

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:work and workflow:work ignored SIGTERM.
    Each selected on tokio::signal::ctrl_c() alone, which installs a
    SIGINT handler — so SIGTERM had no handler anywhere in the process, and
    SIGTERM is what docker stop, Coolify, systemd and Kubernetes send. All
    three already had a careful bounded drain behind that select!; none of
    it had ever executed under a supervisor. Measured before the fix: a
    docker stop on a queue:work container 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::run already 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 swapping QUEUE_DRIVER must not change whether a
    poison job can be stopped. attempts now means "deliveries to a worker"
    rather than "handler failures" — documented in manual/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. serve gets one from
    init_telemetry; queue:work, schedule:work, schedule:run and
    workflow:work come through a different boot path and got nothing, so
    every tracing:: line they emit went nowhere and LOG_LEVEL was 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
    what queue:retry re-pushes: the difference between work recoverable by
    hand and work that ceased to exist.

  • QUEUE_DRIVER=database now binds a failed-jobs store. failed_jobs
    is part of that driver's contract — queue:retry reads it and
    Queue::retry_failed cannot work without it — but bootstrap_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
    via QUEUE_FAILED_DB_TABLE. Only for this driver: memory is ephemeral
    by construction and redis has 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 5 really 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: each schedule:work process
    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_server keys 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=memory and a single-server task is refused, naming
    the offending tasks, with SCHEDULE_ALLOW_MEMORY_LOCK_IN_PRODUCTION=true
    for deployments that genuinely run one scheduler.

Changed

  • manual/deployment.md no longer says "run exactly one schedule: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.