CodeLogic.WebLogic.Testing 4.9.8-preview

This is a prerelease version of CodeLogic.WebLogic.Testing.
This package has a SemVer 2.0.0 package version: 4.9.8-preview+08a0dab.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package CodeLogic.WebLogic.Testing --version 4.9.8-preview
                    
NuGet\Install-Package CodeLogic.WebLogic.Testing -Version 4.9.8-preview
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CodeLogic.WebLogic.Testing" Version="4.9.8-preview" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CodeLogic.WebLogic.Testing" Version="4.9.8-preview" />
                    
Directory.Packages.props
<PackageReference Include="CodeLogic.WebLogic.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CodeLogic.WebLogic.Testing --version 4.9.8-preview
                    
#r "nuget: CodeLogic.WebLogic.Testing, 4.9.8-preview"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CodeLogic.WebLogic.Testing@4.9.8-preview
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CodeLogic.WebLogic.Testing&version=4.9.8-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.WebLogic.Testing&version=4.9.8-preview&prerelease
                    
Install as a Cake Tool

CodeLogic.WebLogic.Testing

Test helpers for apps built on CodeLogic.WebLogic.

WebLogicTestHost runs your WebLogic setup on a real Kestrel server bound to 127.0.0.1:0 with a cookie-aware HttpClient, so tests go through the full pipeline — security stages, CSRF, sessions, output cache — exactly like production requests. No ASP.NET TestHost package needed.

using CL.WebLogic.Testing.Services;

[Fact]
public async Task Anonymous_post_without_a_csrf_token_is_refused()
{
    await using var host = await WebLogicTestHost.StartAsync(new WebLogicTestHostOptions
    {
        ConfigureWebLogic = web =>
        {
            web.UseInMemoryAccounts();
            web.AddContributor(new DelegateContributor(b =>
                b.MapPage("/contact", (r, _) => ValueTask.FromResult<WebResult>(WebResult.Text("ok")), "POST").AllowAnonymous()));
        }
    });

    var response = await host.Client.PostAsync("/contact", new FormUrlEncodedContent([]));

    Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}

What's inside

  • WebLogicTestHost / WebLogicTestHostOptions: config, environment (test production behaviour), your WebLogic registrations, extra services and middleware; theme files via WriteContentFile.
  • DelegateContributor: map routes inline in a test.
  • TemplateTestRenderer + InMemoryTemplateLoader: render templates without a host.
  • ManualTimeProvider: control time for lockouts, token expiry and sessions.
  • WebTestAuthenticator: a software passkey authenticator; turns your passkey endpoints' options into the JSON a browser posts back, with real keys, signatures and attestation (none, packed, fido-u2f).
  • FakeWebLogicContext + RecordingLogger: assert on what the toolkit logged.

Works with any test framework. No third-party dependencies beyond CodeLogic.*.

Docs: https://zyntal-com.github.io/CL.WebLogic2/guides/testing.html

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.9.9-preview 0 10/27/2026
4.9.8-preview 0 10/27/2026
4.9.7-preview 19 10/27/2026
4.9.6-preview 21 10/27/2026
4.9.5-preview 26 10/26/2026
4.9.4-preview 19 10/26/2026
4.9.3-preview 20 10/26/2026
4.9.2-preview 25 10/26/2026
4.9.1-preview 28 10/25/2026

# Changelog

## 4.9.8-preview (2026-10-27)

### Fixed

- **Form binding matched posted names case-sensitively.** A field named after the property (`Image`,
 `Email`) did not bind from a client posting `image` or `email`: the field stayed empty, so a
 `[Required]` field failed and an optional one was silently ignored. Posted names now match
 case-insensitively, in `BindAsync`, remote field validation, wizard steps and repeater items (file
 fields already did). If a name is posted twice in different case, the values are merged in posted
 order and a single-value field takes the first, instead of the post failing. The internal `__wl_*`
 and CSRF fields are recognised in any case too.

### Changed

- A form model whose fields differ only in case (`Name` and `name`) is refused when it is registered
 (`InvalidOperationException`), because those fields could no longer be told apart.

## 4.9.7-preview (2026-10-27)

### Added

