CodeLogic.WebLogic 4.9.2-preview

This is a prerelease version of CodeLogic.WebLogic.
This package has a SemVer 2.0.0 package version: 4.9.2-preview+7009f15.
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.2-preview
                    
NuGet\Install-Package CodeLogic.WebLogic -Version 4.9.2-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.2-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.2-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.2-preview
                    
#r "nuget: CodeLogic.WebLogic, 4.9.2-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.2-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.2-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=CodeLogic.WebLogic&version=4.9.2-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, 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.7-preview 0 10/27/2026
4.9.6-preview 0 10/27/2026
4.9.5-preview 1 10/26/2026
4.9.4-preview 10 10/26/2026
4.9.3-preview 16 10/26/2026
4.9.2-preview 17 10/26/2026
4.9.1-preview 29 10/25/2026

# Changelog

## 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
 gone), with no redirects, timeouts, size caps and a bounded cache.
- Client: `formextras.js` adds show-when toggling (hidden fields are disabled and not validated),
 repeaters from `<template>`, an accessible autocomplete listbox and remote validation; schema
 messages override the defaults; `formnovalidate` submitters skip client validation.
- Builder: `AddForm<T>()`, `ConfigureForm<T, TConfigurator>()`, `AddFormValidator<T, TValidator>()`,
 `AddFormOptions(…)`, `AddFormRenderer<T>()`, `UseUploadSink<T>()`. New `Forms` config section.
- Widgets (`Widgets/`): `b.MapWidget(name, handler, o => …)` from contributors and plugins (removed
 with the contribution), template or trusted-HTML results, actions, instance settings through the
 app's `IWebWidgetSettingsStore`, access rules per widget (signed-in by default), client refresh
 intervals. `{widget:name attr="…"}` and `{widgetarea:name}` render through `WebWidgetRenderer`;
 placements (`b.PlaceWidget(area, widget, …)`) are ordered, filtered by path globs and access; a
 failing widget is logged and replaced by a comment. Endpoints are always registered:
 `GET /_weblogic/widgets/{name}` (fragment) and `POST /_weblogic/widgets/{name}/actions/{action}`
 (CSRF, 64 KB JSON).
- Contributions can stage non-route registrations with `OnCommit`; a contribution that fails while
 committing is rolled back completely.
- Dashboards (`Dashboard/`): layout models and pure helpers (`WebDashboardLayouts`: keys, normalize,
 move, add, remove), app-supplied `IWebDashboardLayoutStore` with optimistic versions,
 `IWebDashboardOwnerResolver` (default: the signed-in user, per tenant) and
 `IWebDashboardDefaults`. `GET/PUT /_weblogic/dashboard/{key}` (JSON, or rendered with
 `?format=html`). The owner always comes from the server — the old `ownerKey` query parameter and
 `..` keys are gone. Client `dashboard.js`: edit mode, drag and drop, Alt+Arrow moves, add/remove,
 conflict reload.
- Realtime (`Realtime/`), replacing SignalR: `WebRealtimeHub` with audiences (Public, Authenticated,
 Group, Permission, User, Tenant, Internal) and channels, server-sent events
 (`/_weblogic/realtime/sse`) and WebSockets (`/_weblogic/realtime/ws`, Origin-checked, ping/pong,
 optional `IWebRealtimeReceiver`), Last-Event-ID replay filtered by audience (a lost resume point
 sends `weblogic.replay-gap`), bounded per-connection queues, connection limits per node and per
 client, a maximum connection lifetime, connection tickets for cookieless clients
 (`POST /_weblogic/realtime/ticket`), `IWebRealtimeBackplane` (in-process default), event bridges
 (`web.BridgeEvent<T>(…)`, `b.BridgeEvent<T>(…)`) with explicit data mapping, connections closed on
 sign-out and password change, config change notices (property names only) to operators.
 `Realtime.BroadcastRequests` is off by default and never includes IPs or query strings.
