Skip to content

Releases

v1.290.0

Choose a tag to compare

@github-actions github-actions released this 13 Nov 19:35
e924965

The 2026-11-13 NuGet audit plus one new architecture fitness base. No breaking change.

Added

  • CrossModuleReferenceTestsBase (MMCA.Common.Testing.Architecture) and ArchitectureRules.CrossModuleReferencesMatchAllowlist. Reads every .csproj under the repo's Source/Modules (ModulesFolder, overridable) and pins each ProjectReference from one module's project to ANOTHER module's project, in both directions, against the subclass's AllowedCrossModuleReferences (entries written "{Module}.{Layer} -> {Module}.{Layer}"): an undeclared reference fails, and so does an entry that matches no reference. It sees couplings the IL-based isolation rules cannot, such as an inlined const or a global using alias. The modules found on disk must equal the architecture map's modules, the map must declare at least one, and a module project outside {RepoToken}.{Module}.{Layer} is reported, so the fact cannot pass vacuously. A single-module app states an empty allowlist.

Changed

  • Dependencies. OpenTelemetry 1.21.0 (Api, OTLP exporter, Extensions.Hosting, Instrumentation.AspNetCore, Http and Runtime, Exporter.InMemory); AWSSDK 4.1.x (Core, DynamoDBv2, RDS, S3, SecretsManager, SNS); Microsoft.Data.SqlClient and Microsoft.Data.SqlClient.Extensions.Azure 7.1.2 (move them together); MudBlazor 9.13.0; StackExchange.Redis 3.4.35; Anthropic 13.8.0; Scalar.AspNetCore 2.21.0; Fido2 4.3.3; FirebaseAdmin 3.7.1; bunit 2.15.2 with AngleSharp 1.10.2.
  • Per-framework versions for the dotnet/extensions train and the OAuth providers. Microsoft.Extensions.Http.Resilience, ServiceDiscovery, Caching.Hybrid, AI, AI.OpenAI and TimeProvider.Testing resolve 10.11.0 on net10.0 and 11.0.0 on net11.0; AspNet.Security.OAuth.Apple and AspNet.Security.OAuth.GitHub resolve 10.1.0 and 11.0.0. The 11.x extensions assets would floor a net10.0 consumer's Microsoft.Extensions.* runtime packages at 11.0.0, and OAuth 11.x ships net11.0 assets only, so the .NET 10 line stays on 10.x.
  • Analyzers (build-time only, nothing flows to a consumer). Meziantou.Analyzer 3.0.339, SonarAnalyzer.CSharp 10.36.0.15337 and StyleCop.Analyzers 1.3.180 (a trial of the first stable 1.3 build), with the shared .editorconfig baseline tightened where a full build measured zero violations and every catch-all documented with a CA1031 pragma stating its reason.

v1.289.0

Choose a tag to compare

@github-actions github-actions released this 13 Nov 01:02
8cf86a9

Breaking: two unused components are removed from MMCA.Common.UI. PageStateScope (MMCA.Common.UI.Components.PageState) and the registerBfcacheHandler/unregisterBfcacheHandler functions of nav-interop.js are removed with no replacement; no consumer used them. MobileCardList<TItem> (MMCA.Common.UI.Components.Lists) is removed; use MobileInfiniteScrollList<TItem>. ClickableCard is unchanged. See UPGRADING.md.

Breaking: MMCA.Common.Application.Auditing.AuditTrailEntryDTO is moved and renamed to MMCA.Common.Shared.Auditing.AuditHistoryEntryDTO (same members, same required set), and IAuditTrailReader.GetForEntityAsync returns Task<IReadOnlyList<AuditHistoryEntryDTO>>; the MMCA.Common.Application.Auditing namespace no longer exists. No consumer references the old type. Replace the using. See UPGRADING.md.

Breaking (binary only): SetConcurrencyETag(object?) moves from EntityControllerBase<TEntity, TEntityDTO, TIdentifierType> to ApiControllerBase. Source compatible: every existing call compiles unchanged, so a recompile is the whole fix. See UPGRADING.md.

Breaking (MMCA.Common.Testing.Architecture): FormsConventionTestsBase.RequiredMarkers now expects the CreatePageBase member names (IsDirtyAccessor="() => IsDirty", @ref="Form", Result="SaveResult", Messages="Form?.Errors"; the _isDirty, _saveResult and _form markers are gone), and the new fact AdminCreateForms_InheritCreatePageBase requires every *Create.razor to derive from CreatePageBase. A consumer subclass is red until its create forms move to the base: rename _isDirty to IsDirty, _form to Form, _saveResult to SaveResult and _cts.LifetimeToken() to PageToken. Every create form moves in the same change as the upgrade: overriding RequiredMarkers does not satisfy AdminCreateForms_InheritCreatePageBase. See UPGRADING.md.