- `WebHtmlSanitizer`: opt-in `style` declarations and embed iframes, for rich text that resizes images or
 embeds videos.
 - `WebHtmlSanitizerOptions.AllowedStyleProperties`: property name → value check. The attribute is
   rebuilt from the allowed declarations only. Values with comments, escapes, quotes, `url()` or any
   function other than `rgb()`/`rgba()`/`hsl()`/`hsla()` are refused before the check, as are
   unknown properties; `!important` is stripped.
 - `WebCssValues`: `Length`, `Percentage`, `Color`, `OneOf(...)`, `Any(...)` value checks.
 - `WebHtmlSanitizerOptions.AllowedFrameSources` (`WebFrameSource(host, pathPrefix)`): https only,
   exact host, default port, no user info. A kept iframe is rebuilt with `src`, `width`, `height`,
   `title`, `loading` and the global attributes, plus `FrameSandbox`, `FrameAllow`, `allowfullscreen`
   and `referrerpolicy="strict-origin-when-cross-origin"`; its fallback content is dropped.
 - Defaults are unchanged: no `style` attributes, no iframes. Add the frame hosts to the CSP
   (`security.headers.frameSources`) as well.

## 4.9.6-preview (2026-10-27)

### Security

- New-device e-mails no longer carry the raw `User-Agent` header. When the browser was not
 recognised, the header (chosen by the client) went into the mail as the `device` value, and a CL.Mail
 HTML template using it could be injected into. Unrecognised browsers are now named "an unknown
 browser".

### Added

- `WebSignInResult.Error`: when `CompletePasswordChangeAsync` refuses the new password, the result keeps
 the token and carries the password policy's message ("Use at least 10 characters.", "The password
 must not contain your user name or e-mail."), so the page can say why.

### Changed

- `UseUserStore<T>()` and the other `Use…<T>()` registrations add `T` as a singleton of its own type and
 resolve the contract to that same instance. Apps can inject their store by type (for app-specific
 methods) without casting `IWebUserStore`, and a class registered for several contracts is one instance.

## 4.9.5-preview (2026-10-26)

### Added

- Step-up with alternatives: `RequireRecentSignInAnyOf(maxAge, ["pwd", "otp"], ["passkey"])` on routes,
 groups and policies accepts any one complete set of methods. `RecentSignInRequirement.Alternatives`
 replaces `Methods`, and `WebRouteSettings.RecentSignInAlternatives` replaces `RecentSignInMethods`.
- `WebUserAgent`: browser and system family of a user agent ("Chrome on Windows"), and
 `WebSessionInfo.Device`.
- `Realtime.SessionCheckSeconds` (30) and `SessionEndedEvent`.

### Fixed

- A revoked session kept its realtime connection. Signing a device out, the per-user session cap and
 password-reset sign-outs ended the session, but its SSE/WebSocket stream stayed open for up to
 `MaxConnectionMinutes` (60) and kept receiving that user's messages. Connections now record their
 session and close when it ends: at once on the same node, and within `SessionCheckSeconds` on others.
- New-device e-mails compared the exact user-agent string, so every browser update was mailed as a new
 sign-in. They now compare browser and system family and name the device ("Chrome on Windows").

### Changed

- CodeLogic.WebLogic.MySql uses only CL.MySQL2's typed API (now CodeLogic.MySQL2 4.9.101): the add-on
 contains no SQL text. Rate limits refill and charge a bucket with one atomic update-and-read, a
 sign-in failure is one upsert, a fresh one-time key is one round trip, and recovery code sets are
 replaced in one transaction.

## 4.9.4-preview (2026-10-26)

### Added

- Passkeys (WebAuthn), verified in-house: CBOR, COSE keys (ES256/384/512, RS256, PS256), authenticator
 data, signatures, and the `none`, `packed` and `fido-u2f` attestation formats.
 - **Three uses:** passwordless sign-in (`request.Auth.PasskeySignInAsync`, multi-factor, so no TOTP
   step), the second factor after a password (`CompleteTwoFactorWithPasskeyAsync`), and step-up
   (`ReauthenticateWithPasskeyAsync`).
 - **Registration and management:** `BeginPasskeyRegistrationAsync` and `CompletePasskeyRegistrationAsync`,
   plus list, rename and delete. Users are e-mailed when a passkey is added or removed.
 - **Safety:** ceremony state is sealed and each challenge is single-use (works across nodes).
   Origin, RP id, user presence and verification, backup flags and the user handle are checked.
   A signature counter that goes backwards refuses the sign-in (`PasskeyCloneSuspectedEvent`).
 - **Lockout:** the password lockout neither blocks passkeys nor counts passkey failures, because
   credential ids are not secret. Failures are throttled instead.
 - **Wiring:** `IWebPasskeyStore` (`web.UsePasskeyStore<T>()`, an in-memory store with
   `UseInMemoryAccounts`) and `Auth.Passkeys` configuration (RP id, origins, timeout, attestation,
   second factor without PIN, maximum per user).
