aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Loading/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src')
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs162
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs199
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs47
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs51
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs124
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs45
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs49
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs334
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs102
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs161
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs35
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs61
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs93
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj39
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml260
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs473
16 files changed, 2235 insertions, 0 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
new file mode 100644
index 0000000..5baf123
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs
@@ -0,0 +1,162 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: AssemblyLoader.cs
+*
+* AssemblyLoader.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
+
+using McMaster.NETCore.Plugins;
+
+using VNLib.Utils.Resources;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// <para>
+ /// Represents a disposable assembly loader wrapper for
+ /// exporting a signle type from a loaded assembly
+ /// </para>
+ /// <para>
+ /// If the loaded type implements <see cref="IDisposable"/> the
+ /// dispose method is called when the loader is disposed
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T">The exported type to manage</typeparam>
+ public class AssemblyLoader<T> : OpenResourceHandle<T>
+ {
+ private readonly PluginLoader _loader;
+ private readonly CancellationTokenRegistration _reg;
+ private readonly Lazy<T> _instance;
+
+ /// <summary>
+ /// The instance of the loaded type
+ /// </summary>
+ public override T Resource => _instance.Value;
+
+ private AssemblyLoader(PluginLoader loader, in CancellationToken unloadToken)
+ {
+ _loader = loader;
+ //Init lazy loader
+ _instance = new(LoadAndGetExportedType, LazyThreadSafetyMode.PublicationOnly);
+ //Register dispose
+ _reg = unloadToken.Register(Dispose);
+ }
+
+ /// <summary>
+ /// Loads the default assembly and gets the expected export type,
+ /// creates a new instance, and calls its parameterless constructor
+ /// </summary>
+ /// <returns>The desired type instance</returns>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ private T LoadAndGetExportedType()
+ {
+ //Load the assembly
+ Assembly asm = _loader.LoadDefaultAssembly();
+
+ Type resourceType = typeof(T);
+
+ //See if the type is exported
+ Type exp = (from type in asm.GetExportedTypes()
+ where resourceType.IsAssignableFrom(type)
+ select type)
+ .FirstOrDefault()
+ ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {resourceType.FullName}");
+ //Create instance
+ return (T)Activator.CreateInstance(exp)!;
+ }
+
+ /// <summary>
+ /// Creates a method delegate for the given method name from
+ /// the instance wrapped by the current loader
+ /// </summary>
+ /// <typeparam name="TDelegate"></typeparam>
+ /// <param name="methodName">The name of the method to recover</param>
+ /// <returns>The delegate method wrapper if found, null otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ public TDelegate? TryGetMethod<TDelegate>(string methodName) where TDelegate : Delegate
+ {
+ //get the type info of the actual resource
+ return Resource!.GetType()
+ .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)
+ ?.CreateDelegate<TDelegate>(Resource);
+ }
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ //If the instance is disposable, call its dispose method on unload
+ if (_instance.IsValueCreated && _instance.Value is IDisposable)
+ {
+ (_instance.Value as IDisposable)?.Dispose();
+ }
+ _loader.Dispose();
+ _reg.Dispose();
+ }
+
+ /// <summary>
+ /// Creates a new assembly loader for the specified type and
+ /// </summary>
+ /// <param name="assemblyName">The name of the assmbly within the current plugin directory</param>
+ /// <param name="unloadToken">The plugin unload token</param>
+ internal static AssemblyLoader<T> Load(string assemblyName, CancellationToken unloadToken)
+ {
+ Assembly executingAsm = Assembly.GetExecutingAssembly();
+ AssemblyLoadContext currentCtx = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context");
+
+ List<Type> shared = new ()
+ {
+ typeof(T),
+ typeof(PluginBase),
+ };
+
+ //Share all VNLib internal libraries
+ shared.AddRange(currentCtx.Assemblies.Where(static s => s.FullName.Contains("VNLib", StringComparison.OrdinalIgnoreCase)).SelectMany(static s => s.GetExportedTypes()));
+
+ PluginLoader loader = PluginLoader.CreateFromAssemblyFile(assemblyName,
+ currentCtx.IsCollectible,
+ shared.ToArray(),
+ conf =>
+ {
+
+ /*
+ * Load context is required to be set to the executing assembly's load context
+ * because it is controlled by the host, so this loader should be considered a
+ * a "child" collection of assemblies
+ */
+ conf.DefaultContext = currentCtx;
+
+ conf.PreferSharedTypes = true;
+
+ //Share utils asm
+ conf.SharedAssemblies.Add(typeof(Utils.Memory.Memory).Assembly.GetName());
+ });
+
+ return new(loader, in unloadToken);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
new file mode 100644
index 0000000..18df8e0
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs
@@ -0,0 +1,199 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ConfigurationExtensions.cs
+*
+* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Reflection;
+using System.Collections.Generic;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// Specifies a configuration variable name in the plugin's configuration
+ /// containing data specific to the type
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class)]
+ public sealed class ConfigurationNameAttribute : Attribute
+ {
+ /// <summary>
+ ///
+ /// </summary>
+ public string ConfigVarName { get; }
+
+ /// <summary>
+ /// Initializes a new <see cref="ConfigurationNameAttribute"/>
+ /// </summary>
+ /// <param name="configVarName">The name of the configuration variable for the class</param>
+ public ConfigurationNameAttribute(string configVarName)
+ {
+ ConfigVarName = configVarName;
+ }
+ }
+
+ /// <summary>
+ /// Contains extensions for plugin configuration specifc extensions
+ /// </summary>
+ public static class ConfigurationExtensions
+ {
+ public const string S3_CONFIG = "s3_config";
+ public const string S3_SECRET_KEY = "s3_secret";
+
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements for the specified type.
+ /// The type must contain a <see cref="ConfigurationNameAttribute"/>
+ /// </summary>
+ /// <typeparam name="T">The type to get the configuration of</typeparam>
+ /// <param name="plugin"></param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfigForType<T>(this PluginBase plugin)
+ {
+ Type t = typeof(T);
+ return plugin.GetConfigForType(t);
+ }
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements with the specified property name,
+ /// from the plugin config first, or falls back to the host config file
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="propName">The config property name to retrieve</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, string propName)
+ {
+ plugin.ThrowIfUnloaded();
+ try
+ {
+ //Try to get the element from the plugin config first
+ if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el))
+ {
+ //Fallback to the host config
+ el = plugin.HostConfig.GetProperty(propName);
+ }
+ //Get the top level config as a dictionary
+ return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value);
+ }
+ catch(KeyNotFoundException)
+ {
+ throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files");
+ }
+ }
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements with the specified property name,
+ /// from the plugin config first, or falls back to the host config file
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="propName">The config property name to retrieve</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement>? TryGetConfig(this PluginBase plugin, string propName)
+ {
+ plugin.ThrowIfUnloaded();
+ //Try to get the element from the plugin config first, or fallback to host
+ if (plugin.PluginConfig.TryGetProperty(propName, out JsonElement el) || plugin.HostConfig.TryGetProperty(propName, out el))
+ {
+ //Get the top level config as a dictionary
+ return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value);
+ }
+ //No config found
+ return null;
+ }
+
+ /// <summary>
+ /// Retrieves a top level configuration dictionary of elements for the specified type.
+ /// The type must contain a <see cref="ConfigurationNameAttribute"/>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="type">The type to get configuration data for</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfigForType(this PluginBase plugin, Type type)
+ {
+ //Get config name attribute from plugin type
+ ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>();
+ return configName?.ConfigVarName == null
+ ? throw new KeyNotFoundException("No configuration attribute set")
+ : plugin.GetConfig(configName.ConfigVarName);
+ }
+
+ /// <summary>
+ /// Shortcut extension for <see cref="GetConfigForType{T}(PluginBase)"/> to get
+ /// config of current class
+ /// </summary>
+ /// <param name="obj">The object that a configuration can be retrieved for</param>
+ /// <param name="plugin">The plugin containing configuration variables</param>
+ /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, object obj)
+ {
+ Type t = obj.GetType();
+ return plugin.GetConfigForType(t);
+ }
+
+ /// <summary>
+ /// Determines if the current plugin configuration contains the require properties to initialize
+ /// the type
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin"></param>
+ /// <returns>True if the plugin config contains the require configuration property</returns>
+ public static bool HasConfigForType<T>(this PluginBase plugin)
+ {
+ Type type = typeof(T);
+ ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>();
+ //See if the plugin contains a configuration varables
+ return configName != null && plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _);
+ }
+
+ /// <summary>
+ /// Attempts to load the basic S3 configuration variables required
+ /// for S3 client access
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The S3 configuration object found in the plugin/host configuration</returns>
+ public static S3Config? TryGetS3Config(this PluginBase plugin)
+ {
+ //Try get the config
+ IReadOnlyDictionary<string, JsonElement>? s3conf = plugin.TryGetConfig(S3_CONFIG);
+ if(s3conf == null)
+ {
+ return null;
+ }
+
+ //Try get the elements
+ return new()
+ {
+ BaseBucket = s3conf.GetPropString("bucket"),
+ ClientId = s3conf.GetPropString("access_key"),
+ ServerAddress = s3conf.GetPropString("server_address"),
+ UseSsl = s3conf.TryGetValue("use_ssl", out JsonElement el) && el.GetBoolean(),
+ Region = s3conf.GetPropString("region"),
+ };
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs
new file mode 100644
index 0000000..85b0b6d
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: AsyncIntervalAttribute.cs
+*
+* AsyncIntervalAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// When added to a method schedules it as a callback on a specified interval when
+ /// the plugin is loaded, and stops when unloaded
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class AsyncIntervalAttribute : Attribute
+ {
+ internal readonly TimeSpan Interval;
+
+ /// <summary>
+ /// Intializes the <see cref="AsyncIntervalAttribute"/> with the specified timeout in milliseconds
+ /// </summary>
+ /// <param name="milliseconds">The interval in milliseconds</param>
+ public AsyncIntervalAttribute(int milliseconds)
+ {
+ Interval = TimeSpan.FromMilliseconds(milliseconds);
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs
new file mode 100644
index 0000000..12c5ec4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/ConfigurableAsyncIntervalAttribute.cs
@@ -0,0 +1,51 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: ConfigurableAsyncIntervalAttribute.cs
+*
+* ConfigurableAsyncIntervalAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// When added to a method schedules it as a callback on a specified interval when
+ /// the plugin is loaded, and stops when unloaded
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class ConfigurableAsyncIntervalAttribute : Attribute
+ {
+ internal readonly string IntervalPropertyName;
+ internal readonly IntervalResultionType Resolution;
+
+ /// <summary>
+ /// Initializes a <see cref="ConfigurableAsyncIntervalAttribute"/> with the specified
+ /// interval property name
+ /// </summary>
+ /// <param name="configPropName">The configuration property name for the event interval</param>
+ /// <param name="resolution">The time resoltion for the event interval</param>
+ public ConfigurableAsyncIntervalAttribute(string configPropName, IntervalResultionType resolution)
+ {
+ IntervalPropertyName = configPropName;
+ Resolution = resolution;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
new file mode 100644
index 0000000..f671b07
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs
@@ -0,0 +1,124 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: EventManagment.cs
+*
+* EventManagment.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+
+ /// <summary>
+ /// A deletage to form a method signature for shedulable interval callbacks
+ /// </summary>
+ /// <param name="log">The plugin's default log provider</param>
+ /// <param name="pluginExitToken">The plugin's exit token</param>
+ /// <returns>A task the represents the asynchronous work</returns>
+ public delegate Task AsyncSchedulableCallback(ILogProvider log, CancellationToken pluginExitToken);
+
+ /// <summary>
+ /// Provides event schedueling extensions for plugins
+ /// </summary>
+ public static class EventManagment
+ {
+ /// <summary>
+ /// Schedules an asynchronous event interval for the current plugin, that is active until canceled or until the plugin unloads
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="asyncCallback">An asyncrhonous callback method.</param>
+ /// <param name="interval">The event interval</param>
+ /// <param name="immediate">A value that indicates if the callback should be run as soon as possible</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
+ public static void ScheduleInterval(this PluginBase plugin, AsyncSchedulableCallback asyncCallback, TimeSpan interval, bool immediate = false)
+ {
+ plugin.ThrowIfUnloaded();
+
+ plugin.Log.Verbose("Interval for {t} scheduled", interval);
+
+ //Run interval on plugins bg scheduler
+ _ = plugin.DeferTask(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate));
+ }
+
+ private static async Task RunIntervalOnPluginScheduler(PluginBase plugin, AsyncSchedulableCallback callback, TimeSpan interval, bool immediate)
+ {
+
+ static async Task RunCallbackAsync(PluginBase plugin, AsyncSchedulableCallback callback)
+ {
+ try
+ {
+ //invoke interval callback
+ await callback(plugin.Log, plugin.UnloadToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ //unloaded
+ plugin.Log.Verbose("Interval callback canceled due to plugin unload or other event cancellation");
+ }
+ catch (Exception ex)
+ {
+ plugin.Log.Error(ex, "Unhandled exception raised during timer callback");
+ }
+ }
+
+ //Run callback immediatly if requested
+ if (immediate)
+ {
+ await RunCallbackAsync(plugin, callback);
+ }
+
+ //Timer loop
+ while (true)
+ {
+ try
+ {
+ //await delay and wait for plugin cancellation
+ await Task.Delay(interval, plugin.UnloadToken);
+ }
+ catch (TaskCanceledException)
+ {
+ //Unload token canceled, exit loop
+ break;
+ }
+
+ await RunCallbackAsync(plugin, callback);
+ }
+ }
+
+ /// <summary>
+ /// Registers an <see cref="IIntervalScheduleable"/> type's event handler for
+ /// raising timed interval events
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="scheduleable">The instance to schedule for timeouts</param>
+ /// <param name="interval">The timeout interval</param>
+ /// <param name="immediate">A value that indicates if the callback should be run as soon as possible</param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
+ public static void ScheduleInterval(this PluginBase plugin, IIntervalScheduleable scheduleable, TimeSpan interval, bool immediate = false) =>
+ ScheduleInterval(plugin, scheduleable.OnIntervalAsync, interval, immediate);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs
new file mode 100644
index 0000000..5ff40f4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IIntervalScheduleable.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: IIntervalScheduleable.cs
+*
+* IIntervalScheduleable.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Threading;
+using System.Threading.Tasks;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// Exposes a type for asynchronous event schelueling
+ /// </summary>
+ public interface IIntervalScheduleable
+ {
+ /// <summary>
+ /// A method that is called when the interval time has elapsed
+ /// </summary>
+ /// <param name="log">The plugin default log provider</param>
+ /// <param name="cancellationToken">A token that may cancel an operations if the plugin becomes unloaded</param>
+ /// <returns>A task that resolves when the async operation completes</returns>
+ Task OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken);
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs
new file mode 100644
index 0000000..d82efc4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/IntervalResultionType.cs
@@ -0,0 +1,49 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: IntervalResultionType.cs
+*
+* IntervalResultionType.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Plugins.Extensions.Loading.Events
+{
+ /// <summary>
+ /// The configurable event interval resulution type
+ /// </summary>
+ public enum IntervalResultionType
+ {
+ /// <summary>
+ /// Specifies event interval resolution in milliseconds
+ /// </summary>
+ Milliseconds,
+ /// <summary>
+ /// Specifies event interval resolution in seconds
+ /// </summary>
+ Seconds,
+ /// <summary>
+ /// Specifies event interval resolution in minutes
+ /// </summary>
+ Minutes,
+ /// <summary>
+ /// Specifies event interval resolution in hours
+ /// </summary>
+ Hours
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
new file mode 100644
index 0000000..c23f5e2
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
@@ -0,0 +1,334 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: LoadingExtensions.cs
+*
+* LoadingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Accounts;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// Provides common loading (and unloading when required) extensions for plugins
+ /// </summary>
+ public static class LoadingExtensions
+ {
+ public const string DEBUG_CONFIG_KEY = "debug";
+ public const string SECRETS_CONFIG_KEY = "secrets";
+ public const string PASSWORD_HASHING_KEY = "passwords";
+
+ /*
+ * Plugin local cache used for storing singletons for a plugin instance
+ */
+ private static readonly ConditionalWeakTable<PluginBase, PluginLocalCache> _localCache = new();
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// </summary>
+ /// <param name="serviceType">The service instance type</param>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func<PluginBase, object> serviceFactory)
+ {
+ Lazy<object>? service;
+ //Get local cache
+ PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create);
+ //Hold lock while get/set the singleton
+ lock (pc.SyncRoot)
+ {
+ //Check if service already exists
+ service = pc.GetService(serviceType);
+ //publish the service if it isnt loaded yet
+ service ??= pc.AddService(serviceType, serviceFactory);
+ }
+ //Deferred load of the service
+ return service.Value;
+ }
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// or creates a new singleton instance for the plugin
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static T GetOrCreateSingleton<T>(PluginBase plugin, Func<PluginBase, T> serviceFactory)
+ => (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!);
+
+
+ /// <summary>
+ /// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will
+ /// be unloaded when the plugin us unloaded.
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="PasswordHashing"/></returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static PasswordHashing GetPasswords(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Get/load the passwords one time only
+ return GetOrCreateSingleton(plugin, LoadPasswords);
+ }
+
+ private static PasswordHashing LoadPasswords(PluginBase plugin)
+ {
+ PasswordHashing Passwords;
+ //Get the global password system secret (pepper)
+ byte[] pepper = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY)
+ .ToBase64Bytes().Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets");
+
+ ERRNO cb(Span<byte> buffer)
+ {
+ //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it
+ plugin.ThrowIfUnloaded();
+
+ pepper.CopyTo(buffer);
+ return pepper.Length;
+ }
+
+ //See hashing params are defined
+ IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY);
+ if (hashingArgs != null)
+ {
+ //Get hashing arguments
+ uint saltLen = hashingArgs["salt_len"].GetUInt32();
+ uint hashLen = hashingArgs["hash_len"].GetUInt32();
+ uint timeCost = hashingArgs["time_cost"].GetUInt32();
+ uint memoryCost = hashingArgs["memory_cost"].GetUInt32();
+ uint parallelism = hashingArgs["parallelism"].GetUInt32();
+ //Load passwords
+ Passwords = new(cb, pepper.Length, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
+ }
+ else
+ {
+ //Init default password hashing
+ Passwords = new(cb, pepper.Length);
+ }
+
+ //Register event to cleanup the password class
+ _ = plugin.RegisterForUnload(() =>
+ {
+ //Zero the pepper
+ CryptographicOperations.ZeroMemory(pepper);
+ });
+ //return
+ return Passwords;
+ }
+
+
+ /// <summary>
+ /// Loads an assembly into the current plugins AppDomain and will unload when disposed
+ /// or the plugin is unloaded from the host application.
+ /// </summary>
+ /// <typeparam name="T">The desired exported type to load from the assembly</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="assemblyName">The name of the assembly (ex: 'file.dll') to search for</param>
+ /// <param name="dirSearchOption">Directory/file search option</param>
+ /// <returns>The <see cref="AssemblyLoader{T}"/> managing the loaded assmbly in the current AppDomain</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static AssemblyLoader<T> LoadAssembly<T>(this PluginBase plugin, string assemblyName, SearchOption dirSearchOption = SearchOption.AllDirectories)
+ {
+ plugin.ThrowIfUnloaded();
+ _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
+
+ //get plugin directory from config
+ IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins");
+ string? pluginsBaseDir = config["path"].GetString();
+
+ /*
+ * This should never happen since this method can only be called from a
+ * plugin context, which means this path was used to load the current plugin
+ */
+ _ = pluginsBaseDir ?? throw new ArgumentNullException("path", "No plugin path is defined for the current host configuration, this is likely a bug");
+
+ //Get the first file that matches the search file
+ string? asmFile = Directory.EnumerateFiles(pluginsBaseDir, assemblyName, dirSearchOption).FirstOrDefault();
+ _ = asmFile ?? throw new FileNotFoundException($"Failed to load custom assembly {assemblyName} from plugin directory");
+
+ //Load the assembly
+ return AssemblyLoader<T>.Load(asmFile, plugin.UnloadToken);
+ }
+
+
+ /// <summary>
+ /// Determintes if the current plugin config has a debug propety set
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>True if debug mode is enabled, false otherwise</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static bool IsDebug(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Check for debug element
+ return plugin.PluginConfig.TryGetProperty(DEBUG_CONFIG_KEY, out JsonElement dbgEl) && dbgEl.GetBoolean();
+ }
+
+ /// <summary>
+ /// Internal exception helper to raise <see cref="ObjectDisposedException"/> if the plugin has been unlaoded
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static void ThrowIfUnloaded(this PluginBase plugin)
+ {
+ //See if the plugin was unlaoded
+ if (plugin.UnloadToken.IsCancellationRequested)
+ {
+ throw new ObjectDisposedException("The plugin has been unloaded");
+ }
+ }
+
+ /// <summary>
+ /// Schedules an asynchronous callback function to run and its results will be observed
+ /// when the operation completes, or when the plugin is unloading
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="asyncTask">The asynchronous operation to observe</param>
+ /// <param name="delayMs">An optional startup delay for the operation</param>
+ /// <returns>A task that completes when the deferred task completes </returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static async Task DeferTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0)
+ {
+ /*
+ * Motivation:
+ * Sometimes during plugin loading, a plugin may want to asynchronously load
+ * data, where the results are not required to be observed during loading, but
+ * should not be pending after the plugin is unloaded, as the assembly may be
+ * unloaded and referrences collected by the GC.
+ *
+ * So we can use the plugin's unload cancellation token to observe the results
+ * of a pending async operation
+ */
+
+ //Test status
+ plugin.ThrowIfUnloaded();
+
+ //Optional delay
+ await Task.Delay(delayMs);
+
+ //Run on ts
+ Task deferred = Task.Run(asyncTask);
+
+ //Add task to deferred list
+ plugin.ObserveTask(deferred);
+ try
+ {
+ //Await the task results
+ await deferred.ConfigureAwait(false);
+ }
+ catch(Exception ex)
+ {
+ //Log errors
+ plugin.Log.Error(ex, "Error occured while observing deferred task");
+ }
+ finally
+ {
+ //Remove task when complete
+ plugin.RemoveObservedTask(deferred);
+ }
+ }
+
+ /// <summary>
+ /// Registers an event to occur when the plugin is unloaded on a background thread
+ /// and will cause the Plugin.Unload() method to block until the event completes
+ /// </summary>
+ /// <param name="pbase"></param>
+ /// <param name="callback">The method to call when the plugin is unloaded</param>
+ /// <returns>A task that represents the registered work</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task RegisterForUnload(this PluginBase pbase, Action callback)
+ {
+ //Test status
+ pbase.ThrowIfUnloaded();
+ _ = callback ?? throw new ArgumentNullException(nameof(callback));
+
+ //Wait method
+ static async Task WaitForUnload(PluginBase pb, Action callback)
+ {
+ //Wait for unload as a task on the threadpool to avoid deadlocks
+ await pb.UnloadToken.WaitHandle.WaitAsync()
+ .ConfigureAwait(false);
+
+ callback();
+ }
+
+ //Registaer the task to cause the plugin to wait
+ return pbase.DeferTask(() => WaitForUnload(pbase, callback));
+ }
+
+
+ private sealed class PluginLocalCache
+ {
+ private readonly PluginBase _plugin;
+
+ private readonly Dictionary<Type, Lazy<object>> _store;
+
+ public object SyncRoot { get; } = new();
+
+ private PluginLocalCache(PluginBase plugin)
+ {
+ _plugin = plugin;
+ _store = new();
+ //Register cleanup on unload
+ _ = _plugin.RegisterForUnload(() => _store.Clear());
+ }
+
+ public static PluginLocalCache Create(PluginBase plugin) => new(plugin);
+
+
+ public Lazy<object>? GetService(Type serviceType)
+ {
+ Lazy<object>? t = _store.Where(t => t.Key.IsAssignableTo(serviceType))
+ .Select(static tk => tk.Value)
+ .FirstOrDefault();
+ return t;
+ }
+
+ public Lazy<object> AddService(Type serviceType, Func<PluginBase, object> factory)
+ {
+ //Get lazy loader to invoke factory outside of cache lock
+ Lazy<object> lazyFactory = new(() => factory(_plugin), true);
+ //Store lazy factory
+ _store.Add(serviceType, lazyFactory);
+ //Pass the lazy factory back
+ return lazyFactory;
+ }
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs b/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs
new file mode 100644
index 0000000..336f6a4
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/PrivateKey.cs
@@ -0,0 +1,102 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: PrivateKey.cs
+*
+* PrivateKey.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text;
+using System.Security.Cryptography;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// A container for a PKSC#8 encoed private key
+ /// </summary>
+ public sealed class PrivateKey : VnDisposeable
+ {
+ private readonly byte[] _utf8RawData;
+
+ /// <summary>
+ /// Decodes the PKCS#8 encoded private key from a secret, as an EC private key
+ /// and recovers the ECDsa algorithm from the key
+ /// </summary>
+ /// <returns>The <see cref="ECDsa"/> algoritm from the private key</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="CryptographicException"></exception>
+ public ECDsa GetECDsa()
+ {
+ //Alloc buffer
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length);
+ //Get base64 bytes from utf8
+ ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span);
+ //Parse the private key
+ ECDsa alg = ECDsa.Create();
+ alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _);
+ //Wipe the buffer
+ Memory.InitializeBlock(buffer.Span);
+ return alg;
+ }
+
+ /// <summary>
+ /// Decodes the PKCS#8 encoded private key from a secret, as an RSA private key
+ /// </summary>
+ /// <returns>The <see cref="RSA"/> algorithm from the private key</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="CryptographicException"></exception>
+ public RSA GetRSA()
+ {
+ //Alloc buffer
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(_utf8RawData.Length);
+ //Get base64 bytes from utf8
+ ERRNO count = VnEncoding.Base64UrlDecode(_utf8RawData, buffer.Span);
+ //Parse the private key
+ RSA alg = RSA.Create();
+ alg.ImportPkcs8PrivateKey(buffer.Span[..(int)count], out _);
+ //Wipe the buffer
+ Memory.InitializeBlock(buffer.Span);
+ return alg;
+ }
+
+ internal PrivateKey(SecretResult secret)
+ {
+ //Alloc and get utf8
+ byte[] buffer = new byte[secret.Result.Length];
+ int count = Encoding.UTF8.GetBytes(secret.Result, buffer);
+ //Verify length
+ if(count != buffer.Length)
+ {
+ throw new FormatException("UTF8 deocde failed");
+ }
+ //Store
+ _utf8RawData = buffer;
+ }
+
+ protected override void Free()
+ {
+ Memory.InitializeBlock(_utf8RawData.AsSpan());
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs
new file mode 100644
index 0000000..9242522
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs
@@ -0,0 +1,161 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: RoutingExtensions.cs
+*
+* RoutingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Reflection;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+using VNLib.Plugins.Extensions.Loading.Events;
+
+namespace VNLib.Plugins.Extensions.Loading.Routing
+{
+ /// <summary>
+ /// Provides advanced QOL features to plugin loading
+ /// </summary>
+ public static class RoutingExtensions
+ {
+ private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new();
+
+ /// <summary>
+ /// Constructs and routes the specific endpoint type for the current plugin
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static T Route<T>(this PluginBase plugin, string? pluginConfigPathName) where T : IEndpoint
+ {
+ Type endpointType = typeof(T);
+ //If the config attribute is not set, then ignore the config variables
+ if (string.IsNullOrWhiteSpace(pluginConfigPathName))
+ {
+ ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) });
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
+ //Create the new endpoint and pass the plugin instance
+ T endpoint = (T)constructor.Invoke(new object[] { plugin });
+ //Register event handlers for the endpoint
+ ScheduleIntervals(plugin, endpoint, endpointType, null);
+ //Route the endpoint
+ plugin.Route(endpoint);
+
+ //Store ref to plugin for endpoint
+ _pluginRefs.Add(endpoint, plugin);
+
+ return endpoint;
+ }
+ else
+ {
+ ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IReadOnlyDictionary<string, JsonElement>) });
+ //Make sure the constructor exists
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
+ //Get config variables for the endpoint
+ IReadOnlyDictionary<string, JsonElement> conf = plugin.GetConfig(pluginConfigPathName);
+ //Create the new endpoint and pass the plugin instance along with the configuration object
+ T endpoint = (T)constructor.Invoke(new object[] { plugin, conf });
+ //Register event handlers for the endpoint
+ ScheduleIntervals(plugin, endpoint, endpointType, conf);
+ //Route the endpoint
+ plugin.Route(endpoint);
+
+ //Store ref to plugin for endpoint
+ _pluginRefs.Add(endpoint, plugin);
+
+ return endpoint;
+ }
+ }
+
+ /// <summary>
+ /// Constructs and routes the specific endpoint type for the current plugin
+ /// </summary>
+ /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam>
+ /// <param name="plugin"></param>
+ /// <exception cref="TargetInvocationException"></exception>
+ public static T Route<T>(this PluginBase plugin) where T : IEndpoint
+ {
+ Type endpointType = typeof(T);
+ //Get config name attribute
+ ConfigurationNameAttribute? configAttr = endpointType.GetCustomAttribute<ConfigurationNameAttribute>();
+ //Route using attribute
+ return plugin.Route<T>(configAttr?.ConfigVarName);
+ }
+
+ /// <summary>
+ /// Gets the plugin that loaded the current endpoint
+ /// </summary>
+ /// <param name="ep"></param>
+ /// <returns>The plugin that loaded the current endpoint</returns>
+ /// <exception cref="InvalidOperationException"></exception>
+ public static PluginBase GetPlugin(this IEndpoint ep)
+ {
+ _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase);
+ return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed");
+ }
+
+ private static void ScheduleIntervals<T>(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary<string, JsonElement>? endpointLocalConfig) where T : IEndpoint
+ {
+ //Get all methods that have the configureable async interval attribute specified
+ IEnumerable<Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>> confIntervals = epType.GetMethods()
+ .Where(m => m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>() != null)
+ .Select(m => new Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>
+ (m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance)));
+
+ //If the endpoint has a local config, then use it to find the interval
+ if (endpointLocalConfig != null)
+ {
+
+ //Schedule event handlers on the current plugin
+ foreach (Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> interval in confIntervals)
+ {
+ int value = endpointLocalConfig[interval.Item1.IntervalPropertyName].GetInt32();
+ //Get the timeout from its resolution variable
+ TimeSpan timeout = interval.Item1.Resolution switch
+ {
+ IntervalResultionType.Seconds => TimeSpan.FromSeconds(value),
+ IntervalResultionType.Minutes => TimeSpan.FromMinutes(value),
+ IntervalResultionType.Hours => TimeSpan.FromHours(value),
+ _ => TimeSpan.FromMilliseconds(value),
+ };
+ //Schedule
+ plugin.ScheduleInterval(interval.Item2, timeout);
+ }
+ }
+
+ //Get all methods that have the async interval attribute specified
+ IEnumerable<Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>> intervals = epType.GetMethods()
+ .Where(m => m.GetCustomAttribute<AsyncIntervalAttribute>() != null)
+ .Select(m => new Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>(
+ m.GetCustomAttribute<AsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance))
+ );
+
+ //Schedule event handlers on the current plugin
+ foreach (Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback> interval in intervals)
+ {
+ plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval);
+ }
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs
new file mode 100644
index 0000000..76c2bc2
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs
@@ -0,0 +1,35 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: S3Config.cs
+*
+* S3Config.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ public sealed class S3Config
+ {
+ public string? ServerAddress { get; init; }
+ public string? ClientId { get; init; }
+ public string? BaseBucket { get; init; }
+ public bool? UseSsl { get; init; }
+ public string? Region { get; init; }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs b/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs
new file mode 100644
index 0000000..15323f3
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/SecretResult.cs
@@ -0,0 +1,61 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: SecretResult.cs
+*
+* SecretResult.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+ /// <summary>
+ /// The result of a secret fetch operation
+ /// </summary>
+ public sealed class SecretResult : VnDisposeable
+ {
+ private readonly char[] _secretChars;
+
+ /// <summary>
+ /// The protected raw result value
+ /// </summary>
+ public ReadOnlySpan<char> Result => _secretChars;
+
+
+ internal SecretResult(ReadOnlySpan<char> value) => _secretChars = value.ToArray();
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ Memory.InitializeBlock(_secretChars.AsSpan());
+ }
+
+ internal static SecretResult ToSecret(string? result)
+ {
+ SecretResult res = new(result.AsSpan());
+ Memory.UnsafeZeroMemory<char>(result);
+ return res;
+ }
+ }
+}
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs b/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs
new file mode 100644
index 0000000..da090ec
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs
@@ -0,0 +1,93 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: UserLoading.cs
+*
+* UserLoading.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Users;
+
+namespace VNLib.Plugins.Extensions.Loading.Users
+{
+ /// <summary>
+ /// Contains extension methods for plugins to load the "users" system
+ /// </summary>
+ public static class UserLoading
+ {
+ public const string USER_CUSTOM_ASSEMBLY = "user_custom_asm";
+ public const string DEFAULT_USER_ASM = "VNLib.Plugins.Essentials.Users.dll";
+ public const string ONLOAD_METHOD_NAME = "OnPluginLoading";
+
+
+ /// <summary>
+ /// Gets or loads the plugin's ambient <see cref="IUserManager"/>, with the specified user-table name,
+ /// or the default table name
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="IUserManager"/> for the current plugin</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IUserManager GetUserManager(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Get stored or load
+ return LoadingExtensions.GetOrCreateSingleton(plugin, LoadUsers);
+ }
+
+ private static IUserManager LoadUsers(PluginBase pbase)
+ {
+ //Try to load a custom user assembly for exporting IUserManager
+ string? customAsm = pbase.PluginConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
+ //See if host config defined the path
+ customAsm ??= pbase.HostConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
+ //Finally default
+ customAsm ??= DEFAULT_USER_ASM;
+
+ //Try to load a custom assembly
+ AssemblyLoader<IUserManager> loader = pbase.LoadAssembly<IUserManager>(customAsm);
+ try
+ {
+ //Try to get the onload method
+ Action<object>? onLoadMethod = loader.TryGetMethod<Action<object>>(ONLOAD_METHOD_NAME);
+
+ //Call the onplugin load method
+ onLoadMethod?.Invoke(pbase);
+
+ if (pbase.IsDebug())
+ {
+ pbase.Log.Verbose("Loading user manager from assembly {name}", loader.Resource.GetType().AssemblyQualifiedName);
+ }
+
+ //Return the loaded instance (may raise exception)
+ return loader.Resource;
+ }
+ catch
+ {
+ loader.Dispose();
+ throw;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj
new file mode 100644
index 0000000..15bb15e
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj
@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <Authors>Vaughn Nugent</Authors>
+ <Version>1.0.1.1</Version>
+
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <Nullable>enable</Nullable>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
+ <PackageReference Include="VaultSharp" Version="1.7.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VNLib\Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml
new file mode 100644
index 0000000..963f506
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.xml
@@ -0,0 +1,260 @@
+<?xml version="1.0"?>
+<!--
+Copyright (c) 2022 Vaughn Nugent
+-->
+<doc>
+ <assembly>
+ <name>VNLib.Plugins.Extensions.Loading</name>
+ </assembly>
+ <members>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute">
+ <summary>
+ Specifies a configuration variable name in the plugin's configuration
+ containing data specific to the type
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute.ConfigVarName">
+ <summary>
+
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute.#ctor(System.String)">
+ <summary>
+ Initializes a new <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/>
+ </summary>
+ <param name="configVarName">The name of the configuration variable for the class</param>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions">
+ <summary>
+ Contains extensions for plugin configuration specifc extensions
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType``1(VNLib.Plugins.PluginBase)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements for the specified type.
+ The type must contain a <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/>
+ </summary>
+ <typeparam name="T">The type to get the configuration of</typeparam>
+ <param name="plugin"></param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfig(VNLib.Plugins.PluginBase,System.String)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements with the specified property name,
+ from the plugin config first, or falls back to the host config file
+ </summary>
+ <param name="plugin"></param>
+ <param name="propName">The config property name to retrieve</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.TryGetConfig(VNLib.Plugins.PluginBase,System.String)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements with the specified property name,
+ from the plugin config first, or falls back to the host config file
+ </summary>
+ <param name="plugin"></param>
+ <param name="propName">The config property name to retrieve</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType(VNLib.Plugins.PluginBase,System.Type)">
+ <summary>
+ Retrieves a top level configuration dictionary of elements for the specified type.
+ The type must contain a <see cref="T:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationNameAttribute"/>
+ </summary>
+ <param name="plugin"></param>
+ <param name="type">The type to get configuration data for</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfig(VNLib.Plugins.PluginBase,System.Object)">
+ <summary>
+ Shortcut extension for <see cref="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.GetConfigForType``1(VNLib.Plugins.PluginBase)"/> to get
+ config of current class
+ </summary>
+ <param name="obj">The object that a configuration can be retrieved for</param>
+ <param name="plugin">The plugin containing configuration variables</param>
+ <returns>A <see cref="T:System.Collections.Generic.Dictionary`2"/> of top level configuration elements for the type</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Configuration.ConfigurationExtensions.HasConfigForType``1(VNLib.Plugins.PluginBase)">
+ <summary>
+ Determines if the current plugin configuration contains the require properties to initialize
+ the type
+ </summary>
+ <typeparam name="T"></typeparam>
+ <param name="plugin"></param>
+ <returns>True if the plugin config contains the require configuration property</returns>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute">
+ <summary>
+ When added to a method schedules it as a callback on a specified interval when
+ the plugin is loaded, and stops when unloaded
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute.#ctor(System.Int32)">
+ <summary>
+ Intializes the <see cref="T:VNLib.Plugins.Extensions.Loading.Events.AsyncIntervalAttribute"/> with the specified timeout in milliseconds
+ </summary>
+ <param name="milliseconds">The interval in milliseconds</param>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType">
+ <summary>
+ The configurable event interval resulution type
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Milliseconds">
+ <summary>
+ Specifies event interval resolution in milliseconds
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Seconds">
+ <summary>
+ Specifies event interval resolution in seconds
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Minutes">
+ <summary>
+ Specifies event interval resolution in minutes
+ </summary>
+ </member>
+ <member name="F:VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType.Hours">
+ <summary>
+ Specifies event interval resolution in hours
+ </summary>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute">
+ <summary>
+ When added to a method schedules it as a callback on a specified interval when
+ the plugin is loaded, and stops when unloaded
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute.#ctor(System.String,VNLib.Plugins.Extensions.Loading.Events.IntervalResultionType)">
+ <summary>
+ Initializes a <see cref="T:VNLib.Plugins.Extensions.Loading.Events.ConfigurableAsyncIntervalAttribute"/> with the specified
+ interval property name
+ </summary>
+ <param name="configPropName">The configuration property name for the event interval</param>
+ <param name="resolution">The time resoltion for the event interval</param>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.EventHandle">
+ <summary>
+ Represents a handle to a scheduled event interval that is managed by the plugin but may be cancled by disposing the instance
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventHandle.Pause">
+ <summary>
+ Pauses the event timer until the <see cref="T:VNLib.Utils.OpenHandle"/> is released or disposed
+ then resumes to the inital interval period
+ </summary>
+ <returns>A <see cref="T:VNLib.Utils.OpenHandle"/> that restores the timer to its initial state when disposed</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventHandle.Free">
+ <inheritdoc/>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Events.EventManagment">
+ <summary>
+ Provides event schedueling extensions for plugins
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Events.EventManagment.ScheduleInterval``1(VNLib.Plugins.PluginBase,System.Func{``0,System.Threading.Tasks.Task},``0,System.TimeSpan)">
+ <summary>
+ Schedules an asynchronous event interval for the current plugin, that is active until canceled or until the plugin unloads
+ </summary>
+ <typeparam name="TState">Stateful event argument</typeparam>
+ <param name="plugin"></param>
+ <param name="asyncCallback">An asyncrhonous callback method.</param>
+ <param name="state"></param>
+ <param name="interval">The event interval</param>
+ <returns>An <see cref="T:VNLib.Plugins.Extensions.Loading.Events.EventHandle"/> that can manage the interval state</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.LoadingExtensions">
+ <summary>
+ Provides common loading (and unloading when required) extensions for plugins
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.GetPasswords(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets the plugins ambient <see cref="T:VNLib.Plugins.Essentials.Accounts.PasswordHashing"/> if loaded, or loads it if required. This class will
+ be unloaded when the plugin us unloaded.
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:VNLib.Plugins.Essentials.Accounts.PasswordHashing"/></returns>
+ <exception cref="T:System.OverflowException"></exception>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.GetUserManager(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets or loads the plugin's ambient <see cref="T:VNLib.Plugins.Essentials.Users.UserManager"/>, with the specified user-table name,
+ or the default table name
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:VNLib.Plugins.Essentials.Users.UserManager"/> for the current plugin</returns>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.IsDebug(VNLib.Plugins.PluginBase)">
+ <summary>
+ Determintes if the current plugin config has a debug propety set
+ </summary>
+ <param name="plugin"></param>
+ <returns>True if debug mode is enabled, false otherwise</returns>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.LoadingExtensions.ThrowIfUnloaded(VNLib.Plugins.PluginBase)">
+ <summary>
+ Internal exception helper to raise <see cref="T:System.ObjectDisposedException"/> if the plugin has been unlaoded
+ </summary>
+ <param name="plugin"></param>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Routing.RoutingExtensions.Route``1(VNLib.Plugins.PluginBase,System.String)">
+ <summary>
+ Constructs and routes the specific endpoint type for the current plugin
+ </summary>
+ <typeparam name="T">The <see cref="T:VNLib.Plugins.IEndpoint"/> type</typeparam>
+ <param name="plugin"></param>
+ <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param>
+ <exception cref="T:System.Reflection.TargetInvocationException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Routing.RoutingExtensions.Route``1(VNLib.Plugins.PluginBase)">
+ <summary>
+ Constructs and routes the specific endpoint type for the current plugin
+ </summary>
+ <typeparam name="T">The <see cref="T:VNLib.Plugins.IEndpoint"/> type</typeparam>
+ <param name="plugin"></param>
+ <exception cref="T:System.Reflection.TargetInvocationException"></exception>
+ </member>
+ <member name="T:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader">
+ <summary>
+ Provides common basic SQL loading extensions for plugins
+ </summary>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader.GetConnectionFactory(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets (or loads) the ambient sql connection factory for the current plugin
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:System.Data.Common.DbConnection"/> factory</returns>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ </member>
+ <member name="M:VNLib.Plugins.Extensions.Loading.Sql.SqlDbConnectionLoader.GetContextOptions(VNLib.Plugins.PluginBase)">
+ <summary>
+ Gets (or loads) the ambient <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions"/> configured from
+ the ambient sql factory
+ </summary>
+ <param name="plugin"></param>
+ <returns>The ambient <see cref="T:Microsoft.EntityFrameworkCore.DbContextOptions"/> for the current plugin</returns>
+ <exception cref="T:System.Collections.Generic.KeyNotFoundException"></exception>
+ <exception cref="T:System.ObjectDisposedException"></exception>
+ <remarks>If plugin is in debug mode, writes log data to the default log</remarks>
+ </member>
+ </members>
+</doc>
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs
new file mode 100644
index 0000000..898d64c
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs
@@ -0,0 +1,473 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: VaultSecrets.cs
+*
+* VaultSecrets.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+
+using VaultSharp;
+using VaultSharp.V1.Commons;
+using VaultSharp.V1.AuthMethods;
+using VaultSharp.V1.AuthMethods.Token;
+using VaultSharp.V1.AuthMethods.AppRole;
+using VaultSharp.V1.SecretsEngines.PKI;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Hashing.IdentityUtility;
+using System.Threading;
+
+namespace VNLib.Plugins.Extensions.Loading
+{
+
+ /// <summary>
+ /// Adds loading extensions for secure/centralized configuration secrets
+ /// </summary>
+ public static class PluginSecretLoading
+ {
+ public const string VAULT_OBJECT_NAME = "hashicorp_vault";
+ public const string SECRETS_CONFIG_KEY = "secrets";
+ public const string VAULT_TOKEN_KEY = "token";
+ public const string VAULT_ROLE_KEY = "role";
+ public const string VAULT_SECRET_KEY = "secret";
+
+ public const string VAULT_URL_KEY = "url";
+
+ public const string VAULT_URL_SCHEME = "vault://";
+
+
+ /// <summary>
+ /// <para>
+ /// Gets a secret from the "secrets" element.
+ /// </para>
+ /// <para>
+ /// Secrets elements are merged from the host config and plugin local config 'secrets' element.
+ /// before searching. The plugin config takes precedence over the host config.
+ /// </para>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="secretName">The name of the secret propery to get</param>
+ /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task<SecretResult?> TryGetSecretAsync(this PluginBase plugin, string secretName)
+ {
+ //Get the secret from the config file raw
+ string? rawSecret = TryGetSecretInternal(plugin, secretName);
+ if (rawSecret == null)
+ {
+ return Task.FromResult<SecretResult?>(null);
+ }
+
+ //Secret is a vault path, or return the raw value
+ if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.FromResult<SecretResult?>(new(rawSecret.AsSpan()));
+ }
+ return GetSecretFromVaultAsync(plugin, rawSecret);
+ }
+
+ /// <summary>
+ /// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]")
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="vaultPath">The raw vault url to lookup</param>
+ /// <returns>The string of the object at the specified vault path</returns>
+ /// <exception cref="UriFormatException"></exception>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task<SecretResult?> GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath)
+ {
+ //print the path for debug
+ if (plugin.IsDebug())
+ {
+ plugin.Log.Debug("Retrieving secret {s} from vault", vaultPath.ToString());
+ }
+
+ //Slice off path
+ ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME);
+ ReadOnlySpan<char> path = paq.SliceBeforeParam('?');
+ ReadOnlySpan<char> query = paq.SliceAfterParam('?');
+
+ if (paq.IsEmpty)
+ {
+ throw new UriFormatException("Vault secret location not valid/empty ");
+ }
+ //Get the secret
+ string secretTableKey = query.SliceAfterParam("secret=").SliceBeforeParam('&').ToString();
+ string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString();
+
+ //get mount and path
+ int lastSep = path.IndexOf('/');
+ string mount = path[..lastSep].ToString();
+ string secret = path[(lastSep + 1)..].ToString();
+
+ async Task<SecretResult?> execute()
+ {
+ //Try load client
+ IVaultClient? client = plugin.GetVault();
+
+ _ = client ?? throw new KeyNotFoundException("Vault client not found");
+ //run read async
+ Secret<SecretData> result = await client.V1.Secrets.KeyValue.V2.ReadSecretAsync(path:secret, mountPoint:mount);
+ //Read the secret
+ return SecretResult.ToSecret(result.Data.Data[secretTableKey].ToString());
+ }
+
+ return Task.Run(execute);
+ }
+
+ /// <summary>
+ /// <para>
+ /// Gets a Certicate from the "secrets" element.
+ /// </para>
+ /// <para>
+ /// Secrets elements are merged from the host config and plugin local config 'secrets' element.
+ /// before searching. The plugin config takes precedence over the host config.
+ /// </para>
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="secretName">The name of the secret propery to get</param>
+ /// <returns>The element from the configuration file with the given name, or null if the configuration or property does not exist</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task<X509Certificate?> TryGetCertificateAsync(this PluginBase plugin, string secretName)
+ {
+ //Get the secret from the config file raw
+ string? rawSecret = TryGetSecretInternal(plugin, secretName);
+ if (rawSecret == null)
+ {
+ return Task.FromResult<X509Certificate?>(null);
+ }
+
+ //Secret is a vault path, or return the raw value
+ if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.FromResult<X509Certificate?>(new (rawSecret));
+ }
+ return GetCertFromVaultAsync(plugin, rawSecret);
+ }
+
+ public static Task<X509Certificate?> GetCertFromVaultAsync(this PluginBase plugin, ReadOnlySpan<char> vaultPath, CertificateCredentialsRequestOptions? options = null)
+ {
+ //print the path for debug
+ if (plugin.IsDebug())
+ {
+ plugin.Log.Debug("Retrieving certificate {s} from vault", vaultPath.ToString());
+ }
+
+ //Slice off path
+ ReadOnlySpan<char> paq = vaultPath.SliceAfterParam(VAULT_URL_SCHEME);
+ ReadOnlySpan<char> path = paq.SliceBeforeParam('?');
+ ReadOnlySpan<char> query = paq.SliceAfterParam('?');
+
+ if (paq.IsEmpty)
+ {
+ throw new UriFormatException("Vault secret location not valid/empty ");
+ }
+
+ //Get the secret
+ string role = query.SliceAfterParam("role=").SliceBeforeParam('&').ToString();
+ string vaultType = query.SliceBeforeParam("vault_type=").SliceBeforeParam('&').ToString();
+ string commonName = query.SliceBeforeParam("cn=").SliceBeforeParam('&').ToString();
+
+ //get mount and path
+ int lastSep = path.IndexOf('/');
+ string mount = path[..lastSep].ToString();
+ string secret = path[(lastSep + 1)..].ToString();
+
+ async Task<X509Certificate?> execute()
+ {
+ //Try load client
+ IVaultClient? client = plugin.GetVault();
+
+ _ = client ?? throw new KeyNotFoundException("Vault client not found");
+
+ options ??= new()
+ {
+ CertificateFormat = CertificateFormat.pem,
+ PrivateKeyFormat = PrivateKeyFormat.pkcs8,
+ CommonName = commonName,
+ };
+
+ //run read async
+ Secret<CertificateCredentials> result = await client.V1.Secrets.PKI.GetCredentialsAsync(pkiRoleName:secret, certificateCredentialRequestOptions:options, pkiBackendMountPoint:mount);
+ //Read the secret
+ byte[] pemCertData = Encoding.UTF8.GetBytes(result.Data.CertificateContent);
+
+ return new (pemCertData);
+ }
+
+ return Task.Run(execute);
+ }
+
+ /// <summary>
+ /// Gets the ambient vault client for the current plugin
+ /// if the configuration is loaded, null otherwise
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="IVaultClient"/> if loaded, null otherwise</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static IVaultClient? GetVault(this PluginBase plugin) => LoadingExtensions.GetOrCreateSingleton(plugin, TryGetVaultLoader);
+
+ private static string? TryGetSecretInternal(PluginBase plugin, string secretName)
+ {
+ bool local = plugin.PluginConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement localEl);
+ bool host = plugin.HostConfig.TryGetProperty(SECRETS_CONFIG_KEY, out JsonElement hostEl);
+
+ //total config
+ IReadOnlyDictionary<string, JsonElement>? conf;
+
+ if (local && host)
+ {
+ //Load both config objects to dict
+ Dictionary<string, JsonElement> localConf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+ Dictionary<string, JsonElement> hostConf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+
+ //merge the two configs
+ foreach(KeyValuePair<string, JsonElement> lc in localConf)
+ {
+ //Overwrite any host config keys, plugin conf takes priority
+ hostConf[lc.Key] = lc.Value;
+ }
+ //set the merged config
+ conf = hostConf;
+ }
+ else if(local)
+ {
+ //Store only local config
+ conf = localEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+ }
+ else if(host)
+ {
+ //store only host config
+ conf = hostEl.EnumerateObject().ToDictionary(x => x.Name, x => x.Value);
+ }
+ else
+ {
+ conf = null;
+ }
+
+ //Get the value or default json element
+ return conf != null && conf.TryGetValue(secretName, out JsonElement el) ? el.GetString() : null;
+ }
+
+ private static IVaultClient? TryGetVaultLoader(PluginBase pbase)
+ {
+ //Get vault config
+ IReadOnlyDictionary<string, JsonElement>? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME);
+
+ if (conf == null)
+ {
+ return null;
+ }
+
+ //try get servre address creds from config
+ string? serverAddress = conf[VAULT_URL_KEY].GetString() ?? throw new KeyNotFoundException($"Failed to load the key {VAULT_URL_KEY} from object {VAULT_OBJECT_NAME}");
+
+ IAuthMethodInfo authMethod;
+
+ //Get authentication method from config
+ if (conf.TryGetValue(VAULT_TOKEN_KEY, out JsonElement tokenEl))
+ {
+ //Init token
+ authMethod = new TokenAuthMethodInfo(tokenEl.GetString());
+ }
+ else if (conf.TryGetValue(VAULT_ROLE_KEY, out JsonElement roleEl) && conf.TryGetValue(VAULT_SECRET_KEY, out JsonElement secretEl))
+ {
+ authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString());
+ }
+ else
+ {
+ throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}");
+ }
+
+ //Settings
+ VaultClientSettings settings = new(serverAddress, authMethod);
+
+ //create vault client
+ return new VaultClient(settings);
+ }
+
+ /// <summary>
+ /// Gets the Secret value as a byte buffer
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The base64 decoded secret as a byte[]</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static byte[] GetFromBase64(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+
+ //Temp buffer
+ using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(secret.Result.Length);
+
+ //Get base64
+ if(Convert.TryFromBase64Chars(secret.Result, buffer, out int count))
+ {
+ //Copy to array
+ byte[] value = buffer.Span[..count].ToArray();
+ //Clear block before returning
+ Memory.InitializeBlock<byte>(buffer);
+
+ return value;
+ }
+
+ throw new InternalBufferTooSmallException("internal buffer too small");
+ }
+
+ /// <summary>
+ /// Converts the secret recovery task to
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>A task whos result the base64 decoded secret as a byte[]</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static async Task<byte[]?> ToBase64Bytes(this Task<SecretResult?> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ return sec?.GetFromBase64();
+ }
+
+ /// <summary>
+ /// Recovers a certificate from a PEM encoded secret
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="X509Certificate2"/> parsed from the PEM encoded data</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static X509Certificate2 GetCertificate(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ return X509Certificate2.CreateFromPem(secret.Result);
+ }
+
+ /// <summary>
+ /// Gets the secret value as a secret result
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The document parsed from the secret value</returns>
+ public static JsonDocument GetJsonDocument(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ //Alloc buffer, utf8 so 1 byte per char
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length);
+ //Get utf8 bytes
+ int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
+ //Reader and parse
+ Utf8JsonReader reader = new(buffer.Span[..count]);
+ return JsonDocument.ParseValue(ref reader);
+ }
+
+ /// <summary>
+ /// Gets a SPKI encoded public key from a secret
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="PublicKey"/> parsed from the SPKI public key</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static PublicKey GetPublicKey(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ //Alloc buffer, base64 is larger than binary value so char len is large enough
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length);
+ //Get base64 bytes
+ ERRNO count = VnEncoding.TryFromBase64Chars(secret.Result, buffer.Span);
+ //Parse the SPKI from base64
+ return PublicKey.CreateFromSubjectPublicKeyInfo(buffer.Span[..(int)count], out _);
+ }
+
+ /// <summary>
+ /// Gets the value of the <see cref="SecretResult"/> as a <see cref="PrivateKey"/>
+ /// container
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="PrivateKey"/> from the secret value</returns>
+ /// <exception cref="FormatException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static PrivateKey GetPrivateKey(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ return new PrivateKey(secret);
+ }
+
+ /// <summary>
+ /// Gets a <see cref="ReadOnlyJsonWebKey"/> from a secret value
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the result</returns>
+ /// <exception cref="JsonException"></exception>
+ /// <exception cref="ArgumentException"></exception>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static ReadOnlyJsonWebKey GetJsonWebKey(this SecretResult secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ //Alloc buffer, utf8 so 1 byte per char
+ using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(secret.Result.Length);
+ //Get utf8 bytes
+ int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
+ return new ReadOnlyJsonWebKey(buffer.Span[..count]);
+ }
+
+ /// <summary>
+ /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/>
+ /// from a <see cref="SecretResult"/> task
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<ReadOnlyJsonWebKey?> ToJsonWebKey(this Task<SecretResult?> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ return sec?.GetJsonWebKey();
+ }
+
+ /// <summary>
+ /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/>
+ /// from a <see cref="SecretResult"/> task
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <param name="required">
+ /// A value that inidcates that a value is required from the result,
+ /// or a <see cref="KeyNotFoundException"/> is raised
+ /// </param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<ReadOnlyJsonWebKey> ToJsonWebKey(this Task<SecretResult?> secret, bool required)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ //If required is true and result is null, raise an exception
+ return required && sec == null ? throw new KeyNotFoundException("A required secret was missing") : (sec?.GetJsonWebKey()!);
+ }
+ }
+}