Breaking (MMCA.Common.Aspire.Hosting): WithE2eForceServer(bool unlessAlso), WithE2eRegistrationThrottleLift(bool alsoLiftWhen) and WithE2eGatewayRateLimitLift(bool alsoLiftWhen) lose their parameter; the helpers read E2E_FORCE_WASM themselves. Fix: delete the argument; replace an inline E2E_FORCE_WASM parse with ui.WithE2eForceWebAssembly(); make App.razor use E2eRenderMode.Resolve(Configuration). See UPGRADING.md.

Added

  • [PublicCachedReads(policyName)] (MMCA.Common.API, MMCA.Common.API.Caching). A controller convention for an entity controller: the inherited base reads (both GetAllAsync overloads, GetAllForLookupAsync, GetByIdAsync, inherited or overridden) get [AllowAnonymous] and [OutputCache(PolicyName = ...)] endpoint metadata, each only when the selector does not already carry one (an override's own [OutputCache] wins), and the by-id read gets the route name ByIdRouteName ?? "Get{EntityName}ById" unless an override declared one. The export, the writes and app-declared actions are untouched; nothing is added to the action filters. A controller not deriving from EntityControllerBase throws when the convention runs, and so does an overridden base read that declares its own [Authorize] or permission attribute without [AllowAnonymous]: the anonymous marker would otherwise replace that requirement silently, so a read that needs a gate belongs on a controller without the attribute. Authorization declared on the controller class (gated writes, public reads) is unaffected.
  • [RefuseMachineClients] (MMCA.Common.API, MMCA.Common.API.Authorization). An authorization filter for a controller or an action where a person takes part (casting a vote, saving a bookmark, checking in). A machine client holds what its backing role holds, so an endpoint that asks only for a signed-in caller is open to every machine client; this attribute answers a request whose token carries token_use = machine with 403 (a problem response with the error code MachineClients.EndpointRefused) and lets every other caller through. Declared on a controller it covers every action, the permission-gated ones included. It adds nothing to the API description, and a controller that does not carry it is unchanged.
  • [EvictOutputCache(params tags)] (MMCA.Common.API, MMCA.Common.API.Caching). A class- or method-level result filter: a write (not GET or HEAD) whose final status is 2xx evicts the tags before the response body, best effort through TryEvictTagsAsync under CancellationToken.None, after [SupportsIfMatch] rewrote a conflict to 412 (which evicts nothing). Each tag is evicted once per request (EvictedTagsItemKey). No IOutputCacheStore registered means nothing is evicted.
  • Conditional-write helpers on ApiControllerBase (MMCA.Common.API). HandleConditionalCommandAsync (204) and HandleConditionalCommandWithResponseAsync (200 with the handler value and its ETag) read the If-Match token [SupportsIfMatch] decoded, build the command from it, dispatch it, await an optional onSuccess callback, and answer HandleFailure on failure. Without [SupportsIfMatch] they throw InvalidOperationException and the handler never runs. SetConcurrencyETag now resolves the token from the row's runtime type (cached per type), so any controller can use it.
  • Cursor paging on list endpoints (MMCA.Common.Application, MMCA.Common.API). IEntityQueryService.GetPageByCursorAsync(KeysetPageRequest, specification, ct), a default interface member answering CursorPagingNotSupportedErrorCode (500) for an implementation that predates it, overridden by EntityQueryService (sort column validated against the response contract and mapped through DTOToEntityPropertyMap). [SupportsCursorPaging] on a controller opts in to GET {controller}/cursor (EntityControllerBase.GetCursorPageAsync); a controller without it has no such endpoint and an unchanged OpenAPI document.
  • Change feed with tombstones (MMCA.Common.Shared, MMCA.Common.Application, MMCA.Common.API). EntityChange<TEntityDTO, TIdentifierType> (Id, IsDeleted, ChangedOn, Item); IEntityQueryService.GetChangesSinceAsync(since, cursor, pageSize, specification, ct), a default interface member answering ChangeFeedNotSupportedErrorCode (500), overridden by EntityQueryService: one keyset read over (LastModifiedOn, Id) ascending with the soft-delete filter lifted (the tenant filter stays), since inclusive and normalized to UTC, deleted rows as tombstones with no item. [SupportsChangeFeed] opts a controller in to GET {controller}/changes (EntityControllerBase.GetChangesAsync).
  • Bounded upload reads (MMCA.Common.API, MMCA.Common.API.Uploads). IFormFile?.ReadBoundedAsync(maxBytes, invalidUpload, ct) returns UploadedFileContent (bytes, declared file name and content type) or a failure carrying exactly the caller's error for a missing, empty, oversize or truncated file; IFormFile?.ReadCsvAsync(...) adds CSV parsing and returns CsvUpload (the raw upload and the CsvTable), passing the reader's Csv.Malformed and Csv.Empty errors unchanged.
  • Import report (MMCA.Common.Shared, MMCA.Common.Shared.Imports). ImportReportDTO (DryRun, TotalRows, Added, Updated, Unchanged, Rejected, RowErrors, never null), for an application to derive its own counts from; ImportRowErrorDTO with FromError(error, lineNumber, column); ImportConventions.DryRunFieldName (dryRun) and the documented dry-run convention.
  • Audit-history endpoint base (MMCA.Common.API, MMCA.Common.API.Controllers.Administration). AuditHistoryControllerBase(reader, params auditedEntityTypes) serves GET {prefix}/{entity}/{key}?page=&pageSize= as a bare array of AuditHistoryEntryDTO, newest first, with AuditHistory.UnknownEntity (404) and AuditHistory.KeyRequired (400) refusals; the class is gated on the new AdministrationPermissions.ViewAuditTrail (audit:view, MMCA.Common.Shared). A host mounts it with a one-line derived controller.
  • Audit-history panel and client (MMCA.Common.UI), the UI half of the endpoint above. services.AddAuditHistoryUI() (once, after AddUIShared; TryAddScoped, idempotent) registers IAuditHistoryUIService (MMCA.Common.UI.Services.Auditing), whose GetPageAsync(routePrefix, entityAlias, entityKey, page, pageSize, ct) sends one bearer-authenticated, retried GET to {routePrefix}/{alias}/{key}?page=&pageSize= (alias and key escaped) and returns the bare array as AuditHistoryEntryDTOs, or a failed Result for a non-2xx answer or a transport fault. AuditHistoryPanel (MMCA.Common.UI.Components.Auditing) renders the entries over a FetchPage(page, pageSize, ct) delegate as a table (when in DisplayTimeZone, default UTC, with the zone name; who through UserLabel, or "System" for a null user; the change kind; the field through PropertyLabel; before; after), fetches nothing during prerender, offers "Load more" while a page comes back full (PageSize, default 20), shows the shared empty and retry states, and cancels its fetch on ReloadAsync() and on dispose. The panel is opt-in; nothing in the framework renders it. New shared resource keys under AuditHistory.*.
  • Offline write queue (IOfflineWriteQueue, AddOfflineWriteQueue, AddOfflineWriteHandler, OfflineWriteNotifier) (MMCA.Common.UI, MMCA.Common.UI.Services.Offline). A durable queue of writes made offline, kept as one list in the device's ILocalCacheStore (IsAvailable is false on Blazor Server) and scoped to the signed-in user. EnqueueAsync(kind, payload, options) stamps an id, an idempotency key (supplied or a new GUID), the payload as web JSON, the user, the time and an optional row version (PendingWrite.IfMatch formats it as the weak ETag) and coalesce key, refusing with OfflineWrites.Unavailable or OfflineWrites.QueueFull; CancelPendingAsync(coalesceKey) drops writes that cancel each other ...
Read more

v1.288.0

Choose a tag to compare

@github-actions github-actions released this 11 Nov 18:07
53a8b6e

Added

  • net11.0 platform targets in MMCA.Common.UI.Maui. The package now carries net11.0-android, -ios, -maccatalyst and -windows10.0.19041.0 beside the four net10.0 ones, so a MAUI head on either .NET version finds its assets. The net11.0 targets depend on MAUI 11.0.10, CommunityToolkit.Maui 17.0.0 and Xamarin.AndroidX.Biometric 1.1.0.33; the net10.0 targets are unchanged (MAUI 10.0.130, 16.0.0, 1.1.0.30). No source change and no API change. A head that moves to net11.0-android compiles against Android API 37: a TargetPlatformVersion of 36.0 is rejected by the .NET 11 Android workload (NETSDK1140), so review the head's target API level when it moves.

v1.287.0

Choose a tag to compare

@github-actions github-actions released this 11 Nov 12:29
65e9d85

Security patch release for the 2026-11-10 Microsoft advisories, plus two opt-in or framework-conditional additions. No breaking change.

Security

  • ASP.NET Core 10.0.15 (CVE-2026-114147, GHSA-wr8v-9x5q-ggcm, high, denial of service). On net10.0 the packages now depend on ASP.NET Core, Microsoft.Extensions.* and EF Core 10.0.15 (was 10.0.14); net11.0 was already on the patched 11.0.0. A net10.0 host must also move its runtime image to 10.0.15: the advisory covers the shared runtime, not only the packages.
  • Aspire 13.6.5 (CVE-2026-120006, GHSA-f7vh-h5g3-7jrh, high, elevation of privilege in Aspire.Hosting). Every Aspire pin moves from 13.6.4; a consumer AppHost should move its own Aspire.Hosting.* pins and Aspire.AppHost.Sdk to match.
  • Microsoft.AspNetCore.WebUtilities 2.3.14 (CVE-2026-118750, GHSA-6527-9fp8-r4x6, moderate, denial of service). MMCA.Common.UI lifts it directly, because Microsoft.AspNetCore.Mvc.Core 2.3.14 still resolves the vulnerable 2.3.12.
  • System.Formats.Cbor 10.0.15 / 11.0.0 (CVE-2026-118747, GHSA-vmv3-wvjp-wqpj, moderate, denial of service in the CBOR reader). MMCA.Common.Passkeys lifts it directly off the 9.0.0 that Fido2 resolves.

Added

  • PushNotifications:CloseConnectionsOnTokenExpiry (MMCA.Common.Infrastructure, MMCA.Common.API; off by default). When on, MapNotificationHub() has the server close a hub connection when the access token it was authenticated with expires. The shipped client reconnects at once with a fresh token; a user who was locked, lost a role or signed out everywhere cannot obtain one and is refused, so an open connection no longer outlives a revoked session by more than one token lifetime. It needs neither negotiation nor a sticky ingress: each replica enforces it on the connections it holds.

Changed

  • Zstandard response compression on .NET 11 (MMCA.Common.API). AddCommonResponseCompression() registers ASP.NET Core 11's ZstandardCompressionProvider ahead of Brotli and Gzip, so a net11.0 host answers a client that accepts zstd with Content-Encoding: zstd. A net10.0 host is unchanged (Brotli, then Gzip), and so is the middleware order. No public API change.

v1.286.0

Choose a tag to compare

@github-actions github-actions released this 10 Nov 21:13
3dfdc4b

Every package now ships for .NET 10 and .NET 11 from one source tree. No public API changes: the surface is identical on both target frameworks.

Added

  • net11.0 assets in every package (all packages except MMCA.Common.UI.Maui, which stays on its four net10.0 platform targets for now). Each .nupkg carries lib/net10.0 and lib/net11.0; a consumer gets the one matching its own target framework and changes nothing. The target frameworks are named once, in build/Frameworks.props, which also states the rules for version-specific code (#if NET11_0_OR_GREATER only, with the existing code as the #else branch).

Changed

  • Per-framework dependency versions. On net11.0 the packages depend on ASP.NET Core and Microsoft.Extensions.* 11.0.0 and on Microsoft.OpenApi 3.11.0; on net10.0 they stay on 10.0.14 and 2.13.0. Entity Framework Core stays on 10.0.14 for BOTH frameworks: the only Npgsql.EntityFrameworkCore.PostgreSQL 11 build is a release candidate that pins EF Core to that exact candidate. EF Core 10 runs on the .NET 11 runtime.
  • Built with the .NET 11 SDK and C# 15. global.json pins SDK 11.0.100 and LangVersion is 15.0 instead of preview. A consumer building MMCA.Common from source (local.props) needs the .NET 11 SDK; a consumer on packages does not.
  • A dropped browser connection no longer throws from three UI JavaScript call sites (MMCA.Common.UI): the file-download button, DocumentLanguage and the list-page scroll tracking now treat JSDisconnectedException as "nothing left to update" instead of letting it escape.

Notes for a .NET 11 host

  • An ASP.NET Core 11 host emits OpenAPI 3.2 by default and orders tags alphabetically. Set OpenApiOptions.OpenApiVersion in the host's own AddOpenApi(...) call to keep a committed document stable across frameworks.
  • NuGet reports System.Security.Cryptography.Pkcs 9.0.18 (GHSA-846f-f764-76mx, GHSA-2fmv-rv2c-59x7) on net11.0 projects that reference the ASP.NET Core shared framework and Microsoft.Data.SqlClient. It is a false positive: the shared framework supplies 11.0.0 and that is what loads. Directory.Build.props here carries the two net11.0-only NuGetAuditSuppress entries with the removal condition; a consumer host that sees the same two warnings can carry the same pair until a Pkcs 11.0.1 exists.

v1.285.0

Choose a tag to compare

@github-actions github-actions released this 10 Nov 13:24
5b114fd

Dependency release from the 2026-11-10 NuGet audit. No public API changes.

Changed

  • MAUI 10.0.130 (MMCA.Common.UI.Maui). Microsoft.Maui.Controls and Microsoft.AspNetCore.Components.WebView.Maui move from 10.0.120; consumer heads pin the same version.
  • CommunityToolkit.Maui 16.0.0 (MMCA.Common.UI.Maui), from 15.0.1. The toolkit now throws ArgumentNullException at the call for a null argument it cannot handle, and on Android CommunityToolkit.Maui.Core adds a launcher <queries> entry to the merged manifest. Heads keep calling UseMauiCommunityToolkit(); MauiSpeechToTextService needs no change.
  • Minor and patch updates: xunit.v3 and xunit.v3.extensibility.core 4.0.3 (Microsoft.Testing.Platform 2.5.2; a consumer pinning Microsoft.Testing.Extensions.Retry moves it to 2.5.2 with it), bunit 2.14.0 with AngleSharp 1.10.1, Polly and Polly.Core 8.9.1, AspNet.Security.OAuth.Apple and .GitHub 10.1.0, OpenTelemetry.Instrumentation.AspNetCore and .Http 1.20.2, Anthropic 13.7.0, OpenAI 2.15.0, Scalar.AspNetCore 2.19.0, Meziantou.Analyzer 3.0.322, and AWSSDK patch updates.

v1.284.0

Choose a tag to compare

@github-actions github-actions released this 10 Nov 01:32
5b4d9c4

A filter changed on a phone list now survives leaving the page and coming back.

Breaking: ListPageActions.ReloadActiveLayoutAsync(isMobile, mobileList, dataGrid) is removed (MMCA.Common.UI). A page derived from DataGridListPageBase<TDto> calls the new protected ReloadActiveLayoutAsync(mobileList) instead, which reloads GridRef on a desktop. See UPGRADING.md.

Added

  • DataGridListPageBase<TDto>.ReloadActiveLayoutAsync(MobileInfiniteScrollList<TDto>?) (MMCA.Common.UI). Reloads whichever layout is rendered. On a phone it calls SaveFilterState() and then resets the card list; otherwise it reloads GridRef, whose fetch saves on its own.

Fixed

  • Phone list filters are saved (MMCA.Common.UI). A MobileInfiniteScrollList fetches through the page's own delegate, which saved nothing, so a search or filter changed on a phone was lost on leaving the page, and an older saved value came back on return (ADC Run 16, D6). UserAdminList search is saved the same way.

v1.283.0

Choose a tag to compare

@github-actions github-actions released this 09 Nov 21:15
8335843

Phone fixes from ADC manual test Run 16: photo uploads are sized on the device, the page bottom clears the Android navigation bar, a denied camera no longer opens a black scanner, and external sign-in logs how each attempt ended.

Breaking: IBarcodeScannerService gains EnsureCameraAccessAsync(CancellationToken) (MMCA.Common.UI), so a caller can explain a denied camera. A custom implementation adds the member; NullBarcodeScannerService returns false. MauiExternalAuthBroker takes an ILogger<MauiExternalAuthBroker> as a new last constructor argument (MMCA.Common.UI.Maui); DI supplies it. See UPGRADING.md.

Added

  • DataGridListPageBase<TDto>.SaveFilterState() (MMCA.Common.UI). Saves the page's current filters to the URL, in-memory state and sessionStorage, keeping pager and sort. For a page whose MobileInfiniteScrollList fetches through its own delegate, which saves nothing.
  • External sign-in exit logging (MMCA.Common.UI.Maui). MauiExternalAuthBroker logs one start line and the exit taken, with the elapsed time, at Information, so the in-app diagnostics log shows it. Lines carry fixed exit names and the server's error name only, never the callback's code or state or any exception text.

Fixed

  • Photos are downscaled on the device (MMCA.Common.UI.Maui). MauiMediaPickerService fits take and choose results within 1024 px at JPEG quality 85, applies the rotation to the pixels and drops the metadata. The platform returns the original file when it cannot process one, so a caller with a size limit still checks it.
  • The page bottom clears the Android navigation bar (MMCA.Common.UI, UI.Maui). MainPageBase adds a MmcaNavigationBar/<css px> user agent token, theme-prepaint.js exposes it as --mmca-navigation-bar-fallback, and the footer and drawer pad with the larger of that and env(safe-area-inset-bottom), which reads 0 on a cold launch.
  • A denied camera no longer opens a black scanner (MMCA.Common.UI.Maui). MauiBarcodeScannerService requests the permission before the scan page opens; a denial reads as no scan.
  • The diagnostics log toolbar wraps (MMCA.Common.UI) instead of overflowing its card on a phone.
  • The language button has an accessible name on Android (MMCA.Common.UI). The Android WebView exposes no name for an aria-label on a button with aria-haspopup, which MudMenu always sets, so CultureSwitcher names its activator with visually hidden text.

v1.282.0

Choose a tag to compare

@github-actions github-actions released this 08 Nov 21:34
7223c97

Reliability wave: the hub tells a subscriber when a connection has been restored, view-as works over gRPC, an open circuit no longer spends the outbox retry budget, retention deletes are batched, long-running scheduled jobs renew their lease, and the deleted-user marker outlives a raised access-token lifetime.

Breaking: DeleteUserHandlerBase<TUser, TCommand> takes ISoftDeletedUserMarkerWriter markerWriter in place of ICacheService cacheService (MMCA.Common.Application; ADR-047), so a host that raises the access-token lifetime cannot silently keep a 20-minute deleted-user marker. A subclass swaps the parameter on its own constructor and passes it to the base as the second argument; AddApplication registers the writer, so no DI change is needed. See UPGRADING.md.

Added

  • NotificationHubService.OnConnectionRestored (MMCA.Common.UI). Subscribes a Func<Task> handler (dispose the returned subscription to unsubscribe) that runs once for each restored connection, after the rejoin of its held channels was attempted, on both reconnect paths (the automatic reconnect and the restart after a close), and even if a join failed, because a page should still refresh once. It does not run for the first successful start of a connection or once the service has been stopped or disposed. SignalR delivers nothing that was sent while the connection was down, so a subscriber re-reads what it shows live. Handlers are awaited one after another; one that throws is logged and affects neither the rejoin nor the other handlers.
  • ViewAsReadOnlyEndpointAttribute (MMCA.Common.Shared, namespace MMCA.Common.Shared.Auth; ADR-137). A host marks a read-only gRPC service or method with it, and ViewAsGuardMiddleware then treats a POST routed to that endpoint as safe for the METHOD rule only (any other method is still refused): ViewAs:Enabled and the grant-liveness check still run, there is no exemption by content type or for gRPC in general, and ViewAsCommandDecorator is unchanged. Without the mark, a view-as read that fans out over gRPC is refused on the peer, because gRPC is always POST.
  • McpToolPublicAttribute (MMCA.Common.Mcp) and McpToolAccessDeclarationTestsBase (MMCA.Common.Testing.Architecture; ADR-138). [McpToolPublic] records that a tool is public on purpose, with no runtime effect. The test base fails when an [McpServerTool] method, or an override of one, carries anything other than exactly one of [McpToolPermission] or [McpToolPublic] of its own; the subclass supplies TargetAssemblies and an optional MinimumMcpTools floor. The rule is also exposed as ArchitectureRules.McpToolsDeclareAccess and ArchitectureRules.CountMcpTools.
  • ISoftDeletedUserMarkerWriter and SoftDeletedUserMarkerWriter (namespace MMCA.Common.Application.Auth.SoftDeletedUsers), registered scoped by AddApplication and needing ITokenService only when resolved, plus SoftDeletedUserCache.MarkerDurationFor and SoftDeletedUserCache.MarkDeletedForAsync (MMCA.Common.Application; ADR-047); see Fixed.

Changed

  • Retention deletes run in batches of 1000 (MMCA.Common.Infrastructure). The outbox processed and dead-letter purges, the inbox purge, the internal-command processed and dead-letter purges and the refresh-session purge were single unbatched deletes; they now share the loop AuditTrailCleanupJob used (ids read a page at a time, deleted by id). Cancellation is observed between batches, the total deleted is logged even when a later batch throws or the sweep is cancelled mid-batch, a soft-deletable entity is refused, and it runs on every relational engine the purges support. The refresh-session sweep orders by its key. No new table or column.
  • A scheduled job's lease is renewed while it runs (MMCA.Common.Infrastructure). ScheduledJobRunner extends LockedUntil every half Scheduler:LeaseSeconds, guarded by the claim token and on the cycle's own idle context, stopped before the completion stamp. A renewal matching no row means another replica owns the row: the run is cancelled, nothing is stamped, and one Warning names the job. A failed renewal is logged and retried sooner. A replica that dies stops renewing, so its claim is released when the lease expires, as before.

Fixed

  • An open circuit no longer consumes the outbox retry budget (MMCA.Common.Infrastructure, Shared; ADR-087). A BrokenCircuitException was charged to the row like a failed publish, so a long broker outage dead-lettered rows that never reached the broker. It no longer increments RetryCount and cannot dead-letter the row; the row is re-leased for about BrokerResilienceDefaults.BreakDuration plus jitter, capped at the lease, and keeps LastError, the broker.circuit.open.count counter and the once-per-batch log. Real publish failures and failed half-open probes keep counting. The breaker now also runs on the processor's injected TimeProvider.
  • A scheduled job longer than its lease no longer runs twice (MMCA.Common.Infrastructure). On two or more replicas such a job ran concurrently, its completion stamp was dropped and the schedule never advanced; the renewal above closes it.
  • The deleted-user marker outlives a raised access-token lifetime (MMCA.Common.Application, API; ADR-047). SoftDeletedUserCache.MarkerDuration stays 20 minutes, the floor. DeleteUserHandlerBase (through ISoftDeletedUserMarkerWriter), SoftDeletedUserMiddleware and MachineClientService now write max(20 minutes, access-token lifetime plus clock skew), so above 15 minutes a deleted user is no longer served on a host without the validator once the marker lapses. The middleware reads the host's bearer clock skew; the two Application-layer writers assume the 5-minute default.

v1.281.0

Choose a tag to compare

@github-actions github-actions released this 08 Nov 17:04
0ab3a88

Generic OIDC sign-in, passkeys, machine clients, reloadable feature flags, a permission coverage gate, and background-job failures that keep their cause.

A new package, MMCA.Common.Passkeys, joins the framework (the only package that references Fido2NetLib and its native crypto dependency).

A host that turns on reloadable feature flags, passkeys or machine clients needs one migration per opt-in (a FeatureFlagOverrides table on each database that owns overrides, a PasskeyCredentials or MachineClients table on the Identity database); see UPGRADING.md. Nothing changes for a host that opts into none of them.

Added

  • Generic OIDC sign-in providers (MMCA.Common.API, Shared, UI; ADR-036). AddExternalAuthProviders also registers one OpenID Connect scheme per OAuth:Oidc:<Name> section that sets a ClientId (with Authority and ClientSecret required, optional Scopes, DisplayName, Domains and TrustEmail, default false): authorization code flow with PKCE into the existing external-login cookie, callback at /auth/callback/<Name>. A misconfigured section fails startup naming the key; the names google, github, apple, complete, exchange and discover are reserved, a name must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$, and a domain claimed by two providers fails startup. The provider key the app's ExternalLoginAsync receives is {tid}|{oid} when the ID token carries both (Entra ID), else {iss}|{sub}, never the email; email_verified=true reaches the app only when the provider sets TrustEmail AND the token asserts it, so an app that links by verified email never links on an unverified Entra address. OAuthControllerBase gains GET auth/oauth/{provider} (the literal google, github and apple routes keep precedence; 404 for an unknown name) and GET auth/oauth/discover?email= (anonymous, on the auth-ip policy; exact domain match; OidcDiscoveryResponse or 404). The Common.UI login shows one button per configured provider and, when any provider declares Domains, a "Sign in with your organization" email-first step (English and Spanish); a WebAssembly client lists a provider when its client configuration carries OAuth:Oidc:<Name>:Enabled. Entra ID tokens carry email only when that optional claim is configured on the app registration; without it the sign-in ends with missing_claims (there is deliberately no fallback to the unverified preferred_username).

  • Passkeys (WebAuthn) (MMCA.Common.Passkeys, new; MMCA.Common.Shared, Domain, Application, Infrastructure, API, UI, UI.Web; ADR-139). A phishing-resistant sign-in and second factor for web heads. services.AddPasskeys(configuration) from the new package (Passkeys:Enabled, Passkeys:RelyingPartyId, Passkeys:RelyingPartyName, Passkeys:Origins, Passkeys:DataSourceName, validated at startup) registers the Fido2NetLib-backed IPasskeyCeremony; no other framework package references Fido2, NSec or libsodium (EnforcePasskeysBoundary at build time plus PasskeysIsolationTests). Passkeys:Enabled true without the call fails startup naming it. PasskeysControllerBase (abstract, under auth/passkeys): register/options and register for a signed-in user whose interactive sign-in (auth_time) is under 10 minutes old; GET, PATCH {id} (rename) and DELETE {id} (the last passkey goes only while a password or an external login remains, under a per-account lock); anonymous signin/options and signin on the auth-ip policy, with discoverable credentials. Challenges are single-use and live 5 minutes in the shared cache; a decreasing signature counter is refused (0 to 0 is allowed for synced passkeys). A passkey sign-in mints the normal token pair and refresh session through AuthenticationServiceBase.SignInWithPasskeyAsync, with the new mfa value passkey. PasskeyCredentials (hard-deleted, so a removed key can never sign in) is mapped only where enabled. Common.UI: a PasskeysPanel card for the profile page and a passkey button plus browser autofill on Login when SignIn:OfferPasskeys is true; autofill asks for a challenge only on a browser that supports it, and MAUI heads hide passkeys. New consumer hooks on IAuthenticationService: SignInWithPasskeyAsync and HasSignInMethodBesidesPasskeysAsync (both implemented by AuthenticationServiceBase), and AuthenticationServiceBase.HasExternalLoginAsync (protected virtual, default false: override it when users can sign in through an external provider). The anonymous passkey sign-in path joins the same-origin proxy's default token-issuing paths.

  • Machine clients through OAuth client credentials (MMCA.Common.Shared, Domain, Application, Infrastructure, API, UI; ADR-140). Programs get a short-lived bearer token every service, and every permissioned MCP tool, already validates. services.AddMachineClients(configuration) (MachineClients:Enabled, MachineClients:AccessTokenMinutes default 15 and at most 15 so a token never outlives the revocation marker, MachineClients:DataSourceName) with a consumer-supplied IServiceAccountProvisioner (CreateAsync(name, role) returning the new user's id, FindAsync(userId) returning a ServiceAccount(UserId, Name, Email, Role)); enabled without the provisioner, or without the call, fails startup naming it. Each client is backed one-to-one by its own service-account user, so audit stamps, ownership and the per-user rate-limit partition work unchanged; IAuthUser.IsServiceAccount (defaulted, false) and ViewAsSubject.IsServiceAccount (init-only) let password login, sign-in links, external login, passkeys and view-as refuse such an account. MachineTokenControllerBase (route it at auth/token): anonymous POST, grant_type=client_credentials with HTTP Basic or form credentials, on the auth-ip policy, answering RFC 6749 JSON or invalid_client / unsupported_grant_type. The token's sub is the service account, its permissions are the role's, and it carries client_id and token_use=machine, with no refresh token, session, auth_time or mfa. Secrets are 32 random bytes shown once, stored as SHA-256 and compared in constant time; rotate issues a secondary, promote retires the old primary, revoke writes the soft-deleted marker so every service rejects outstanding tokens. A role granting users:view-as, users:manage, roles:manage or clients:manage cannot back a client, checked at create and at every mint. MachineClientsControllerBase (abstract, no route, the new AdministrationPermissions.ManageClients, clients:manage) lists, creates, rotates, promotes and revokes; a Common.UI admin page (no route) with English and Spanish resources. MachineClients is mapped only where enabled (DesignTimeDbContextOptions.EnableMachineClients).

  • auth_time claim (MMCA.Common.Shared, Application). Every interactive sign-in that opens a session (password, registration, sign-in link, external login, passkey) stamps AuthClaimTypes.AuthTime (Unix seconds); a refresh carries it unchanged and a view-as token has none. Passkey registration is its first reader, so a refreshed session cannot pass for a recent sign-in.

  • Reloadable feature flags (MMCA.Common.Shared, Domain, Application, Infrastructure, API, UI; ADR-031). AddReloadableFeatureFlags(configuration, params Assembly[] featureAssemblies), called after AddAPI(), layers stored overrides (on, off, a sticky per-user percentage, or targeted users and groups) over the FeatureManagement configuration, so a kill switch flips without a new revision. Configuration stays the default; an override wins until cleared (a soft delete, so the audit fields record who cleared it). Overrides live in the owning service's database (FeatureFlags:DataSourceName, default Default), are read as one snapshot through ICacheService under a generation-suffixed key that every write invalidates (seconds across replicas with the two-level cache; FeatureFlags:OverrideCacheSeconds, default 30, bounds staleness otherwise), and only a flag declared with [FeatureFlag] in the given assemblies can be overridden (FeatureFlag.Undeclared, 404). When the snapshot cannot be loaded, every flag answers from configuration and a Warning is logged. FeatureFlagsControllerBase (abstract, no route, features:manage, the new AdministrationPermissions.ManageFeatures) serves GET (each declared flag with lifetime, owner, configured value, override and effective value), PUT {name} (a Reason is required) and DELETE {name}; the FeatureFlagAdmin page (no route: the app supplies it) shows one table across the flag sources given to services.AddFeatureFlagAdministrationUI(params string[] sources), in English and Spanish. The FeatureFlagOverrides table is mapped only where AddReloadableFeatureFlags is called; set DesignTimeDbContextOptions.EnableFeatureFlagOverrides = true in the owning service's migrations project to generate it.

  • PermissionCoverageTestsBase (MMCA.Common.Testing.Architecture). A fitness test that fails when a controller, page, MCP tool or menu entry names a permission no role is granted, since nobody can ever pass such a gate. The subclass supplies TargetAssemblies, DeclaredPermissions (pass IPermissionCatalog.Permissions of the registry the token issuer builds, which lists granted permissions only), optional AdditionalReferencedPermissions for permissions named in code such as NavItem.RequiredPermission, and a MinimumNamedPermissions non-vacuity floor (default 1). It reads [HasPermission], [HasPagePermission], [McpToolPermission] and a plain [Authorize(Policy = "perm:...")] or "page-perm:...". Class-level attributes are read with inheritance, so an app controller derived from a framework base must grant the base's gate; method-level ones only where declared. The match is ordinal, as the permission registry's is. Only this direction is checked: a granted permission nothing names ...

Read more