- Client: `WebLogic.passkeys` (`signIn`, `register`, `verify`, passkey autofill) and declarative
 `data-wl-passkey` buttons.
- Testing: `WebTestAuthenticator`, a software authenticator for testing passkey endpoints. The suite
 also runs the client against a headless-Chrome virtual authenticator.
- CodeLogic.WebLogic.MySql: `wl_passkeys` (`WebMySqlStores.Passkeys`, included in `All`), with an
 atomic counter update.
- Starter sample: passkey sign-in with autofill, passkey as the second factor, and a Passkeys card on
 the account page.

### Fixed

- A suspended user's reason and end date were lost on the way back from password, magic-link and
 external sign-in (`WebSignInResult.Suspension` was null), so the suspension page could not show them.

## 4.9.3-preview (2026-10-26)

### Added

- App-managed IP rules and blocklists: `IWebIpRuleProvider` (`web.AddIpRuleProvider<T>()`) returns
 `WebIpRule`s (deny, allow or exempt; network or country; expiry; reason) merged with
 `Security.IpRules`, and `IWebReputationListProvider` returns DNSBL zones that replace the CL.NetUtils
 services. Cached for `IpRules.ProviderCacheSeconds` (60); `IWebSecurityRulesInvalidator.Invalidate()`
 applies edits at once; a failing provider keeps the last good rules. Exempt addresses skip the
 allow-list, country rules and the DNSBL check.
- `WebSecurityRejectedEvent.RuleId` (and `WebSecurityDecision.RuleId`): the matched rule
 (`ban-42`, `config.deny[0]`, `dnsbl:{zone}`) for hit counters, never the client address.
- DataProtection key ring in CL.Storage: `AddDataProtection().PersistKeysToCodeLogicStorage(connectionId, path)`
 (one file per key, never overwritten, storage errors fail instead of minting new keys), and
 `ProtectKeysWithSecret(variable, previousVariables...)` to encrypt keys at rest with AES-256-GCM
 from an environment secret, with rotation. Cluster checks accept the storage repository and warn
 about a plain folder.
- Plugin assets: `b.MapAssets(assembly, "assets/", name)` serves embedded files at
 `/_weblogic/plugins/{name}/...` (ETag, extension allowlist, path checks, removed with the plugin);
 `{asset:name:path}` gives a fingerprinted URL, and a theme file at `assets/plugins/{name}/{path}`
 overrides it.
- Group layouts: `WithLayout("admin/layout")` on routes and groups, used by page templates that
 declare no `{layout:...}` (`TemplateRenderRequest.DefaultLayout`).
- Account suspension: `WebUser.SuspendedUntil` and `SuspensionReason`. Sign-in returns
 `WebSignInStatus.Suspended` with `WebSignInResult.Suspension` (only after the credentials were
 proven); sessions end at the next stamp check and API keys stop working.
 `WebResult.AccountSuspended(suspension)` renders the theme's `errors/suspended` page (plain fallback,
 ProblemDetails for API clients). Both samples handle it; MiniBlog adds two user columns.
- Git theme credentials in `ThemeConfig`: `GitTokenEnvironmentVariable` (or the secret `GitToken`),
 `GitUserName`, `GitSshKeyPath` and `GitSshPassphraseEnvironmentVariable`.
- `WebThemeChecker.Check(folder)` lists templates that do not parse (file, line, column), and
 `tools/CL.WebLogic.ThemeCheck` runs it from the command line. CI checks both sample themes.

- New package **CodeLogic.WebLogic.MySql**: production stores on CL.MySQL2 for every contract
 (users with lockout, e-mail and external logins; sessions with device listing; API keys; recovery
 codes; one-time keys; login throttle; rate limits; audit; widget settings; dashboards) and the
 DataProtection key ring. `web.UseMySqlStores(connectionId, o => o.Stores = ...)`,
 `AddDataProtection().PersistKeysToMySql(connectionId)`. Schema sync at startup, a background sweep,
 and cross-node guarantees (single-use consumes, atomic lockout, exact rate-limit capacity, versioned
 dashboards). Tested against MySQL 8.4 in CI.
