Repository navigation
DESIGN: endpoint contributions on the module lane — the sixth MeshNodeProviderAttribute hook #1655
Description
The wall this removes (documented in UiExtensibility → "What cannot be extended today"): there is no endpoint-contribution seam, so every HTTP surface belonging to a MODULE'S concern stays compiled in the portal, resolving its module service optionally and answering an actionable 503 when the module is delisted. The live inventory of exactly this shape:
/api/speech/transcribe— compiled in Portal.Shared; resolvesISpeechTranscriberoptionally (MeshWeaver.Speech module)./api/log-incidents— compiled; resolves theILogIncidentIngestcontract seam optionally (MeshWeaver.Observability module)./api/og/{**path}— compiled, while the OgCard AREA rides the module (feat(modules): the OgCard link-preview area becomes a module #1643): the module's own HTTP half could not follow it.- The course-asset endpoints — the Wave-2 falsification that kept education runtime code compiled.
- The
Social/*Endpointsfamilies — candidates only if their concerns ever modularize.
Proposal: a sixth hook on MeshNodeProviderAttribute:
public virtual IEnumerable<Action<IEndpointRouteBuilder>> EndpointConfigurations => [];The host applies every installed module's endpoint configurations at its endpoint-mapping stage (after auth middleware, before the Blazor catch-all — modules load at boot via InstallAssemblies, well before MapEndpoints, so ordering is structurally safe). Delisting a module then removes its routes wholesale — a 404 instead of today's compiled 503 — and the compiled optional-service endpoints migrate INTO their modules.
Design questions for review (no code before sign-off)
- Contract width: raw
IEndpointRouteBuilder(maximum power, host-framework coupling) vs a narrower record (pattern + verb + handler) the host maps itself. Raw is simpler and modules are TRUSTED CODE (unlikeUiContributiondata — the security model difference is load-bearing and must be documented); the narrow form would ease a future non-ASP.NET host but has no consumer today. - Auth defaults: contributed endpoints should default to the authenticated policy with explicit opt-out (
AllowAnonymousstays a deliberate per-route act — the/api/ogfetcher and webhook inboxes need it), so a module cannot accidentally publish an open route. - Route collisions: fail LOUD at startup on duplicate patterns (the silent-config-mismatch class) — two modules mapping
/api/xmust refuse to boot, not last-write-win. - Hosting layering:
Mesh.Contractmust not reference ASP.NET. Either the hook lives in a smallMeshWeaver.Hosting.AspNetCoreattribute subclass, orEndpointConfigurationsis typedAction<object>-shaped… the former is honest; proposal: a derivedMeshNodeProviderAttributemember discovered by the HOST package, keeping the contract assembly clean. - Migration list (each its own PR after the hook lands): speech transcribe → Speech; log-incidents → Observability; og preview → OgCard; course assets → revisit the Wave-2 falsification.
- Gate parity: the plugin compile gate is unaffected (modules stay in the compile surface via feat(modules): the modules/ publish layout, step 1 — probing, compile surface, fingerprint (#1644) #1653's
InstalledModuleAssembly); the e2e that exercises a migrated endpoint must run with the module LISTED — same as any module feature.
Review wanted before any code — the same gate #1644 carried. Related: #1644 (modules-folder publish), #1645 (UI contributions — the DATA lane this deliberately is not), #1630 (bundles).
🤖 Generated with Claude Code
Activity
Merge pull request #1663 from Systemorph/feat/1655-endpoint-hook
rbuergi commentedon Aug 21, 2026
Verdict: DONE — closing with evidence
Verified against
origin/main, item by item. The design asked for review before code; the review happened and the code landed with two deliberate, documented deviations from the proposal, both of which I judge correct.The hook
src/MeshWeaver.Hosting.AspNetCore/MeshEndpointProviderAttribute.cs—public abstract IEnumerable<Action<IEndpointRouteBuilder>> EndpointConfigurations { get; }, applied byMeshModuleEndpointExtensions.MapMeshModuleEndpointsover everyInstalledModuleAssembly. Consumers:SocialModuleAttribute(#1667) andGrpcModuleAttribute(#1670).Deviation 1 — it is NOT a sixth hook on
MeshNodeProviderAttribute. It is a separate assembly attribute, for the two reasons design question 4 raised, now stated in the code: layering (Mesh.Contractmust never reference ASP.NET) and timing (endpoints map after the auth middleware, not at mesh build). A module needing both carries both attributes. This is the "former is honest" option the issue itself preferred.The four design questions
IEndpointRouteBuilder. The trusted-code-vs-UiContribution-data distinction is documented in the attribute's<summary>, as the issue required.app.MapGroup(string.Empty).RequireAuthorization()wraps every contribution;AllowAnonymous()is a per-route act. "A module cannot accidentally publish an open route."RegisterCollisionCheckatApplicationStarted, over the materializedEndpointDataSource: logsCritical, throws ANDStopApplication(). Platform-only duplicates exempt (precompressed static assets legitimately share a route). ASP.NET alone surfaces ambiguity only at REQUEST time — precisely the silent-until-hit mode the hook must not add.Mesh.Contractstays ASP.NET-free.The migration list — deliberately REVISED, and that is the right outcome
Deviation 2, and the more interesting one.
Doc/Architecture/Modules→ "Which routes ride the module, and which stay in the host" (#1731) answers the list with a rule instead of executing it:meshweaver.v1.MeshgRPC → ride the module. They exist only because the module does; a 404 when delisted is honest. ✅ migrated.POST /api/speech/transcribe,POST /api/log-incidents→ stay in the host behind the 503 seam, reversing the proposal. Two concrete failures if pushed onto the hook: the caller loses the diagnosis (an actionable "module not listed" 503 becomes an indistinguishable 404), and the route loses the host's specific authorization policy — speech needs the portal's Bearer-onlyMcpAuth, whose challenge forwarding is what makes an unauthenticated API call answer401 + WWW-Authenticateinstead of a302to an HTML login. Naming a policy by string across an assembly boundary throws at request time in any host that never registered it. "Both failures pass CI and surface as the mobile app logs me out."/api/og/{**path}stays inMemex.Portal.Shared/Api/SeoEndpoints.cs;MeshWeaver.OgCardships the layout area only. Same rule — it is the portal's public link-preview surface, not the module's own protocol.0b3b4266f); the Wave-2 falsification no longer has a subject.Doc/Architecture/UiExtensibility→ "What cannot be extended today" now strikes the entry through:No compiled endpoint of the "module's concern, host's route" shape survives unexamined: the two that remain compiled are there by a stated rule, not by omission. The wall this issue set out to remove is gone.