- Caching (`Caching/`): routes opt into the shared-framework output cache with
 `.WithOutputCache(policy)`; WebLogic's policy runs first (anonymous GET/HEAD without credentials;
 nothing stored with cookies, issued CSRF tokens, session writes, non-200 or private responses;
 varies by host, tenant and client type) and the cache sits after endpoint security.
 `web.AddOutputCachePolicy(name, …)`, fragment cache `request.Cache.GetOrAddAsync(…)` (tenant-scoped
 keys, tags, single-flight factories, bounded), `IWebCacheInvalidator.EvictTagsAsync` for both.
- Explorer: `/_weblogic/explorer` and `/_weblogic/explorer.json` list routes with their protection,
 the anonymous routes, the security pipeline, widgets and areas, forms, options providers,
 realtime and contributors. On in Development; elsewhere only with `Explorer.InProduction` and the
 `weblogic.explorer` permission. The page is static, nonced and builds everything as text.
- New config sections `Realtime`, `Cache`, `Explorer`; the client config carries the realtime
 settings (`Realtime.ClientTransport`, `Realtime.ClientAutoConnect`).
- Development stores for widget settings and dashboards log a warning outside Development.
- Samples (`samples/`), rebuilt on the v2 toolkit and runnable as a deployable Docker demo
 (`samples/docker`: compose with MySQL 8.4, Mailpit and an optional Caddy HTTPS proxy):
 - `StarterWebsite`: every toolkit area in use, with app-owned JSON-file stores for users,
   sessions, API keys, recovery codes, dashboards and widget settings. It covers registration
   with e-mail verification, password reset, TOTP with recovery codes, personal API keys for
   `/api/v1`, Steam OpenID sign-in, the forms gallery and wizard, dashboards, the live wall
   (SSE), and output and fragment caching.
 - `MiniBlog`, `MiniBlog.Data` and `Plugins/MiniBlog.Admin.Plugin`: a blog on CL.MySQL2 with
   MySQL stores, cached pages tagged `posts`, RSS and sitemap, posts and the editor account
   seeded once, and the admin area as a CodeLogic plugin.
 - The Docker images pack the toolkit and publish the samples against the package. They run in
   Production as a non-root user with a read-only root filesystem and keep all state in `/data`.
 - `samples/docker/smoke.py` is an end-to-end test over HTTP. A new CI job (`samples`) builds
   the images, runs the stack, and runs the smoke tests before and after a restart.
 - The samples are in the solution, so CI compiles them with warnings as errors.
- Contributors and plugins can register forms and options providers: `b.MapForm<T>()` and
 `b.MapFormOptions(provider)`. They are removed with the contribution, and a name that is
 already registered fails the contribution.
- `WebDashboardErrors`: the error codes app stores return (`Conflict()` → 409, `Full()` → 503).

- Plugins can ship templates: `b.MapTemplates(assembly, "theme/")` registers embedded `.html`
 resources (`b.MapTemplate(path, source)` for one), removed with the contribution. A theme file with
 the same path wins, so sites can restyle plugin pages. The MiniBlog admin plugin ships its own
 templates this way.
- Cluster mode (`Cluster.Enabled`): startup fails while state that must be shared (sessions,
 accounts, API keys, recovery codes, one-time tokens, login throttle, rate limits, realtime
 backplane, notifier) is per node or the DataProtection key ring is not shared; warns for widget
 settings, dashboards, fragment and output caches and a missing application name.
 `Cluster.AllowNodeLocal` accepts deliberate exceptions (logged). Per-node stores implement the
 marker `IWebNodeLocalStore`.
- `WebRealtimeHub.PublishToTenantAsync(tenantId, …)`; envelopes carry an optional `TenantId` and
 are only delivered to connections of that tenant.
- `RateLimits.Ipv6PrefixLength` (default 64): IPv6 clients are limited per prefix and IPv4-mapped
 addresses count as IPv4, in the rate limits and the login throttle.