- New package **CodeLogic.WebLogic.ExternalLogins**: `AddSteamLogin()` (moved from the starter
 sample), `AddOAuthLogin(options)` for any OAuth 2.0 / OpenID Connect provider (authorization code +
 PKCE S256, nonce, discovery with issuer check, in-house JWT validation for RS/PS/ES 256-512 with
 JWKS rotation, `iss`/`aud`/`azp`/`exp`/`nbf`/`iat`, RFC 9207 `iss` check), and presets
 `AddGoogleLogin`, `AddMicrosoftLogin`, `AddGitHubLogin`, `AddDiscordLogin`.
- `WebExternalChallenge.CodeVerifier` and `CodeChallenge`: `WebExternalLoginFlow` now creates a PKCE
 verifier per sign-in and keeps it in its protected cookie.

### Changed

- Releases are manual only: the `promote.yml` workflow that merged `main` into `release` after green
 tests is removed. Publish with `git push origin main:prerelease` or `main:release`.
- `IpRulesStage` and `ReputationStage` take a `WebSecurityRules` (resolved from DI).
- MiniBlog uses `CodeLogic.WebLogic.MySql` for its stores and key ring (encrypted when
 `MINIBLOG_KEYRING_SECRET` is set); its own user, session, recovery-code, widget and dashboard
 stores are gone. The starter sample uses `CodeLogic.WebLogic.ExternalLogins` (Steam, plus Google,
 Microsoft, GitHub and Discord when `STARTER_{PROVIDER}_CLIENT_ID` and `_CLIENT_SECRET` are set).

## 4.9.2-preview (2026-10-26)

### Added

- `request.Auth.SignInExternalAsync(user, provider)`: sign-in for users an external provider vouched
 for. Applies the account rules (disabled, locked, other tenant) and asks users with two-factor
 authentication for their code, like a password sign-in. Recorded as `ext:{provider}`.
- Per-area sign-in pages: `WithLoginPath(path)` and `WithAccessDeniedPath(path)` on routes and
 groups (inherited; local paths only). Signed-in users without access are sent to the denied page
 instead of a 403 (pages only).
- Step-up: `RequireRecentSignIn(maxAge, methods...)` on routes and groups, and on policies
 (`RecentSignInRequirement`). Failing it asks the user to sign in again (`reauth=1` redirect, or
 `401` with `error="reauthentication_required"` for APIs) instead of forbidding.
 `request.Auth.ReauthenticateAsync(password, code)` proves the user again; sessions and
 `WebIdentity` record `AuthenticatedAt`.
- "Remember me": a `persistent` flag on every sign-in method, carried through the second factor
 and a required password change. `Session.PersistentIdleTimeoutDays` (30) and
 `Session.PersistentAbsoluteTimeoutDays` (90).
- Sessions as devices: records keep the user agent, authentication time and "remember me"; the
 optional store capability `IWebSessionListStore` enables `request.Auth.ListSessionsAsync()`,
 `RevokeSessionAsync(idHash)`, `Session.MaxPerUser` and new-device e-mails
 (`Auth.Notifications.NotifyNewDevice`, notification kind `NewDeviceSignIn`). The in-memory store
 and both samples implement it; the starter's account page lists signed-in devices.
- CSP: `MediaSources`, `WorkerSources`, `ManifestSources`, `FormActions` and `CspReportTo`
 (`Reporting-Endpoints` + `report-to`). Per-route additions with `WithCsp(c => c.AddFrameSource(...))`
 (validated: hosts, schemes and `'self'` only), built once per route.
- The explorer shows a route's login path, step-up requirement and CSP additions.
- `CodeLogic.WebLogic.Testing` ships a package readme (usage example and helper overview); the
 `CodeLogic.WebLogic` readme gains a quick start and links to the docs.

### Changed (breaking)

- `request.Auth.PasswordSignInAsync`, `SignInAsync` and `SignInWithMagicLinkAsync` take
 `persistent` before the cancellation token: callers passing the token positionally must name it
 (`cancellationToken: ct`). The matching `WebAuthService` methods changed the same way.
- Sessions signed in without "remember me" now use a browser-session cookie (no `Expires`); before,
 every session cookie lasted until the absolute timeout.
- Magic-link sign-in refuses users of another tenant, like password sign-in.
- `WebAuthService` takes a `WebSessionDevices` (resolved from DI).

