diff --git a/Directory.Packages.props b/Directory.Packages.props
index 09cd0b0..b1dd289 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -21,6 +21,7 @@
+
diff --git a/src/Host/Configuration/Authentication/AuthorizationPolicyCollisionGuard.cs b/src/Host/Configuration/Authentication/AuthorizationPolicyCollisionGuard.cs
index 35be289..d5dfa35 100644
--- a/src/Host/Configuration/Authentication/AuthorizationPolicyCollisionGuard.cs
+++ b/src/Host/Configuration/Authentication/AuthorizationPolicyCollisionGuard.cs
@@ -1,5 +1,6 @@
using System.Collections;
using System.Reflection;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
using Host.Plugins.Loading;
using Microsoft.AspNetCore.Authorization;
@@ -14,7 +15,7 @@ namespace Host.Configuration.Authentication;
/// silently replaces a previously registered policy when the same name is added twice, which
/// would let a plugin silently overwrite another plugin's policy. To keep collisions explicit,
/// the host snapshots the policy map before and after each plugin's
-/// hook
+/// hook
/// and fails startup when a name already owned by another plugin is registered again.
///
///
diff --git a/src/Host/Configuration/Pipeline/AppMiddlewareConfiguration.cs b/src/Host/Configuration/Pipeline/AppMiddlewareConfiguration.cs
index dc34eb6..f0b2f63 100644
--- a/src/Host/Configuration/Pipeline/AppMiddlewareConfiguration.cs
+++ b/src/Host/Configuration/Pipeline/AppMiddlewareConfiguration.cs
@@ -4,6 +4,7 @@
using Host.Security.Middleware;
using AuthKit.Plugins.Abstractions;
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
namespace Host.Configuration.Pipeline;
@@ -19,7 +20,7 @@ namespace Host.Configuration.Pipeline;
///
/// New pipeline hooks are inserted at their strongly typed
/// . Plugins at the same position are
-/// ordered by stable plugin ID. Legacy
+/// ordered by stable plugin ID. Legacy
/// middleware remains in its original slot unless the plugin opts into a new
/// application or pipeline hook.
///
diff --git a/src/Host/Plugins/Configuration/PluginApplicationConfiguration.cs b/src/Host/Plugins/Configuration/PluginApplicationConfiguration.cs
index 30444f4..cc199f7 100644
--- a/src/Host/Plugins/Configuration/PluginApplicationConfiguration.cs
+++ b/src/Host/Plugins/Configuration/PluginApplicationConfiguration.cs
@@ -2,6 +2,7 @@
using AuthKit.Plugins.Abstractions;
using AuthKit.Plugins.Abstractions.Contracts;
using Host.Plugins.Loading;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace Host.Plugins.Configuration;
diff --git a/src/Host/Plugins/Configuration/PluginConfigurationInvoker.cs b/src/Host/Plugins/Configuration/PluginConfigurationInvoker.cs
index 9b34448..24f0b47 100644
--- a/src/Host/Plugins/Configuration/PluginConfigurationInvoker.cs
+++ b/src/Host/Plugins/Configuration/PluginConfigurationInvoker.cs
@@ -1,6 +1,7 @@
using System.Reflection;
using AuthKit.Plugins.Abstractions.Contracts;
using AuthKit.Plugins.Abstractions.Contracts.Plugins;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace Host.Plugins.Configuration;
diff --git a/src/Host/Plugins/Contract/PluginContractValidator.cs b/src/Host/Plugins/Contract/PluginContractValidator.cs
index c987ea4..3a8e905 100644
--- a/src/Host/Plugins/Contract/PluginContractValidator.cs
+++ b/src/Host/Plugins/Contract/PluginContractValidator.cs
@@ -2,6 +2,7 @@
using AuthKit.Plugins.Abstractions.Contracts;
using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
using Microsoft.Extensions.Logging;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace Host.Plugins.Contract;
diff --git a/src/Host/Plugins/Lifecycle/PluginLifecycleHostedService.cs b/src/Host/Plugins/Lifecycle/PluginLifecycleHostedService.cs
index e89f1bf..552f2ce 100644
--- a/src/Host/Plugins/Lifecycle/PluginLifecycleHostedService.cs
+++ b/src/Host/Plugins/Lifecycle/PluginLifecycleHostedService.cs
@@ -1,5 +1,6 @@
using AuthKit.Plugins.Abstractions.Contracts;
using Host.Plugins.Loading;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace Host.Plugins.Lifecycle;
diff --git a/src/Host/Plugins/Loading/LoadedPlugin.cs b/src/Host/Plugins/Loading/LoadedPlugin.cs
index 7d45b49..8e2e1b9 100644
--- a/src/Host/Plugins/Loading/LoadedPlugin.cs
+++ b/src/Host/Plugins/Loading/LoadedPlugin.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using AuthKit.Plugins.Abstractions.Contracts;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace Host.Plugins.Loading;
diff --git a/src/Host/Plugins/Loading/PluginLoader.cs b/src/Host/Plugins/Loading/PluginLoader.cs
index c5d132b..78b8258 100644
--- a/src/Host/Plugins/Loading/PluginLoader.cs
+++ b/src/Host/Plugins/Loading/PluginLoader.cs
@@ -5,6 +5,7 @@
using AuthKit.Plugins.Abstractions.Models;
using Host.Plugins.Contract;
using Microsoft.Extensions.Logging;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace Host.Plugins.Loading;
diff --git a/src/Host/appsettings.json b/src/Host/appsettings.json
index b7cf050..d5bf9c6 100644
--- a/src/Host/appsettings.json
+++ b/src/Host/appsettings.json
@@ -2,8 +2,10 @@
"ConnectionStrings": {
"Marten": "Host=authdev-db;Port=5432;Database=AuthDev;Username=postgres;Password=postgres"
},
- "AuthKit": {
- "MaxDeveloperTokens": 3
+ "Plugins": {
+ "authkit.devtokens": {
+ "MaxDeveloperTokens": 3
+ }
},
"Encryption": {
"AES_MASTER_KEY": "Tww4kOE+310tZvqXvw1tg0779qccuy8t4vrYthcPP7c="
diff --git a/src/Plugins/Abstractions/AuthKitApiKeyLocation.cs b/src/Plugins/Abstractions/AuthKitApiKeyLocation.cs
index 0b7af4f..3d7c3b4 100644
--- a/src/Plugins/Abstractions/AuthKitApiKeyLocation.cs
+++ b/src/Plugins/Abstractions/AuthKitApiKeyLocation.cs
@@ -1,3 +1,5 @@
+using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
+
namespace AuthKit.Plugins.Abstractions;
///
diff --git a/src/Plugins/Abstractions/Contracts/IAuthKitPlugin.cs b/src/Plugins/Abstractions/Contracts/IAuthKitPlugin.cs
deleted file mode 100644
index 631b86c..0000000
--- a/src/Plugins/Abstractions/Contracts/IAuthKitPlugin.cs
+++ /dev/null
@@ -1,446 +0,0 @@
-using AuthKit.Plugins.Abstractions.Contracts.Plugins;
-using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
-using AuthKit.Plugins.Abstractions.Models;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Routing;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authorization;
-using System.Reflection;
-
-namespace AuthKit.Plugins.Abstractions.Contracts;
-
-///
-/// Defines the contract implemented by an AuthKit plugin.
-///
-///
-///
-/// AuthKit plugins are discovered and loaded dynamically by the host at startup.
-/// A plugin does not need to be directly referenced by the host project.
-///
-///
-/// The plugin contract allows an extension to contribute services, middleware,
-/// health checks, and OpenAPI security scheme metadata to the host application.
-///
-///
-/// Plugin implementations should keep their integration with the host limited
-/// to the abstractions exposed by this contract and should register any
-/// plugin-specific dependencies through .
-///
-///
-public interface IAuthKitPlugin
-{
- ///
- /// Gets the stable, host-unique identifier of the plugin.
- ///
- ///
- /// The ID is an author-declared identifier (e.g. "authkit.devtokens"),
- /// is expected to be non-empty and stable across restarts. The host is
- /// responsible for validating format and uniqueness before activation.
- ///
- string Id => Metadata.Id;
-
- ///
- /// Gets the unique name of the plugin.
- ///
- ///
- /// The name is used to identify the plugin in host diagnostics,
- /// startup output, and other plugin-related metadata.
- ///
- string Name => Metadata.Name;
-
- ///
- /// Gets an optional human-readable display name for UIs.
- ///
- ///
- /// The host UI should display DisplayName ?? Name when presenting
- /// the plugin to users.
- ///
- string? DisplayName => Metadata.DisplayName;
-
- ///
- /// Gets an optional human-readable description of the plugin.
- ///
- ///
- /// The host may display the description in startup output,
- /// diagnostics, administrative interfaces, or other status surfaces.
- ///
- string Description => Metadata.Description;
-
- ///
- /// Gets the version of the plugin as a semantic version (SemVer 2.0.0).
- ///
- ///
- /// The Version replaces the previous string-based version and exposes
- /// full semantic version semantics (parsing, equality, precedence).
- ///
- SemanticVersion Version => SemanticVersion.Parse(Metadata.Version);
-
- ///
- /// Optional author metadata, visible in catalogs and diagnostics.
- ///
- string? Author => Metadata.Author;
-
- ///
- /// Optional SPDX-style license string (no validation performed by host).
- ///
- string? License => Metadata.License;
-
- ///
- /// Optional absolute URI pointing to the license text.
- ///
- string? LicenseUrl => Metadata.LicenseUrl;
-
- ///
- /// Optional absolute HTTP/HTTPS URI pointing to a plugin homepage.
- ///
- string? Homepage => Metadata.Homepage;
-
- ///
- /// Optional absolute HTTP/HTTPS URI pointing to the plugin repository.
- ///
- string? RepositoryUrl => Metadata.RepositoryUrl;
-
- ///
- /// Optional classification tags for UI filtering. Defaults to empty.
- /// Null or whitespace elements are invalid and should be rejected during validation.
- /// Used for filtering plugins in UIs and catalogs.
- ///
- ///
- /// Tags are case-sensitive strings without controlled vocabulary.
- /// Example: ["security", "auth", "audit"].
- ///
- IReadOnlyList Tags => Metadata.Tags;
-
- ///
- /// Priority used for activation ordering among dependency-ready plugins.
- /// Lower values are activated earlier, higher values later.
- /// Defaults to 0.
- ///
- ///
- /// The ordering algorithm is: topological sort where, among the set of currently
- /// dependency-ready plugins, the next plugin is chosen by Priority ascending.
- /// Dependency order (G7) wins over Priority.
- /// Example: A (p. 100) → B (p-100, DependsOn A), C (p. 0) ⇒ order: A, C, B.
- ///
- int Priority => Metadata.Priority;
-
- ///
- /// Indicates whether the plugin is enabled. Defaults to true.
- ///
- ///
- /// If false, the plugin is skipped before loading (no consistency check runs for it).
- /// For plugins accepted by the preload gate and subsequently loaded,
- /// manifest.IsEnabled == instance.IsEnabled is part of consistency validation.
- ///
- bool IsEnabled => Metadata.IsEnabled;
-
- ///
- /// Features/capabilities exposed by the plugin. Contract
- /// requires Case-insensitive comparison. Defaults to an immutable empty set.
- ///
- ///
- /// Used for pre-activation capability checks (via ) and
- /// post-load consistency validation. Host checks capabilities using the
- /// extension method.
- /// Example: plugin.Supports("auth").
- ///
- IReadOnlySet Capabilities =>
- System.Collections.Immutable.ImmutableHashSet.CreateRange(StringComparer.OrdinalIgnoreCase, Metadata.Capabilities);
-
- ///
- /// Gets the metadata associated with the plugin.
- ///
- PluginMetadataAttribute Metadata => GetType().GetCustomAttribute()
- ?? throw new InvalidOperationException($"Plugin {GetType().Name} is missing [PluginMetadata] attribute.");
-
- ///
- /// Registers the plugin's services in the host dependency injection container.
- ///
- /// The host's dependency injection service collection.
- /// The host application configuration.
- ///
- ///
- /// This method is called while the host application is being configured,
- /// before the application is built.
- ///
- ///
- /// Plugins should register all services required by their functionality
- /// through this method rather than creating their own dependency injection
- /// container.
- ///
- ///
- void ConfigureServices(
- IServiceCollection services,
- IConfiguration configuration) =>
- throw new NotSupportedException(
- $"Plugin '{GetType().Name}' must implement a supported ConfigureServices overload.");
-
- ///
- /// Configures plugin services using the host application builder.
- ///
- /// The host application builder used by AuthKit.
- /// The application configuration.
- ///
- /// This overload is optional. Its default implementation delegates to the
- /// legacy service collection overload for existing plugins.
- ///
- void ConfigureServices(
- IHostApplicationBuilder builder,
- IConfiguration configuration) =>
- ConfigureServices(builder.Services, configuration);
-
- ///
- /// Configures plugin services with stable plugin context information.
- ///
- /// The service collection used by the host.
- /// The context for the plugin being configured.
- ///
- /// This overload is optional. Its default implementation delegates to the
- /// legacy service collection overload for existing plugins.
- ///
- void ConfigureServices(IServiceCollection services, AuthKitPluginContext context) =>
- ConfigureServices(services, context.Configuration);
-
- ///
- /// Performs an optional structured health check for the plugin.
- ///
- /// The root service provider of the host application.
- /// A token that can cancel the health check.
- ///
- /// One or more structured health results reported by the plugin.
- ///
- ///
- ///
- /// The host may invoke this method as part of its health endpoint.
- /// Plugins can resolve the services they require from
- /// to verify the availability of their
- /// dependencies.
- ///
- ///
- /// A plugin may return separate results for independent dependencies or
- /// capabilities. Cancellation must be propagated to cancellable operations
- /// and is not converted into a fabricated health result.
- ///
- ///
- /// The default implementation reports the plugin as healthy. Existing
- /// plugins that do not require custom health validation therefore do not
- /// need to implement this member.
- ///
- ///
- Task> CheckHealthAsync(
- IServiceProvider services,
- CancellationToken cancellationToken = default) =>
- Task.FromResult>(
- [new PluginHealthResult(PluginHealthStatus.Healthy)]);
-
- ///
- /// Gets the optional ASP.NET Core middleware type contributed by the plugin.
- ///
- ///
- ///
- /// When specified, the host inserts the middleware into its request
- /// processing pipeline at the plugin middleware slot.
- ///
- ///
- /// The middleware type must follow the conventional ASP.NET Core middleware
- /// pattern, including a constructor accepting
- /// and an InvokeAsync method accepting .
- /// Additional dependencies may be supplied through dependency injection.
- ///
- ///
- /// The default value is null, indicating that the plugin does not
- /// contribute middleware.
- ///
- ///
- Type? MiddlewareType => null;
-
- ///
- /// Registers plugin-owned endpoints during host endpoint configuration.
- ///
- /// The application's endpoint route builder.
- ///
- /// This optional hook runs after host services are configured and before
- /// the application starts processing requests. Exceptions are propagated.
- ///
- void MapEndpoints(IEndpointRouteBuilder endpoints)
- {
- }
-
- ///
- /// Configures plugin application middleware on the actual host application.
- ///
- /// The application's a live builder.
- ///
- /// When implemented, this hook takes precedence over
- /// to prevent accidental duplicate middleware registration.
- ///
- void ConfigureApplication(IApplicationBuilder application)
- {
- }
-
- ///
- /// Gets the explicit pipeline position used by .
- ///
- PluginPipelinePosition PipelinePosition => PluginPipelinePosition.BeforeAuthentication;
-
- ///
- /// Configures plugin middleware at the declared pipeline position.
- ///
- /// The application's a live builder.
- /// The position currently being configured.
- ///
- /// The host invokes this hook once at .
- /// Plugins at the same position are ordered by stable plugin ID.
- ///
- void ConfigurePipeline(IApplicationBuilder application, PluginPipelinePosition position)
- {
- }
-
- ///
- /// Gets the minimum host version required to load this plugin.
- ///
- ///
- /// If the host version is lower than , the plugin is rejected.
- ///
- SemanticVersion? MinHostVersion => string.IsNullOrEmpty(Metadata.MinHostVersion) ? null : SemanticVersion.Parse(Metadata.MinHostVersion);
-
- ///
- /// Gets the list of plugin IDs this plugin depends on.
- ///
- ///
- /// Each entry must be a valid plugin ID. The host validates that:
- /// - Dependencies exist among discovered plugins.
- /// - There are no self-dependencies.
- /// - There are no duplicate dependencies.
- /// - There are no dependency cycles.
- ///
- IReadOnlyList DependsOn => Metadata.DependsOn;
-
- ///
- /// Gets the OpenAPI security schemes contributed by the plugin.
- ///
- ///
- ///
- /// The host exposes security schemes returned by this method as
- /// part of its Swagger/OpenAPI security metadata.
- ///
- ///
- /// Plugins that do not contribute to security schemes can rely on the default
- /// empty collection.
- ///
- ///
- /// readonly dictionary keyed by the security scheme name.
- IReadOnlyDictionary GetSecuritySchemes() =>
- new Dictionary();
-
- ///
- /// Configures authentication schemes on the host-owned authentication builder.
- ///
- /// The actual host authentication builder.
- ///
- ///
- /// This optional hook is invoked once per plugin while the host configures its
- /// security infrastructure, before the service provider is built. Plugins are
- /// processed in ascending order, so invocation order is
- /// deterministic and does not depend on discovery order.
- ///
- ///
- /// The hook receives the same used by the host,
- /// so schemes registered here participate in the application authentication
- /// infrastructure. Plugin services, including handler dependencies, may be
- /// registered later through ConfigureServices.
- ///
- ///
- /// This optional hook does not change the host default scheme. Scheme names are
- /// globally significant; registering a name already owned by the host or by
- /// another plugin fails explicitly when the authentication options are built.
- ///
- ///
- void ConfigureAuthentication(AuthenticationBuilder builder)
- {
- }
-
- ///
- /// Configures authorization policies on the host-owned authorization options.
- ///
- /// The actual host authorization options.
- ///
- ///
- /// This optional hook is invoked once per plugin while the host configures its
- /// security infrastructure, before the service provider is built. Plugins are
- /// processed in ascending order, so invocation order is
- /// deterministic and does not depend on discovery order.
- ///
- ///
- /// Policies registered here are available through the standard ASP.NET Core
- /// authorization infrastructure and can protect endpoints mapped by any plugin.
- ///
- ///
- /// Policy names are globally significant. The host rejects duplicate ownership:
- /// a policy name already registered by another plugin fails to start up with an
- /// exception identifying both plugins. Plugins should use globally unique,
- /// preferably namespaced policy names.
- ///
- ///
- /// Existing default and fallback policies are not replaced by the host, and this
- /// hook must not assume ownership of such global defaults.
- ///
- ///
- void ConfigureAuthorization(AuthorizationOptions options)
- {
- }
-
- ///
- /// Binds strongly typed plugin options from the plugin configuration section using
- /// the standard options DI infrastructure.
- ///
- /// The plugin options type.
- /// The host service collection.
- /// The application configuration.
- ///
- ///
- /// Typically called from ConfigureServices. The section is resolved through
- /// :
- /// Plugins:{Id} wins when it exists, otherwise Plugins:{Name} is used,
- /// matching the scoping of .
- ///
- ///
- /// Registered options become readable through IOptions<TOptions> once
- /// the service provider is built.
- ///
- ///
- void BindConfiguration(IServiceCollection services, IConfiguration configuration)
- where TOptions : class =>
- services.Configure(this.GetPluginConfiguration(configuration));
-
- ///
- /// Initializes plugin runtime resources before the host is considered started.
- ///
- /// The host startup cancellation token.
- /// A task that completes when initialization is complete.
- Task OnStartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-
- ///
- /// Notifies the plugin after the host has started successfully.
- ///
- /// The host lifecycle cancellation token.
- /// A task that completes when post-start work is complete.
- Task OnStartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-
- ///
- /// Releases plugin runtime resources during a graceful host shutdown.
- ///
- /// The host shutdown cancellation token.
- /// A task that completes when shutdown preparation is complete.
- Task OnStoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-
- ///
- /// Gets hosted services owned by this plugin.
- ///
- /// A non-null collection of services registered in the host DI container.
- IReadOnlyList GetHostedServices() => [];
-}
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Configuration.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Configuration.cs
new file mode 100644
index 0000000..0d94f2a
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Configuration.cs
@@ -0,0 +1,80 @@
+using AuthKit.Plugins.Abstractions.Contracts.Plugins;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Registers the plugin's services in the host dependency injection container.
+ ///
+ /// The host's dependency injection service collection.
+ /// The host application configuration.
+ ///
+ ///
+ /// This method is called while the host application is being configured,
+ /// before the application is built.
+ ///
+ ///
+ /// Plugins should register all services required by their functionality
+ /// through this method rather than creating their own dependency injection
+ /// container.
+ ///
+ ///
+ void ConfigureServices(
+ IServiceCollection services,
+ IConfiguration configuration) =>
+ throw new NotSupportedException(
+ $"Plugin '{GetType().Name}' must implement a supported ConfigureServices overload.");
+
+ ///
+ /// Configures plugin services using the host application builder.
+ ///
+ /// The host application builder used by AuthKit.
+ /// The application configuration.
+ ///
+ /// This overload is optional. Its default implementation delegates to the
+ /// legacy service collection overload for existing plugins.
+ ///
+ void ConfigureServices(
+ IHostApplicationBuilder builder,
+ IConfiguration configuration) =>
+ ConfigureServices(builder.Services, configuration);
+
+ ///
+ /// Configures plugin services with stable plugin context information.
+ ///
+ /// The service collection used by the host.
+ /// The context for the plugin being configured.
+ ///
+ /// This overload is optional. Its default implementation delegates to the
+ /// legacy service collection overload for existing plugins.
+ ///
+ void ConfigureServices(IServiceCollection services, AuthKitPluginContext context) =>
+ ConfigureServices(services, context.Configuration);
+
+ ///
+ /// Binds strongly typed plugin options from the plugin configuration section using
+ /// the standard options DI infrastructure.
+ ///
+ /// The plugin options type.
+ /// The host service collection.
+ /// The application configuration.
+ ///
+ ///
+ /// Typically called from ConfigureServices. The section is resolved through
+ /// :
+ /// Plugins:{Id} wins when it exists, otherwise Plugins:{Name} is used,
+ /// matching the scoping of .
+ ///
+ ///
+ /// Registered options become readable through IOptions<TOptions> once
+ /// the service provider is built.
+ ///
+ ///
+ void BindConfiguration(IServiceCollection services, IConfiguration configuration)
+ where TOptions : class =>
+ services.Configure(this.GetPluginConfiguration(configuration));
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Health.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Health.cs
new file mode 100644
index 0000000..cb2e774
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Health.cs
@@ -0,0 +1,38 @@
+using AuthKit.Plugins.Abstractions.Models;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Performs an optional structured health check for the plugin.
+ ///
+ /// The root service provider of the host application.
+ /// A token that can cancel the health check.
+ ///
+ /// One or more structured health results reported by the plugin.
+ ///
+ ///
+ ///
+ /// The host may invoke this method as part of its health endpoint.
+ /// Plugins can resolve the services they require from
+ /// to verify the availability of their
+ /// dependencies.
+ ///
+ ///
+ /// A plugin may return separate results for independent dependencies or
+ /// capabilities. Cancellation must be propagated to cancellable operations
+ /// and is not converted into a fabricated health result.
+ ///
+ ///
+ /// The default implementation reports the plugin as healthy. Existing
+ /// plugins that do not require custom health validation therefore do not
+ /// need to implement this member.
+ ///
+ ///
+ Task> CheckHealthAsync(
+ IServiceProvider services,
+ CancellationToken cancellationToken = default) =>
+ Task.FromResult>(
+ [new PluginHealthResult(PluginHealthStatus.Healthy)]);
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.HostRequirements.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.HostRequirements.cs
new file mode 100644
index 0000000..d7e8986
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.HostRequirements.cs
@@ -0,0 +1,26 @@
+using AuthKit.Plugins.Abstractions.Models;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Gets the minimum host version required to load this plugin.
+ ///
+ ///
+ /// If the host version is lower than , the plugin is rejected.
+ ///
+ SemanticVersion? MinHostVersion => string.IsNullOrEmpty(Metadata.MinHostVersion) ? null : SemanticVersion.Parse(Metadata.MinHostVersion);
+
+ ///
+ /// Gets the list of plugin IDs this plugin depends on.
+ ///
+ ///
+ /// Each entry must be a valid plugin ID. The host validates that:
+ /// - Dependencies exist among discovered plugins.
+ /// - There are no self-dependencies.
+ /// - There are no duplicate dependencies.
+ /// - There are no dependency cycles.
+ ///
+ IReadOnlyList DependsOn => Metadata.DependsOn;
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Lifecycle.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Lifecycle.cs
new file mode 100644
index 0000000..beca798
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Lifecycle.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Hosting;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Initializes plugin runtime resources before the host is considered started.
+ ///
+ /// The host startup cancellation token.
+ /// A task that completes when initialization is complete.
+ Task OnStartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ /// Notifies the plugin after the host has started successfully.
+ ///
+ /// The host lifecycle cancellation token.
+ /// A task that completes when post-start work is complete.
+ Task OnStartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ /// Releases plugin runtime resources during a graceful host shutdown.
+ ///
+ /// The host shutdown cancellation token.
+ /// A task that completes when shutdown preparation is complete.
+ Task OnStoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ /// Gets hosted services owned by this plugin.
+ ///
+ /// A non-null collection of services registered in the host DI container.
+ IReadOnlyList GetHostedServices() => [];
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Pipeline.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Pipeline.cs
new file mode 100644
index 0000000..3510699
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Pipeline.cs
@@ -0,0 +1,71 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Gets the optional ASP.NET Core middleware type contributed by the plugin.
+ ///
+ ///
+ ///
+ /// When specified, the host inserts the middleware into its request
+ /// processing pipeline at the plugin middleware slot.
+ ///
+ ///
+ /// The middleware type must follow the conventional ASP.NET Core middleware
+ /// pattern, including a constructor accepting
+ /// and an InvokeAsync method accepting .
+ /// Additional dependencies may be supplied through dependency injection.
+ ///
+ ///
+ /// The default value is null, indicating that the plugin does not
+ /// contribute middleware.
+ ///
+ ///
+ Type? MiddlewareType => null;
+
+ ///
+ /// Registers plugin-owned endpoints during host endpoint configuration.
+ ///
+ /// The application's endpoint route builder.
+ ///
+ /// This optional hook runs after host services are configured and before
+ /// the application starts processing requests. Exceptions are propagated.
+ ///
+ void MapEndpoints(IEndpointRouteBuilder endpoints)
+ {
+ }
+
+ ///
+ /// Configures plugin application middleware on the actual host application.
+ ///
+ /// The application's a live builder.
+ ///
+ /// When implemented, this hook takes precedence over
+ /// to prevent accidental duplicate middleware registration.
+ ///
+ void ConfigureApplication(IApplicationBuilder application)
+ {
+ }
+
+ ///
+ /// Gets the explicit pipeline position used by .
+ ///
+ PluginPipelinePosition PipelinePosition => PluginPipelinePosition.BeforeAuthentication;
+
+ ///
+ /// Configures plugin middleware at the declared pipeline position.
+ ///
+ /// The application's a live builder.
+ /// The position currently being configured.
+ ///
+ /// The host invokes this hook once at .
+ /// Plugins at the same position are ordered by stable plugin ID.
+ ///
+ void ConfigurePipeline(IApplicationBuilder application, PluginPipelinePosition position)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Security.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Security.cs
new file mode 100644
index 0000000..cfdc729
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.Security.cs
@@ -0,0 +1,82 @@
+using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authorization;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Gets the OpenAPI security schemes contributed by the plugin.
+ ///
+ ///
+ ///
+ /// The host exposes security schemes returned by this method as
+ /// part of its Swagger/OpenAPI security metadata.
+ ///
+ ///
+ /// Plugins that do not contribute to security schemes can rely on the default
+ /// empty collection.
+ ///
+ ///
+ /// readonly dictionary keyed by the security scheme name.
+ IReadOnlyDictionary GetSecuritySchemes() =>
+ new Dictionary();
+
+ ///
+ /// Configures authentication schemes on the host-owned authentication builder.
+ ///
+ /// The actual host authentication builder.
+ ///
+ ///
+ /// This optional hook is invoked once per plugin while the host configures its
+ /// security infrastructure, before the service provider is built. Plugins are
+ /// processed in ascending order, so invocation order is
+ /// deterministic and does not depend on discovery order.
+ ///
+ ///
+ /// The hook receives the same used by the host,
+ /// so schemes registered here participate in the application authentication
+ /// infrastructure. Plugin services, including handler dependencies, may be
+ /// registered later through ConfigureServices.
+ ///
+ ///
+ /// This optional hook does not change the host default scheme. Scheme names are
+ /// globally significant; registering a name already owned by the host or by
+ /// another plugin fails explicitly when the authentication options are built.
+ ///
+ ///
+ void ConfigureAuthentication(AuthenticationBuilder builder)
+ {
+ }
+
+ ///
+ /// Configures authorization policies on the host-owned authorization options.
+ ///
+ /// The actual host authorization options.
+ ///
+ ///
+ /// This optional hook is invoked once per plugin while the host configures its
+ /// security infrastructure, before the service provider is built. Plugins are
+ /// processed in ascending order, so invocation order is
+ /// deterministic and does not depend on discovery order.
+ ///
+ ///
+ /// Policies registered here are available through the standard ASP.NET Core
+ /// authorization infrastructure and can protect endpoints mapped by any plugin.
+ ///
+ ///
+ /// Policy names are globally significant. The host rejects duplicate ownership:
+ /// a policy name already registered by another plugin fails to start up with an
+ /// exception identifying both plugins. Plugins should use globally unique,
+ /// preferably namespaced policy names.
+ ///
+ ///
+ /// Existing default and fallback policies are not replaced by the host, and this
+ /// hook must not assume ownership of such global defaults.
+ ///
+ ///
+ void ConfigureAuthorization(AuthorizationOptions options)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.cs b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.cs
new file mode 100644
index 0000000..90f6d7d
--- /dev/null
+++ b/src/Plugins/Abstractions/Contracts/PluginContract/IAuthKitPlugin.cs
@@ -0,0 +1,156 @@
+using System.Reflection;
+using AuthKit.Plugins.Abstractions.Contracts.Plugins;
+using AuthKit.Plugins.Abstractions.Models;
+
+namespace AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
+///
+/// Defines the contract implemented by an AuthKit plugin.
+///
+///
+///
+/// AuthKit plugins are discovered and loaded dynamically by the host at startup.
+/// A plugin does not need to be directly referenced by the host project.
+///
+///
+/// The plugin contract allows an extension to contribute services, middleware,
+/// health checks, and OpenAPI security scheme metadata to the host application.
+///
+///
+/// Plugin implementations should keep their integration with the host limited
+/// to the abstractions exposed by this contract and should register any
+/// plugin-specific dependencies through .
+///
+///
+/// The contract surface is split into cohesive partial declarations grouped
+/// into the IAuthKitPlugin*.cs files in this folder: the base identity
+/// and metadata accessors, host requirements, configuration, health, pipeline,
+/// security, and lifecycle hooks.
+///
+///
+public partial interface IAuthKitPlugin
+{
+ ///
+ /// Gets the stable, host-unique identifier of the plugin.
+ ///
+ ///
+ /// The ID is an author-declared identifier (e.g. "authkit.devtokens"),
+ /// is expected to be non-empty and stable across restarts. The host is
+ /// responsible for validating format and uniqueness before activation.
+ ///
+ string Id => Metadata.Id;
+
+ ///
+ /// Gets the unique name of the plugin.
+ ///
+ ///
+ /// The name is used to identify the plugin in host diagnostics,
+ /// startup output, and other plugin-related metadata.
+ ///
+ string Name => Metadata.Name;
+
+ ///
+ /// Gets an optional human-readable display name for UIs.
+ ///
+ ///
+ /// The host UI should display DisplayName ?? Name when presenting
+ /// the plugin to users.
+ ///
+ string? DisplayName => Metadata.DisplayName;
+
+ ///
+ /// Gets an optional human-readable description of the plugin.
+ ///
+ ///
+ /// The host may display the description in startup output,
+ /// diagnostics, administrative interfaces, or other status surfaces.
+ ///
+ string Description => Metadata.Description;
+
+ ///
+ /// Gets the version of the plugin as a semantic version (SemVer 2.0.0).
+ ///
+ ///
+ /// The Version replaces the previous string-based version and exposes
+ /// full semantic version semantics (parsing, equality, precedence).
+ ///
+ SemanticVersion Version => SemanticVersion.Parse(Metadata.Version);
+
+ ///
+ /// Optional author metadata, visible in catalogs and diagnostics.
+ ///
+ string? Author => Metadata.Author;
+
+ ///
+ /// Optional SPDX-style license string (no validation performed by host).
+ ///
+ string? License => Metadata.License;
+
+ ///
+ /// Optional absolute URI pointing to the license text.
+ ///
+ string? LicenseUrl => Metadata.LicenseUrl;
+
+ ///
+ /// Optional absolute HTTP/HTTPS URI pointing to a plugin homepage.
+ ///
+ string? Homepage => Metadata.Homepage;
+
+ ///
+ /// Optional absolute HTTP/HTTPS URI pointing to the plugin repository.
+ ///
+ string? RepositoryUrl => Metadata.RepositoryUrl;
+
+ ///
+ /// Optional classification tags for UI filtering. Defaults to empty.
+ /// Null or whitespace elements are invalid and should be rejected during validation.
+ /// Used for filtering plugins in UIs and catalogs.
+ ///
+ ///
+ /// Tags are case-sensitive strings without controlled vocabulary.
+ /// Example: ["security", "auth", "audit"].
+ ///
+ IReadOnlyList Tags => Metadata.Tags;
+
+ ///
+ /// Priority used for activation ordering among dependency-ready plugins.
+ /// Lower values are activated earlier, higher values later.
+ /// Defaults to 0.
+ ///
+ ///
+ /// The ordering algorithm is: topological sort where, among the set of currently
+ /// dependency-ready plugins, the next plugin is chosen by Priority ascending.
+ /// Dependency order (G7) wins over Priority.
+ /// Example: A (p. 100) → B (p-100, DependsOn A), C (p. 0) ⇒ order: A, C, B.
+ ///
+ int Priority => Metadata.Priority;
+
+ ///
+ /// Indicates whether the plugin is enabled. Defaults to true.
+ ///
+ ///
+ /// If false, the plugin is skipped before loading (no consistency check runs for it).
+ /// For plugins accepted by the preload gate and subsequently loaded,
+ /// manifest.IsEnabled == instance.IsEnabled is part of consistency validation.
+ ///
+ bool IsEnabled => Metadata.IsEnabled;
+
+ ///
+ /// Features/capabilities exposed by the plugin. Contract
+ /// requires Case-insensitive comparison. Defaults to an immutable empty set.
+ ///
+ ///
+ /// Used for pre-activation capability checks (via ) and
+ /// post-load consistency validation. Host checks capabilities using the
+ /// extension method.
+ /// Example: plugin.Supports("auth").
+ ///
+ IReadOnlySet Capabilities =>
+ System.Collections.Immutable.ImmutableHashSet.CreateRange(StringComparer.OrdinalIgnoreCase, Metadata.Capabilities);
+
+ ///
+ /// Gets the metadata associated with the plugin.
+ ///
+ PluginMetadataAttribute Metadata => GetType().GetCustomAttribute()
+ ?? throw new InvalidOperationException($"Plugin {GetType().Name} is missing [PluginMetadata] attribute.");
+}
\ No newline at end of file
diff --git a/src/Plugins/Abstractions/Contracts/PluginValidator.cs b/src/Plugins/Abstractions/Contracts/PluginValidator.cs
index 18eb7fd..26a60e1 100644
--- a/src/Plugins/Abstractions/Contracts/PluginValidator.cs
+++ b/src/Plugins/Abstractions/Contracts/PluginValidator.cs
@@ -77,7 +77,7 @@ public static void ValidateDependsOn(IReadOnlyList dependsOn, string plu
}
}
- // Check for self dependency
+ // Check for self-dependency
if (dependsOn.Contains(pluginId, StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Plugin cannot depend on itself.");
@@ -91,7 +91,7 @@ public static void ValidateDependsOn(IReadOnlyList dependsOn, string plu
/// The plugin instance.
/// Thrown if either manifest or plugin is null.
/// Thrown if the manifest and instance are inconsistent.
- public static void ValidateConsistency(PluginManifest manifest, IAuthKitPlugin plugin)
+ public static void ValidateConsistency(PluginManifest manifest, PluginContract.IAuthKitPlugin plugin)
{
if (manifest == null)
{
diff --git a/src/Plugins/Abstractions/Contracts/Plugins/PluginExtensions.cs b/src/Plugins/Abstractions/Contracts/Plugins/PluginExtensions.cs
index 51e07f7..ed7fb5d 100644
--- a/src/Plugins/Abstractions/Contracts/Plugins/PluginExtensions.cs
+++ b/src/Plugins/Abstractions/Contracts/Plugins/PluginExtensions.cs
@@ -10,7 +10,7 @@ namespace AuthKit.Plugins.Abstractions.Contracts.Plugins;
public static class PluginExtensions
{
/// The plugin whose configuration section is used.
- extension(IAuthKitPlugin plugin)
+ extension(PluginContract.IAuthKitPlugin plugin)
{
///
/// Binds strongly typed options from the plugin's configuration section.
diff --git a/src/Plugins/Abstractions/Contracts/Plugins/PluginMetadataExtensions.cs b/src/Plugins/Abstractions/Contracts/Plugins/PluginMetadataExtensions.cs
index 63faedc..b10684d 100644
--- a/src/Plugins/Abstractions/Contracts/Plugins/PluginMetadataExtensions.cs
+++ b/src/Plugins/Abstractions/Contracts/Plugins/PluginMetadataExtensions.cs
@@ -14,7 +14,7 @@ public static class PluginMetadataExtensions
/// The plugin instance.
/// An immutable record.
/// Thrown if the plugin is null.
- public static PluginMetadata GetMetadata(this IAuthKitPlugin plugin)
+ public static PluginMetadata GetMetadata(this PluginContract.IAuthKitPlugin plugin)
{
ArgumentNullException.ThrowIfNull(plugin);
diff --git a/src/Plugins/Abstractions/Contracts/SecuritySchemes/AuthKitSecuritySchemeType.cs b/src/Plugins/Abstractions/Contracts/SecuritySchemes/AuthKitSecuritySchemeType.cs
index 64e273b..e10d42d 100644
--- a/src/Plugins/Abstractions/Contracts/SecuritySchemes/AuthKitSecuritySchemeType.cs
+++ b/src/Plugins/Abstractions/Contracts/SecuritySchemes/AuthKitSecuritySchemeType.cs
@@ -1,6 +1,4 @@
-using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
-
-namespace AuthKit.Plugins.Abstractions;
+namespace AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
///
/// Specifies the authentication mechanism represented by an AuthKit security scheme.
diff --git a/src/Plugins/Abstractions/Contracts/SecuritySchemes/SecuritySchemeAttribute.cs b/src/Plugins/Abstractions/Contracts/SecuritySchemes/SecuritySchemeAttribute.cs
index 3c50fb8..47ae52d 100644
--- a/src/Plugins/Abstractions/Contracts/SecuritySchemes/SecuritySchemeAttribute.cs
+++ b/src/Plugins/Abstractions/Contracts/SecuritySchemes/SecuritySchemeAttribute.cs
@@ -1,3 +1,5 @@
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
+
namespace AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
///
@@ -12,7 +14,7 @@ namespace AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
///
///
/// The scheme name must match a key returned by an enabled plugin's
-/// ; otherwise the host rejects
+/// ; otherwise the host rejects
/// requests to the endpoint as a configuration error.
///
///
diff --git a/src/Plugins/Abstractions/Models/PluginManifest.cs b/src/Plugins/Abstractions/Models/PluginManifest.cs
index 85872a0..df3c0c2 100644
--- a/src/Plugins/Abstractions/Models/PluginManifest.cs
+++ b/src/Plugins/Abstractions/Models/PluginManifest.cs
@@ -1,4 +1,5 @@
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
using AuthKit.Plugins.Abstractions.Contracts.Plugins;
namespace AuthKit.Plugins.Abstractions.Models;
@@ -13,7 +14,7 @@ namespace AuthKit.Plugins.Abstractions.Models;
///
///
/// The manifest mirrors selected runtime metadata exposed by
-/// while remaining independent of a plugin instance.
+/// while remaining independent of a plugin instance.
/// This allows the host to perform pre-activation validation without loading
/// or activating the plugin.
///
diff --git a/src/Plugins/Abstractions/Models/PluginMetadata.cs b/src/Plugins/Abstractions/Models/PluginMetadata.cs
index 52d74bf..0469275 100644
--- a/src/Plugins/Abstractions/Models/PluginMetadata.cs
+++ b/src/Plugins/Abstractions/Models/PluginMetadata.cs
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
namespace AuthKit.Plugins.Abstractions.Models;
@@ -7,7 +8,7 @@ namespace AuthKit.Plugins.Abstractions.Models;
/// Represents an immutable runtime snapshot of plugin metadata.
///
///
-/// This record aggregates all metadata from
+/// This record aggregates all metadata from
/// and . It is created after the plugin is loaded
/// and provides a consistent, immutable view of the plugin's metadata.
///
diff --git a/src/Plugins/Solutions/DevTokens/DevTokensPlugin.cs b/src/Plugins/Solutions/DevTokens/DevTokensPlugin.cs
index d628210..28f1234 100644
--- a/src/Plugins/Solutions/DevTokens/DevTokensPlugin.cs
+++ b/src/Plugins/Solutions/DevTokens/DevTokensPlugin.cs
@@ -14,8 +14,8 @@
using DevTokens.UseCase.Commands.Validations;
using Marten;
using Microsoft.AspNetCore.Authorization;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace DevTokens;
@@ -56,10 +56,10 @@ public sealed class DevTokensPlugin : IAuthKitPlugin
/// and authorization components in the dependency injection container.
///
/// The used to register plugin services.
- /// Application configuration used to configure .
- public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
+ /// Stable plugin context including the plugin-scoped configuration section.
+ public void ConfigureServices(IServiceCollection services, AuthKitPluginContext context)
{
- services.Configure(configuration.GetSection("AuthKit"));
+ services.Configure(context.Configuration);
services.AddScoped();
services.AddScoped();
diff --git a/src/Plugins/Solutions/DevTools/DevToolsPlugin.cs b/src/Plugins/Solutions/DevTools/DevToolsPlugin.cs
index 341f1ee..3e6d922 100644
--- a/src/Plugins/Solutions/DevTools/DevToolsPlugin.cs
+++ b/src/Plugins/Solutions/DevTools/DevToolsPlugin.cs
@@ -8,6 +8,7 @@
using DevTools.Runtime;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace DevTools;
@@ -51,10 +52,10 @@ public sealed class DevToolsPlugin : IAuthKitPlugin
/// serving components in the dependency injection container.
///
/// The used to register plugin services.
- /// Application configuration used to configure .
- public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
+ /// Stable plugin context including the plugin-scoped configuration section.
+ public void ConfigureServices(IServiceCollection services, AuthKitPluginContext context)
{
- services.Configure(configuration.GetSection("DevTools"));
+ services.Configure(context.Configuration);
services.AddSingleton();
services.AddSingleton();
diff --git a/tests/Host.IntegrationTests/PluginLifecycleIntegrationTests.cs b/tests/Host.IntegrationTests/PluginLifecycleIntegrationTests.cs
index 0f5f337..a0696c0 100644
--- a/tests/Host.IntegrationTests/PluginLifecycleIntegrationTests.cs
+++ b/tests/Host.IntegrationTests/PluginLifecycleIntegrationTests.cs
@@ -7,6 +7,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.IntegrationTests;
diff --git a/tests/Host/PluginApplicationConfigurationTests.cs b/tests/Host/PluginApplicationConfigurationTests.cs
index 2083333..3673ea7 100644
--- a/tests/Host/PluginApplicationConfigurationTests.cs
+++ b/tests/Host/PluginApplicationConfigurationTests.cs
@@ -9,6 +9,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginConfigurationInvokerTests.cs b/tests/Host/PluginConfigurationInvokerTests.cs
index fd33a10..3667b0a 100644
--- a/tests/Host/PluginConfigurationInvokerTests.cs
+++ b/tests/Host/PluginConfigurationInvokerTests.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginContractValidatorTests.cs b/tests/Host/PluginContractValidatorTests.cs
index 201e1c1..9c09a4f 100644
--- a/tests/Host/PluginContractValidatorTests.cs
+++ b/tests/Host/PluginContractValidatorTests.cs
@@ -7,6 +7,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginHealthEndpointTests.cs b/tests/Host/PluginHealthEndpointTests.cs
index dcd5408..cef0ec7 100644
--- a/tests/Host/PluginHealthEndpointTests.cs
+++ b/tests/Host/PluginHealthEndpointTests.cs
@@ -15,6 +15,7 @@
using PluginMetadataAttribute = AuthKit.Plugins.Abstractions.Contracts.Plugins.PluginMetadataAttribute;
using Xunit;
using Host.Plugins.Health;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginHealthExecutorTests.cs b/tests/Host/PluginHealthExecutorTests.cs
index bae0333..23a6059 100644
--- a/tests/Host/PluginHealthExecutorTests.cs
+++ b/tests/Host/PluginHealthExecutorTests.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginInfrastructureIntegrationTests.cs b/tests/Host/PluginInfrastructureIntegrationTests.cs
index d571562..eb87375 100644
--- a/tests/Host/PluginInfrastructureIntegrationTests.cs
+++ b/tests/Host/PluginInfrastructureIntegrationTests.cs
@@ -16,6 +16,7 @@
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginIntegrationContractTests.cs b/tests/Host/PluginIntegrationContractTests.cs
index 01e1556..43e6092 100644
--- a/tests/Host/PluginIntegrationContractTests.cs
+++ b/tests/Host/PluginIntegrationContractTests.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginLifecycleHostedServiceTests.cs b/tests/Host/PluginLifecycleHostedServiceTests.cs
index 1f4e242..72431d1 100644
--- a/tests/Host/PluginLifecycleHostedServiceTests.cs
+++ b/tests/Host/PluginLifecycleHostedServiceTests.cs
@@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginSecurityConfigurationTests.cs b/tests/Host/PluginSecurityConfigurationTests.cs
index 85a4355..4bf5563 100644
--- a/tests/Host/PluginSecurityConfigurationTests.cs
+++ b/tests/Host/PluginSecurityConfigurationTests.cs
@@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Host/PluginSecurityEndpointIntegrationTests.cs b/tests/Host/PluginSecurityEndpointIntegrationTests.cs
index 999ea50..4e848cc 100644
--- a/tests/Host/PluginSecurityEndpointIntegrationTests.cs
+++ b/tests/Host/PluginSecurityEndpointIntegrationTests.cs
@@ -15,6 +15,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Host.Tests;
diff --git a/tests/Plugins/Abstractions/AuthKitSecuritySchemeTypeTests.cs b/tests/Plugins/Abstractions/AuthKitSecuritySchemeTypeTests.cs
index 5f2e166..337b3af 100644
--- a/tests/Plugins/Abstractions/AuthKitSecuritySchemeTypeTests.cs
+++ b/tests/Plugins/Abstractions/AuthKitSecuritySchemeTypeTests.cs
@@ -1,5 +1,5 @@
using System;
-using AuthKit.Plugins.Abstractions;
+using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
using Xunit;
namespace AuthKit.Plugins.Abstractions.Tests.SecuritySchemes;
diff --git a/tests/Plugins/Abstractions/IAuthKitPluginCapabilitiesTests.cs b/tests/Plugins/Abstractions/IAuthKitPluginCapabilitiesTests.cs
index 1f04fa0..2f8ca70 100644
--- a/tests/Plugins/Abstractions/IAuthKitPluginCapabilitiesTests.cs
+++ b/tests/Plugins/Abstractions/IAuthKitPluginCapabilitiesTests.cs
@@ -1,6 +1,7 @@
using AuthKit.Plugins.Abstractions.Contracts;
using AuthKit.Plugins.Abstractions.Contracts.Plugins;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Plugins.Abstractions.Tests;
diff --git a/tests/Plugins/Abstractions/PluginHealthResultTests.cs b/tests/Plugins/Abstractions/PluginHealthResultTests.cs
index 318cda8..c5c2de2 100644
--- a/tests/Plugins/Abstractions/PluginHealthResultTests.cs
+++ b/tests/Plugins/Abstractions/PluginHealthResultTests.cs
@@ -3,6 +3,7 @@
using AuthKit.Plugins.Abstractions.Contracts.Plugins;
using AuthKit.Plugins.Abstractions.Models;
using Xunit;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.Plugins.Abstractions.Tests;
diff --git a/tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj b/tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj
index 0889449..feed372 100644
--- a/tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj
+++ b/tools/AuthKit.PluginContractValidator/AuthKit.PluginContractValidator.csproj
@@ -15,5 +15,6 @@
+
\ No newline at end of file
diff --git a/tools/AuthKit.PluginContractValidator/PluginAssemblyLoader.cs b/tools/AuthKit.PluginContractValidator/PluginAssemblyLoader.cs
index a5cf4b0..54a0861 100644
--- a/tools/AuthKit.PluginContractValidator/PluginAssemblyLoader.cs
+++ b/tools/AuthKit.PluginContractValidator/PluginAssemblyLoader.cs
@@ -6,6 +6,7 @@
using System.Threading;
using AuthKit.PluginContractValidator.Core;
using AuthKit.Plugins.Abstractions.Contracts;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.PluginContractValidator;
diff --git a/tools/AuthKit.PluginContractValidator/src/Core/IPluginLoader.cs b/tools/AuthKit.PluginContractValidator/src/Core/IPluginLoader.cs
index f0892d8..4a22da8 100644
--- a/tools/AuthKit.PluginContractValidator/src/Core/IPluginLoader.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Core/IPluginLoader.cs
@@ -1,15 +1,16 @@
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
namespace AuthKit.PluginContractValidator.Core;
///
-/// Loads plugin entry assembly and instantiates its implementation.
+/// Loads plugin entry assembly and instantiates its implementation.
///
public interface IPluginLoader
{
///
/// Loads the plugin entry assembly located at and activates its
- /// implementation.
+ /// implementation.
///
/// The full path to the plugin's entry assembly (named after its directory).
///
diff --git a/tools/AuthKit.PluginContractValidator/src/Core/LoadedPlugin.cs b/tools/AuthKit.PluginContractValidator/src/Core/LoadedPlugin.cs
index 4c16fa1..3d63c92 100644
--- a/tools/AuthKit.PluginContractValidator/src/Core/LoadedPlugin.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Core/LoadedPlugin.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using AuthKit.Plugins.Abstractions.Contracts;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
namespace AuthKit.PluginContractValidator.Core;
diff --git a/tools/AuthKit.PluginContractValidator/src/Core/PluginConfigurationInvoker.cs b/tools/AuthKit.PluginContractValidator/src/Core/PluginConfigurationInvoker.cs
new file mode 100644
index 0000000..8f6d168
--- /dev/null
+++ b/tools/AuthKit.PluginContractValidator/src/Core/PluginConfigurationInvoker.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Reflection;
+using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.Plugins;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using IAuthKitPlugin = AuthKit.Plugins.Abstractions.Contracts.PluginContract.IAuthKitPlugin;
+
+namespace AuthKit.PluginContractValidator.Core;
+
+///
+/// Selects and invokes the most specific ConfigureServices overload a plugin
+/// implements, mirroring the selection performed by the AuthKit host.
+///
+///
+///
+/// Plugins may implement any single supported ConfigureServices overload. The
+/// invoker picks the most specific one actually overridden by the plugin instead of
+/// requiring all plugins to adopt a single signature.
+///
+///
+/// The default interface implementations of IAuthKitPlugin.ConfigureServices
+/// forward to one another, but the concrete method a plugin implements is only visible
+/// on the concrete type, so the overload is resolved through reflection exactly as the
+/// host resolver does.
+///
+///
+internal static class PluginConfigurationInvoker
+{
+ ///
+ /// Invokes the plugin's ConfigureServices, returning the service collection the
+ /// plugin registered into.
+ ///
+ /// The loaded plugin to configure.
+ /// The service collection the plugin should register into.
+ /// The application configuration used to build the plugin context.
+ /// The service collection containing the plugin's registrations.
+ public static IServiceCollection Configure(
+ IAuthKitPlugin plugin,
+ IServiceCollection services,
+ IConfiguration configuration)
+ {
+ var pluginType = plugin.GetType();
+
+ if (HasImplementation(pluginType, typeof(IServiceCollection), typeof(AuthKitPluginContext)))
+ {
+ plugin.ConfigureServices(
+ services,
+ new AuthKitPluginContext(
+ plugin.Id,
+ plugin.Name,
+ plugin.GetPluginConfiguration(configuration),
+ configuration));
+ return services;
+ }
+
+ if (HasImplementation(pluginType, typeof(IHostApplicationBuilder), typeof(IConfiguration)))
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings());
+ plugin.ConfigureServices(builder, configuration);
+ return builder.Services;
+ }
+
+ plugin.ConfigureServices(services, configuration);
+ return services;
+ }
+
+ ///
+ /// Determines whether a plugin provides a concrete implementation of the
+ /// ConfigureServices overload identified by the given parameter types.
+ ///
+ /// The plugin type to inspect.
+ /// The parameter types that identify the overload.
+ ///
+ /// true when the plugin overrides the overload; otherwise, false.
+ ///
+ private static bool HasImplementation(Type pluginType, params Type[] parameterTypes)
+ {
+ var method = pluginType.GetMethod(
+ nameof(IAuthKitPlugin.ConfigureServices),
+ BindingFlags.Instance | BindingFlags.Public,
+ binder: null,
+ types: parameterTypes,
+ modifiers: null);
+
+ return method is not null && method.DeclaringType != typeof(IAuthKitPlugin);
+ }
+}
\ No newline at end of file
diff --git a/tools/AuthKit.PluginContractValidator/src/Rules/HealthRule.cs b/tools/AuthKit.PluginContractValidator/src/Rules/HealthRule.cs
index d70e05f..cc02ee8 100644
--- a/tools/AuthKit.PluginContractValidator/src/Rules/HealthRule.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Rules/HealthRule.cs
@@ -16,7 +16,7 @@ namespace AuthKit.PluginContractValidator.Rules;
///
///
/// The rule builds an isolated service provider from the services registered
-/// by the plugin and invokes .
+/// by the plugin and invokes .
/// Basic framework services the host would provide, such as logging, are
/// registered so plugins can rely on them. Any exception during service
/// configuration or health checking is reported as a contract violation.
@@ -48,7 +48,7 @@ public async Task> ValidateAsync(
try
{
- plugin.Instance.ConfigureServices(services, configuration);
+ PluginConfigurationInvoker.Configure(plugin.Instance, services, configuration);
}
catch (Exception ex)
{
diff --git a/tools/AuthKit.PluginContractValidator/src/Rules/MetadataRule.cs b/tools/AuthKit.PluginContractValidator/src/Rules/MetadataRule.cs
index bdcb933..bca9d8c 100644
--- a/tools/AuthKit.PluginContractValidator/src/Rules/MetadataRule.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Rules/MetadataRule.cs
@@ -3,6 +3,7 @@
using System.Threading.Tasks;
using AuthKit.PluginContractValidator.Core;
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
namespace AuthKit.PluginContractValidator.Rules;
@@ -10,8 +11,8 @@ namespace AuthKit.PluginContractValidator.Rules;
/// Ensures plugin metadata (Name, Version) is present and well-formed.
///
///
-/// The rule verifies that is non-empty and that
-/// follows the SemVer
+/// The rule verifies that is non-empty and that
+/// follows the SemVer
/// (major.minor.patch[-prerelease]) format expected by the host.
///
public sealed class MetadataRule : IPluginContractRule
diff --git a/tools/AuthKit.PluginContractValidator/src/Rules/MiddlewareRule.cs b/tools/AuthKit.PluginContractValidator/src/Rules/MiddlewareRule.cs
index 25a4166..44d8227 100644
--- a/tools/AuthKit.PluginContractValidator/src/Rules/MiddlewareRule.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Rules/MiddlewareRule.cs
@@ -5,6 +5,7 @@
using System.Threading.Tasks;
using AuthKit.PluginContractValidator.Core;
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
using Microsoft.AspNetCore.Http;
namespace AuthKit.PluginContractValidator.Rules;
@@ -25,7 +26,7 @@ public sealed class MiddlewareRule : IPluginContractRule
///
/// Validates the middleware type contributed by
- /// , if any.
+ /// , if any.
///
public Task> ValidateAsync(
LoadedPlugin plugin,
diff --git a/tools/AuthKit.PluginContractValidator/src/Rules/RegistrationRule.cs b/tools/AuthKit.PluginContractValidator/src/Rules/RegistrationRule.cs
index 5563a50..944e1c9 100644
--- a/tools/AuthKit.PluginContractValidator/src/Rules/RegistrationRule.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Rules/RegistrationRule.cs
@@ -19,7 +19,7 @@ public sealed class RegistrationRule : IPluginContractRule
public string Name => "Registration";
///
- /// Invokes against a fresh service collection
+ /// Invokes against a fresh service collection
/// and then builds the to confirm the dependency graph is
/// constructible.
///
@@ -39,7 +39,7 @@ public Task> ValidateAsync(
try
{
- plugin.Instance.ConfigureServices(services, configuration);
+ PluginConfigurationInvoker.Configure(plugin.Instance, services, configuration);
}
catch (Exception ex)
{
diff --git a/tools/AuthKit.PluginContractValidator/src/Rules/SecuritySchemesRule.cs b/tools/AuthKit.PluginContractValidator/src/Rules/SecuritySchemesRule.cs
index c9c3474..8a4303f 100644
--- a/tools/AuthKit.PluginContractValidator/src/Rules/SecuritySchemesRule.cs
+++ b/tools/AuthKit.PluginContractValidator/src/Rules/SecuritySchemesRule.cs
@@ -4,6 +4,7 @@
using System.Threading.Tasks;
using AuthKit.PluginContractValidator.Core;
using AuthKit.Plugins.Abstractions.Contracts;
+using AuthKit.Plugins.Abstractions.Contracts.PluginContract;
using AuthKit.Plugins.Abstractions.Contracts.SecuritySchemes;
namespace AuthKit.PluginContractValidator.Rules;
@@ -13,7 +14,7 @@ namespace AuthKit.PluginContractValidator.Rules;
///
///
/// The rule verifies that each key in the dictionary returned by
-/// matches the Name of its
+/// matches the Name of its
/// .
///
public sealed class SecuritySchemesRule : IPluginContractRule