- `tools/CL.WebLogic.LoadTest`: a dependency-free HTTP load generator (requests/s, p50/p95/p99,
 status codes; `--header` for cookies).
- Documentation site (`docs/`, DocFX, `docs.yml` publishes to GitHub Pages): getting started, 16
 guides and 9 scenario guides (marketing site, CMS, admin, multi-tenant SaaS, SPA/mobile API,
 intranet, Steam community, plugin platform, cluster), plus the security model with its documented
 limits.

### Changed (breaking)

- `IWebWidgetSettingsStore` is keyed by owner and instance: `GetAsync(ownerKey, instanceId, ct)`,
 and `WebWidgetSettings` gains `OwnerKey`. The owner comes from `IWebDashboardOwnerResolver`
 (dashboard key `widgets`), never from the request. `WebWidgetContext.SaveSettingsAsync` returns
 `false` when there is no owner (anonymous visitors).
- Remote field validation (`/_weblogic/forms/{form}/validate/{field}`) only answers for fields with
 `[WebField(Remote = true)]` (404 otherwise) and applies the options provider's access rules.
- Templates: `|js` is only accepted inside a quoted JavaScript string; elsewhere in `<script>` it is
 a parse error (use `|json`). A value right after `<` or `</` and the `srcdoc` attribute are
 rejected like event handlers.
- The external-login state cookie is `__Host-wl_ext`.
- Cookies are `Secure` and `__Host-`-prefixed, and HSTS is sent, outside Development whenever
 `PublicBaseUrl` is `https`, even if the request arrived over HTTP from an untrusted TLS proxy.
- A wrong password on a locked account now returns `InvalidCredentials`; the lock is only revealed
 together with the correct password.

### Security

Fixes from the pre-release security review (each with a regression test):

- Sign-out fully resets the session (values, CSRF secret, identity) and rotates its id; re-signing
 the same user keeps the absolute timeout. `RefreshAsync` after a security-stamp change deletes the
 user's other sessions.
- One password attempt per login and one second-factor attempt per user run at a time on a node
 (`WebAttemptGate`), so parallel bursts cannot outrun the throttle.
- Widget endpoints enforce placement access rules; widget settings can no longer be read or written
 across owners by guessing instance ids.
- Markdown inline parsing is bounded (no quadratic scans on crafted input).
- Realtime tenant isolation (see `PublishToTenantAsync`).
- The in-memory rate-limit store evicts by each bucket's own refill rate, fullest first, so a flood
 of new addresses cannot reset drained buckets; IPv6 clients cannot rotate addresses inside a /64.
- `ForwardedHeadersStage` parses `X-Forwarded-For` values that carry a port.
- API keys stop working when their owner is disabled or deleted (`api-key-owner`).
- Magic-link sign-in on a never-confirmed address drops a password set at registration, rotates the
 stamp and ends other sessions (account pre-hijacking); it also honours `RequiresPasswordChange`.
- Password reset, magic-link, verification and e-mail-change mails are limited to 3 per address and
 kind per 15 minutes; alerts are never limited.
- The output cache neither serves nor stores responses for requests carrying a session cookie, or
 when the session was loaded.
- Path exclude patterns fail closed when their regex times out.
- `DisabledStages` is fixed at startup; a config reload cannot switch stages off.
- Client: inert script types are allowlisted when swapping HTML, upload CSRF headers go to the same
 origin only, and dashboard URLs must be local.
- Outside Development a warning is logged when DataProtection keys are not persisted.

### Performance

- Theme assets: resolutions (including misses) are cached for 2 seconds and files up to 256 KB are
 served from memory (about 8x the throughput for small assets).
- The template loader caches whether optional templates exist (field overrides, themed error
 pages), so forms and 404 pages no longer touch the file system on every render.

### Fixed

- Kestrel's `Server: Kestrel` header is no longer sent (`AddServerHeader = false`), and
 `X-Powered-By` is removed.
- Default dashboard items get stable instance ids (derived from the owner, key, position and

---

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