### Fixed

- CI: golden template snapshots are found under deterministic (`/_/`) build paths, the Markdown
 pathological-input test compares scaling instead of wall-clock time, and parallel Docker builds
 lock the shared NuGet cache.

## 4.9.1-preview (2026-10-25)

First preview of the rebuild on nuget.org.

### Added

- New repository: CL.WebLogic rebuilt from the ground up on CodeLogic 4.9 and published as
 `CodeLogic.WebLogic`. Breaking with the 0.x line — see `docs/guides/migrating.md`.
- `WebLogicLibrary` implements `IConfigReloadable`: `weblogic` config changes are applied live
 through `WebLogicConfigHolder`; restart-only fields are flagged (`WebLogicRestartRequiredEvent`,
 Degraded health) instead of failing the change.
- `IWebLogicContext` decouples toolkit services from the static CodeLogic runtime.
- `CodeLogic.WebLogic.Testing` package with `FakeWebLogicContext` and `RecordingLogger`.
- Hosting: `AddCodeLogicWebLogic()` / `UseCodeLogicWebLogic()` / `MapCodeLogicWebLogic()`. Every
 WebLogic route is a real ASP.NET endpoint (templates and constraints such as `{id:int}`, 405 with
 `Allow`, HEAD for GET routes, per-endpoint CORS) and mixes freely with minimal APIs.
- Routing: `IWebContributor.ContributeAsync(WebContributionBuilder, ct)` with `MapPage`, `MapApi`,
 `MapFragment`, `MapSse`, `MapFallback` and nested `MapGroup` prefixes whose options (anonymous
 access, groups, permissions, policies, CORS, rate limit, body limit, middleware) are inherited.
 Routes are **secure by default**; anonymous routes are listed at registration.
- Contributions are all-or-nothing and removable: `WebContributionHandle`, `OnRemove` cleanups,
 ambiguity detection, a single fallback. CodeLogic plugins implementing `IWebContributor` are
 registered on load and removed on unload.
- Results: `Html`, `Text`, `Json` (per-contributor serializer options), `Bytes`, local-only
 `Redirect` / explicit `ExternalRedirect`, `File` (ranges, ETag), `Problem` / `ValidationProblem`
 (RFC 7807), `NoContent`, `Status`, `Commands` (allowlisted client commands), `Sse`, `FromAspNet`.
- `WebRequestContext` per request (no static accessor) with bounded body readers (413 above the
 route limit), typed route values and cancellation on client disconnect.
- Exception boundary: correlation ids, safe 500 pages (details only in Development), ProblemDetails
 for APIs, bodies for empty 4xx/5xx responses, client disconnects logged at Debug.
- Health probes `/_weblogic/health/live` and `/_weblogic/health/ready`.
- Theme assets served only from the theme's `assets` folder with path containment, dotfile and
 symlink refusal, an extension allowlist, ETag / Last-Modified and immutable caching for `?v=` URLs.
- Templates: the AST template engine, ported and split into small types. Two engines share one set
 of execution semantics (`TemplateOps`): `ClosureTemplateCompiler` (default; compiles a template
 once into delegates) and `TemplateInterpreter` (the reference AST walk), selected with
 `Theme.TemplateEngine`. The legacy regex engine is gone.
- Context-aware output encoding: the parser infers whether each value lands in element text, a
 quoted attribute, a URL attribute (scheme allowlist: http, https, mailto, tel), a `style`
 attribute, `<script>` or `<style>`. Unsafe positions (inside a tag, unquoted attributes, `on*`
 handlers, script values without `|json`/`|js`) are template **parse errors**. New filters
 `|json`, `|js`, `|attr`, `|css`, `|url`, `|markdown`, `|sanitize`, `|join`; `|nl2br` now encodes
 first. `WebHtml` marks trusted markup.
- CSP nonces: `{nonce}`, and static `<script>` / `<style>` tags get a per-request nonce
 automatically. New directives `{asset:path}` (fingerprinted, immutable-cacheable URLs),
 `{t:key}` (`IWebTextLocalizer`), `{weblogic_scripts}`; `{foreach}` gains `{empty}`; `{item}` /
 `{alias}` write the current item.
- v1 theme syntax keeps working: `{{key}}` / `{{page.x}}` / `{{item.x}}` tokens (now encoded for
 their context), `{if:accessgroup:…}`, `|uppercase` / `|lowercase`, `a→b` replacements; unknown
 filters still pass the value through.
