CodeLogic.WebLogic 4.9.8-preview

This is a prerelease version of CodeLogic.WebLogic.
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 --version 4.9.8-preview
                    
NuGet\Install-Package CodeLogic.WebLogic -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" 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" Version="4.9.8-preview" />
                    
Directory.Packages.props
<PackageReference Include="CodeLogic.WebLogic" />
                    
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 --version 4.9.8-preview
                    
#r "nuget: CodeLogic.WebLogic, 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@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&version=4.9.8-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.WebLogic&version=4.9.8-preview&prerelease
                    
Install as a Cake Tool

CodeLogic.WebLogic

Web application toolkit for CodeLogic 4.9.

  • Real ASP.NET Core endpoints for pages, APIs, fragments and SSE — mixes freely with minimal APIs
  • An ordered, extensible security pipeline (proxy trust, hosts, HTTPS, IP rules, reputation, rate limits, authentication, policy authorization, CSRF, response hardening, audit)
  • Auth flows: password sign-in with lockout, passkeys (WebAuthn, verified in-house), TOTP 2FA with recovery codes, magic links, API keys, bearer tokens, external providers, tenants — on stores your app supplies
  • Template engine with context-aware encoding, CSP nonces, Markdown and an HTML sanitizer
  • Model-driven forms: validation, conditional fields, repeaters, wizards, uploads, anti-bot
  • Widgets, dashboards, realtime (SSE / WebSocket), output and fragment caching, API explorer
  • Plugins contribute routes, forms, widgets and templates; cluster-mode startup checks
  • A vanilla JS client runtime with no dependencies
var builder = WebApplication.CreateBuilder(args);
builder.AddCodeLogicWebLogic(web =>
{
    web.UseUserStore<MyUserStore>().UseSessionStore<MySessionStore>();
    web.AddForm<ContactForm>();
    web.AddContributor<SitePages>();
});

var app = builder.Build();
app.UseCodeLogicWebLogic();
app.MapCodeLogicWebLogic();
await app.RunAsync();

WebLogic is a toolkit: it ships contracts and helpers, and your application owns persistence. Routes require sign-in unless they opt out. No third-party dependencies beyond CodeLogic.*.

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 (3)

Showing the top 3 NuGet packages that depend on CodeLogic.WebLogic:

Package Downloads
CodeLogic.WebLogic.Testing

Test helpers for CodeLogic.WebLogic apps — an in-process Kestrel test host and fakes for the toolkit context. No third-party dependencies beyond CodeLogic.*.

CodeLogic.WebLogic.ExternalLogins

External sign-in for CodeLogic.WebLogic: Steam OpenID 2.0 and OAuth 2.0 / OpenID Connect (authorization code + PKCE, nonce, issuer and audience checks, in-house JWT/JWKS validation) with Google, Microsoft, GitHub and Discord presets.

CodeLogic.WebLogic.MySql

MySQL stores for CodeLogic.WebLogic on CL.MySQL2: users, sessions, API keys, recovery codes, one-time use, login throttle, rate limits, audit, widget settings, dashboards and the DataProtection key ring — with schema sync and expiry sweeps.

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 26 10/27/2026
4.9.5-preview 27 10/26/2026
4.9.4-preview 22 10/26/2026
4.9.3-preview 24 10/26/2026
4.9.2-preview 23 10/26/2026
4.9.1-preview 32 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.