- Layouts nest, sections defined closer to the page win, partials take parameters, and recursion /
 depth (32) are guarded. `WebResult.Page(...)` and `WebResult.Fragment(...)` (no layout,
 `X-WebLogic-Fragment`).
- `{renderhead}` from `WebPageMeta`: title, description, robots, keywords, canonical, hreflang
 alternates, Open Graph (incl. article times, image alt), Twitter cards and safely serialized
 JSON-LD.
- Theme templates load from the active theme with containment and link refusal; cached compiled
 templates revalidate by timestamp, then source hash, at most every
 `Theme.TemplateChangeCheckSeconds`.
- Themed error pages: `errors/{status}.html`, then `errors/error.html`, falling back to the
 built-in page; API / JSON clients still get ProblemDetails.
- Theme sources: Local, **Git** (CL.GitHelper; clone + fast-forward into `{data}/themes/{id}`) and
 **Storage** (CL.Storage; atomic directory download). `ThemeSyncService.RefreshAsync()` re-syncs on
 demand; a failed update keeps serving the last good copy. `ThemeSyncedEvent`.
- Content: our own `WebMarkdownRenderer` (CommonMark + GFM subset: headings, emphasis,
 strikethrough, code, fenced/indented code, quotes, nested lists, tables, links, images,
 autolinks; raw HTML is written as text; URLs scheme-checked) and `WebHtmlSanitizer` (allowlist,
 re-serialized from our own `HtmlTokenizer`, always balanced, `rel` on links), both covered by XSS
 corpora.
- Template hooks for later milestones are DI contracts with safe defaults: `ITemplateCsrfProvider`,
 `ITemplateWidgetRenderer`, `ITemplateScriptProvider`, `ITemplateAuthorizer`,
 `ITemplateGlobalsProvider`, `ITemplateFilter` (`web.AddTemplateFilter<T>()`).
- Testing: `TemplateTestRenderer` / `InMemoryTemplateLoader` render templates in unit tests
 without a host; golden snapshots run on both engines.

- Security pipeline: ordered `IWebSecurityStage`s in two phases (pre-routing and endpoint),
 registered through DI, validated at startup (unique names, required order and phase) and
 switchable by name (`Security.DisabledStages`, logged). Rejections are audited and published as
 `WebSecurityRejectedEvent` (no client addresses).
- Stages: forwarded headers (off by default, explicit proxies / CIDRs only, hop limit, safe
 `X-Forwarded-Prefix`), host filter and canonical-host redirect, HTTPS redirect to the
 `PublicBaseUrl` host (never the Host header), tenant (`IWebTenantResolver`), IP and country rules
 (CL.NetUtils GeoIP, cached), DNSBL reputation (CL.NetUtils; blocking with a time budget or
 background), global and per-route token-bucket rate limiting (`429` with `Retry-After` and
 `RateLimit-*`; unknown policies fail closed), authentication, request limits (`413`, form field
 limits), authorization and CSRF.
- Response hardening applied when the response starts: nonce-based CSP with `'strict-dynamic'`
 (configurable sources, report-only), HSTS on HTTPS, nosniff, frame protection, Referrer-,
 Permissions-, COOP and CORP policies, `no-store` for signed-in users and CSRF-bearing responses.
 Handlers cannot weaken them except through `request.Security.OverrideHeader` (logged).
- Sessions: one lazy session per request (nothing stored, no cookie until written), `__Host-`
 cookie over HTTPS, token stored only as a SHA-256 hash, idle and absolute timeouts, optional IP
 binding, rotation on sign-in / sign-out / privilege change, sign out everywhere, a sweeper, and
 the `IWebSessionStore` contract (`web.UseSessionStore<T>()`).
- Identity: `WebIdentity` (scheme, ambient flag, groups, permissions, scopes, tenant, amr),
 `IWebAuthenticationHandler` chain (session cookie built in; API keys / bearer / trusted header
 come with the auth milestone), `WebLogic` ASP.NET scheme so `RequireAuthorization()` works on
 minimal APIs.
- Authorization: routes are secure by default; groups, permissions (wildcards, resolved lazily
 through `IWebPermissionResolver`), named policies (`web.AddAuthorizationPolicy`), custom and
 resource-based requirements, `IWebAuthorizationService` for handler code. Pages redirect to
 `Security.LoginPath` with a safe `returnUrl`; APIs get `401` + `WWW-Authenticate`; unknown
 policies deny.
- CSRF v2: session-bound synchronizer tokens (DataProtection-protected pre-session cookie before
 sign-in), masked per render, enforced for every unsafe request to WebLogic routes including
 anonymous ones and the fallback, plus `Sec-Fetch-Site` and `Origin`/`Referer` checks; tokens
 rotate on sign-in / sign-out; `GET /_weblogic/csrf` for SPAs; `{csrf}` in templates.
- Audit: bounded background queue to an app-supplied `IWebAuditSink`; client IPs are HMAC hashed.
- `DevelopmentStoreGuard` refuses to start outside Development with in-memory security stores
 unless `Security.AllowDevelopmentStores` is set.
- `request.Identity`, `request.Session`, `request.Security`; `WebUrl.SafeReturnUrl`.
- Testing: `ManualTimeProvider`; the test host allows development stores by default.

- Auth flows (`Auth/`): `request.Auth` with password sign-in, TOTP and recovery-code second
 factor, magic links, required password change, sign-out (everywhere), refresh and password
 change; `WebSignInStatus` results that never reveal whether an account exists.
- Users: `WebUser` + `IWebUserStore` with optional lockout, e-mail and external-login
 capabilities; null default store, `web.UseUserStore<T>()`, `web.UseInMemoryAccounts()` for
 development.
- Passwords: own PBKDF2-SHA256 hasher (`$wl$pbkdf2-sha256$v=1$i=…`, 600k iterations, optional
 pepper, automatic rehash, pluggable legacy algorithms), NIST-style `WebPasswordPolicy`,
 per-login exponential back-off and per-IP cap (`IWebLoginThrottle`), account lockout.
- Two-factor: enrollment with QR code via CL.TwoFactorAuth, secrets stored DataProtection-
 protected, every time step accepted once, hashed single-use recovery codes
 (`IWebRecoveryCodeStore`).
- Account lifecycle (`WebAccountService`): registration, e-mail verification, password reset,
 e-mail change (confirmed at the new address, old address told), magic links. Tokens are
 DataProtection-protected, purpose-bound, time-limited, bound to the security stamp and
 single-use (`IWebSecurityTokenService`, `IWebOneTimeUseStore`).
- Security stamps: credential changes end other sessions (checked every
 `Auth.SecurityStampCheckMinutes` through the new `IWebSessionValidator` hook); password resets
 end all sessions at once.
- Notifications: background `WebNotificationDispatcher`; `MailSecurityNotifier` (CL.Mail
 templates `weblogic.{kind}` with built-in fallback texts), development outbox writing `.eml`
 files, null notifier by default.
- API authentication: API keys `wl_{prefix}_{secret}` (SHA-256 at rest, constant-time check,
 scopes, groups, tenant, expiry, revocation, allowed networks, per-key rate policy) via
 `X-Api-Key` / `Authorization`; app-validated bearer tokens (`web.AddBearerTokens<T>()`);
 trusted proxy headers (trusted peer + shared secret only); ASP.NET schemes such as Negotiate
 (`web.AddAspNetAuthenticationScheme`).
- External login: `IWebExternalLoginProvider` + `WebExternalLoginFlow` (protected state/nonce
 cookie, safe return URL, account linking).
- Tenancy: `web.UseHostTenants(map)`; users, sessions and API keys of another tenant are
 rejected.
- Events: `UserSignedInEvent`, `UserSignedOutEvent`, `SignInFailedEvent`, `UserLockedOutEvent`,
 `UserRegisteredEvent`, `PasswordChangedEvent`, `EmailConfirmedEvent`, `TwoFactorChangedEvent`,
 `RecoveryCodeRedeemedEvent`, `ApiKeyChangedEvent`.
- `DevelopmentStoreGuard` now also covers the account stores and the outbox.

- Client runtime v2 (`Client/src`, vanilla ES2022, no npm dependencies, CSP-safe): config from
 `#wl-config`, event bus, `http` (same-origin-only CSRF header with lazy token fetch and one
 retry after rotation, keyed cancellation, timeouts, ProblemDetails → `HttpError`), safe `dom`
 helpers, page `modules` (mount/unmount), allowlisted `commands`, ajax `forms` (schema-driven
 client validation, double-submit guard, XHR upload progress, form results), `widgets`
 (`data-wl-action` / `data-wl-refresh`), `realtime` (SSE or WebSocket, back-off with jitter,
 last-event-id resume), partial `navigation` for `[data-wl-shell]` pages (head/title swap,
 history, focus and announcements, full-load fallback), and a pluggable `ui` with `plain` and
 `bootstrap5` adapters. `Client/weblogic.d.ts` ships in the package.
- Our own bundler (`tools/CL.WebLogic.ClientBundler`): exact JS lexer (strings, templates,
 regex vs division, comments), strict module subset, dependency-ordered linking into ESM and
 classic bundles, comment stripping, Brotli/gzip, content hashes and SRI. Runs incrementally
 from MSBuild; output is embedded in `CL.WebLogic.dll`.
- Serving: `/_weblogic/client/{name}.{hash}.{ext}` (immutable, precompressed, ETag/304);
 `{weblogic_scripts}` writes the config block, stylesheet and scripts with SRI and the CSP
 nonce (no CSRF token, so pages stay cacheable). New `Client` config section (adapter, format,
 navigation, stylesheet, test page).
- Head meta written by `WebPageHeadRenderer` is marked `data-wl-head` so navigation can swap it.
- Tests: bundler unit tests, `node --test` suites for the DOM-free modules, and a browser suite
 at `/_weblogic/tests` (Development) that the integration tests run in headless Chrome. CI gains
 a client job; the release pre-flight checks the client is embedded and the bundler is not a
 dependency.
- Forms v2 (`Forms/`): definitions from DataAnnotations (`Required`, `StringLength`, `Range`,
 `RegularExpression` with a 1 s timeout, `EmailAddress`, `Url`, `Phone`, `Compare`, `DataType`,
 any other `ValidationAttribute`, `IValidatableObject`) plus `[WebForm]`, `[WebField]`,
 `[WebOptions]`, `[WebShowWhen]`, `[WebFile]`, `[WebRepeater]`, `[WebStep]` and `[WebIgnore]`;
 fluent `IWebFormConfigurator<T>`; definitions cached per type in a `ConditionalWeakTable`.
- `request.Forms.BindAsync<T>()`: binds only defined fields (no over-posting), invariant browser
 formats, real checkboxes, multi-value fields, indexed repeaters (`Lines[0].Product`), hidden
 conditional fields skipped, rich text sanitized, fixed and provider choices checked, async
 cross-field `IWebFormValidator<T>`s. Messages go through `IWebTextLocalizer` (`forms.*` keys).
- `FormResult`: the client's JSON result (`ok`, `formId`, `errors`, `summary`, `message`,
 `redirect`, `commands`, `data`, `step`; 422 for errors); plain posts get a 303 on success and the
 fallback page re-rendered with values and errors.
- Rendering: `IWebFormRenderer` with `bootstrap5` (default) and `plain` renderers, native
 validation attributes, ARIA (`aria-invalid`, `aria-describedby`, alert summary), the client
 schema block, CSRF, honeypot and timestamp fields; theme overrides per field type
 (`forms/fields/{type}.html`). `{form:id action=… class=… submit=… renderer=…}` template
 directive (both engines); `request.Forms.Prefill(model)` / `RenderAsync<T>()`.
- Anti-bot without third parties: honeypot plus a protected render timestamp (minimum and maximum
 age); `FormBotRejectedEvent`, `FormSubmittedEvent`.
- Multi-step forms: progress in a DataProtection-protected token (stateless, cluster-safe);
 earlier steps can't be altered by later posts; Back button; works without script.
- Uploads: count and size limits, extension allowlist, content sniffing that must match the
 extension, our own `ImageHeaderReader` (PNG, JPEG, GIF, WebP, BMP dimensions), cleaned display
 names; `IWebUploadSink` with a CL.Storage-backed default that stores under random names.
- Endpoints, always registered: remote field validation
 (`POST /_weblogic/forms/{form}/validate/{field}`, CSRF) and options/search
 (`GET /_weblogic/forms/options/{provider}`, anonymous or policy per provider), rate limited
 by `Forms.EndpointRatePolicy`. `WebFormHttpLookupProvider` takes an absolute https URL template
 fixed in code and calls only hosts in `Forms.AllowedLookupHosts` (the old Host-header SSRF is

---

Release notes truncated to fit NuGet's 35 000 character limit. The complete changelog ships inside this package as CHANGELOG.md.