Scopes { get; init; } = ["openid", "profile", "email", "offline_access"];
+
+ /// Path the loopback listener answers the redirect on.
+ public string RedirectPath { get; init; } = "/callback";
+
+ /// Whether provider metadata must be served over HTTPS. Only false for local development.
+ public bool RequireHttpsMetadata { get; init; } = true;
+
+ /// How long to wait for the user to finish in the browser.
+ ///
+ /// Generous, because the user may have to find a password manager, complete a second factor, or
+ /// approve a push notification on another device.
+ ///
+ public TimeSpan BrowserTimeout { get; init; } = TimeSpan.FromMinutes(5);
+
+ /// Page shown in the browser once the callback is captured.
+ public string CompletionHtml { get; init; } =
+ """
+ DodoSSH
+
+ Signed in
You can close this tab and return to DodoSSH.
+
+ """;
+}
diff --git a/src/DodoSSH.Client.Auth/OidcProviderMetadata.cs b/src/DodoSSH.Client.Auth/OidcProviderMetadata.cs
new file mode 100644
index 0000000..84dbc5a
--- /dev/null
+++ b/src/DodoSSH.Client.Auth/OidcProviderMetadata.cs
@@ -0,0 +1,145 @@
+using System.Text.Json;
+
+namespace DodoSSH.Client.Auth;
+
+/// Raised when an OIDC exchange fails in a way the caller must handle.
+public sealed class OidcException(string message, string? errorCode = null) : Exception(message)
+{
+ /// The provider's OAuth error code, when it supplied one.
+ public string? ErrorCode { get; } = errorCode;
+}
+
+/// The parts of an OIDC discovery document this client uses.
+/// The provider's own idea of its issuer identifier.
+/// Where the browser is sent.
+/// Where codes and refresh tokens are redeemed.
+///
+/// Where signing keys are published. Not used to validate tokens received directly from the token
+/// endpoint, but required in M3 to verify other users' key bindings — fetched from here, never
+/// proxied through the DodoSSH server, which is the entire point of the binding. See ADR 0001.
+///
+/// Advertised PKCE methods.
+public sealed record OidcProviderMetadata(
+ Uri Issuer,
+ Uri AuthorizationEndpoint,
+ Uri TokenEndpoint,
+ Uri? JwksUri,
+ IReadOnlyList CodeChallengeMethodsSupported);
+
+/// Fetches and validates an OIDC discovery document.
+public sealed class OidcDiscoveryClient(HttpClient http)
+{
+ private const string DiscoveryPath = ".well-known/openid-configuration";
+
+ ///
+ /// Reads the discovery document for an authority.
+ ///
+ ///
+ /// The document's issuer is checked against the authority the URL was built from, per
+ /// RFC 8414 §3.3. Skipping that check is what enables a mix-up attack: an attacker-controlled
+ /// authority can serve metadata pointing at a legitimate provider's endpoints, and the client
+ /// then hands its code to the attacker's token endpoint while believing it is talking to the
+ /// real one.
+ ///
+ public async Task GetAsync(
+ Uri authority,
+ bool requireHttps,
+ CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(authority);
+
+ if (requireHttps && !string.Equals(authority.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal))
+ {
+ throw new OidcException(
+ $"The authority '{authority}' is not HTTPS. Plaintext metadata is only ever "
+ + "acceptable for local development.");
+ }
+
+ // A trailing slash matters: without it Uri would replace the last path segment, so an
+ // authority with a realm path would resolve to the wrong document.
+ var basePath = authority.AbsoluteUri.EndsWith('/') ? authority : new Uri(authority.AbsoluteUri + "/");
+ var discoveryUri = new Uri(basePath, DiscoveryPath);
+
+ using var response = await http.GetAsync(discoveryUri, cancellationToken).ConfigureAwait(false);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new OidcException(
+ $"Discovery at '{discoveryUri}' returned {(int)response.StatusCode}.");
+ }
+
+ var content = await response.Content
+ .ReadAsStreamAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ await using var contentScope = content.ConfigureAwait(false);
+
+ using var document = await JsonDocument
+ .ParseAsync(content, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ var metadata = Read(document.RootElement, discoveryUri);
+
+ if (!IssuerMatches(metadata.Issuer, authority))
+ {
+ throw new OidcException(
+ $"The discovery document at '{discoveryUri}' declares issuer '{metadata.Issuer}', "
+ + $"which is not the authority '{authority}' it was fetched from.");
+ }
+
+ return metadata;
+ }
+
+ private static OidcProviderMetadata Read(JsonElement root, Uri discoveryUri) =>
+ new(
+ Issuer: RequireUri(root, "issuer", discoveryUri),
+ AuthorizationEndpoint: RequireUri(root, "authorization_endpoint", discoveryUri),
+ TokenEndpoint: RequireUri(root, "token_endpoint", discoveryUri),
+ JwksUri: OptionalUri(root, "jwks_uri"),
+ CodeChallengeMethodsSupported: ReadStrings(root, "code_challenge_methods_supported"));
+
+ ///
+ /// Compared after normalising a single trailing slash, because providers are inconsistent about
+ /// it and a mismatch there is cosmetic rather than an attack.
+ ///
+ private static bool IssuerMatches(Uri issuer, Uri authority) =>
+ string.Equals(
+ issuer.AbsoluteUri.TrimEnd('/'),
+ authority.AbsoluteUri.TrimEnd('/'),
+ StringComparison.OrdinalIgnoreCase);
+
+ private static Uri RequireUri(JsonElement root, string name, Uri discoveryUri)
+ {
+ var value = OptionalUri(root, name)
+ ?? throw new OidcException(
+ $"The discovery document at '{discoveryUri}' has no usable '{name}'.");
+
+ return value;
+ }
+
+ private static Uri? OptionalUri(JsonElement root, string name) =>
+ root.TryGetProperty(name, out var property)
+ && property.ValueKind == JsonValueKind.String
+ && Uri.TryCreate(property.GetString(), UriKind.Absolute, out var uri)
+ ? uri
+ : null;
+
+ private static List ReadStrings(JsonElement root, string name)
+ {
+ if (!root.TryGetProperty(name, out var property) || property.ValueKind != JsonValueKind.Array)
+ {
+ return [];
+ }
+
+ var values = new List();
+ foreach (var element in property.EnumerateArray())
+ {
+ if (element.ValueKind == JsonValueKind.String && element.GetString() is { } value)
+ {
+ values.Add(value);
+ }
+ }
+
+ return values;
+ }
+}
diff --git a/src/DodoSSH.Client.Auth/PkcePair.cs b/src/DodoSSH.Client.Auth/PkcePair.cs
new file mode 100644
index 0000000..dcd8397
--- /dev/null
+++ b/src/DodoSSH.Client.Auth/PkcePair.cs
@@ -0,0 +1,57 @@
+using System.Buffers.Text;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace DodoSSH.Client.Auth;
+
+///
+/// A PKCE code verifier and its S256 challenge. See RFC 7636.
+///
+///
+///
+/// A public client cannot keep a secret, so PKCE is what stops an authorization code being
+/// redeemed by anyone who intercepts it — on a loopback redirect that means any other local
+/// process that manages to receive the callback. The token endpoint only accepts the code
+/// alongside the verifier whose hash it saw at authorization time.
+///
+///
+/// S256 only. The plain method is still in the RFC and offers no protection
+/// whatsoever against an attacker who saw the authorization request.
+///
+///
+public sealed class PkcePair
+{
+ /// The challenge method sent to the authorization endpoint.
+ public const string Method = "S256";
+
+ ///
+ /// Entropy behind the verifier. 32 bytes renders as 43 base64url characters, the RFC's
+ /// minimum length and comfortably beyond guessing.
+ ///
+ private const int VerifierEntropyBytes = 32;
+
+ private PkcePair(string codeVerifier, string codeChallenge)
+ {
+ CodeVerifier = codeVerifier;
+ CodeChallenge = codeChallenge;
+ }
+
+ /// The secret held until the token exchange. Never leaves the process.
+ public string CodeVerifier { get; }
+
+ /// The hash sent with the authorization request.
+ public string CodeChallenge { get; }
+
+ /// Generates a fresh pair.
+ public static PkcePair Create()
+ {
+ // Base64url of random bytes, which satisfies the RFC's unreserved-character set without
+ // any escaping. Generating characters directly from an alphabet would be one more place to
+ // get a modulo bias wrong for no benefit.
+ var verifier = Base64Url.EncodeToString(RandomNumberGenerator.GetBytes(VerifierEntropyBytes));
+
+ var challenge = Base64Url.EncodeToString(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));
+
+ return new PkcePair(verifier, challenge);
+ }
+}
diff --git a/src/DodoSSH.Client.Auth/TokenSet.cs b/src/DodoSSH.Client.Auth/TokenSet.cs
new file mode 100644
index 0000000..a9f3c54
--- /dev/null
+++ b/src/DodoSSH.Client.Auth/TokenSet.cs
@@ -0,0 +1,38 @@
+namespace DodoSSH.Client.Auth;
+
+/// What a token exchange returned.
+///
+/// The refresh token is the one long-lived credential the client holds, and the only part of this
+/// that belongs in the OS keystore. The access token is short-lived and re-obtainable, and the ID
+/// token is an assertion rather than a credential. None of them can open the vault: that needs the
+/// passphrase, which is never stored anywhere.
+///
+/// Bearer token for the DodoSSH API.
+/// Refresh token, when offline_access was granted.
+/// Identity assertion, when openid was requested.
+/// When the access token stops being accepted.
+/// Scopes actually granted, which may be narrower than those requested.
+public sealed record TokenSet(
+ string AccessToken,
+ string? RefreshToken,
+ string? IdToken,
+ DateTimeOffset ExpiresAtUtc,
+ string? Scope)
+{
+ ///
+ /// Whether the access token should be refreshed before use.
+ ///
+ ///
+ /// The margin exists because expiry is checked here and enforced by the server after a network
+ /// round trip. Without it a token that is valid at the moment of the check is rejected by the
+ /// time it arrives, which surfaces as a random 401 mid-sync.
+ ///
+ /// Time source.
+ /// How far ahead to consider the token already expired.
+ public bool NeedsRefresh(TimeProvider clock, TimeSpan? margin = null)
+ {
+ ArgumentNullException.ThrowIfNull(clock);
+
+ return clock.GetUtcNow() + (margin ?? TimeSpan.FromSeconds(60)) >= ExpiresAtUtc;
+ }
+}
diff --git a/src/DodoSSH.Client.Auth/packages.lock.json b/src/DodoSSH.Client.Auth/packages.lock.json
new file mode 100644
index 0000000..722652b
--- /dev/null
+++ b/src/DodoSSH.Client.Auth/packages.lock.json
@@ -0,0 +1,19 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.134, )",
+ "resolved": "3.0.134",
+ "contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
+ },
+ "Microsoft.CodeAnalysis.BannedApiAnalyzers": {
+ "type": "Direct",
+ "requested": "[5.6.0, )",
+ "resolved": "5.6.0",
+ "contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/DodoSSH.Client.Auth.Tests/DodoSSH.Client.Auth.Tests.csproj b/tests/DodoSSH.Client.Auth.Tests/DodoSSH.Client.Auth.Tests.csproj
new file mode 100644
index 0000000..8d5f469
--- /dev/null
+++ b/tests/DodoSSH.Client.Auth.Tests/DodoSSH.Client.Auth.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/DodoSSH.Client.Auth.Tests/FakeBrowser.cs b/tests/DodoSSH.Client.Auth.Tests/FakeBrowser.cs
new file mode 100644
index 0000000..f7a6f28
--- /dev/null
+++ b/tests/DodoSSH.Client.Auth.Tests/FakeBrowser.cs
@@ -0,0 +1,92 @@
+using System.Collections.Specialized;
+using System.Web;
+
+namespace DodoSSH.Client.Auth.Tests;
+
+///
+/// Stands in for the system browser, and actually fetches the loopback redirect.
+///
+///
+///
+/// Genuinely drives the client's own over TCP rather than
+/// injecting a callback result. The listener's request parsing, path filtering and response writing
+/// are all part of what could break, and none of it is exercised by handing the flow a fabricated
+/// code.
+///
+///
+/// The redirect is fetched on a background task, not awaited inside . The
+/// client awaits OpenAsync before it begins accepting, so fetching inline would deadlock: the
+/// browser would be waiting for a response that only arrives once the client starts listening.
+///
+///
+internal sealed class FakeBrowser : IBrowserLauncher
+{
+ private readonly Func>? buildCallback;
+
+ ///
+ /// Produces the callback query parameters from the authorization request's parameters. Defaults
+ /// to a successful code response echoing the state back.
+ ///
+ internal FakeBrowser(
+ Func>? buildCallback = null) =>
+ this.buildCallback = buildCallback;
+
+ /// The authorization URL the client asked to open.
+ internal Uri? OpenedUrl { get; private set; }
+
+ /// The authorization request's query parameters.
+ internal NameValueCollection AuthorizeParameters =>
+ HttpUtility.ParseQueryString(OpenedUrl?.Query ?? string.Empty);
+
+ /// The background fetch, so a test can surface its failures.
+ internal Task? CallbackDelivery { get; private set; }
+
+ ///
+ public Task OpenAsync(Uri url, CancellationToken cancellationToken)
+ {
+ OpenedUrl = url;
+
+ var parameters = HttpUtility.ParseQueryString(url.Query);
+ var redirectUri = parameters["redirect_uri"]
+ ?? throw new InvalidOperationException("The authorization URL carried no redirect_uri.");
+
+ var callback = buildCallback is null
+ ? new Dictionary(StringComparer.Ordinal)
+ {
+ ["code"] = "authorization-code",
+ ["state"] = parameters["state"] ?? string.Empty,
+ }
+ : buildCallback(parameters);
+
+ CallbackDelivery = Task.Run(() => FetchAsync(redirectUri, callback), cancellationToken);
+
+ return Task.CompletedTask;
+ }
+
+ private static async Task FetchAsync(
+ string redirectUri,
+ IReadOnlyDictionary callback)
+ {
+ var query = string.Join(
+ '&',
+ callback.Select(p =>
+ $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}"));
+
+ using var client = new HttpClient();
+
+ // A real browser asks for this first. The listener must ignore it rather than treating it as
+ // the callback, so it is part of the flow under test.
+ try
+ {
+ using var favicon = await client.GetAsync(new Uri($"{redirectUri}/../favicon.ico"));
+ }
+ catch (HttpRequestException)
+ {
+ // The listener closes the connection after answering; a transport-level failure here is
+ // not what the test is about.
+ }
+
+ using var response = await client.GetAsync(new Uri($"{redirectUri}?{query}"));
+ _ = await response.Content.ReadAsStringAsync();
+ }
+}
diff --git a/tests/DodoSSH.Client.Auth.Tests/OidcClientTests.cs b/tests/DodoSSH.Client.Auth.Tests/OidcClientTests.cs
new file mode 100644
index 0000000..850468b
--- /dev/null
+++ b/tests/DodoSSH.Client.Auth.Tests/OidcClientTests.cs
@@ -0,0 +1,406 @@
+using System.Collections.Specialized;
+using DodoSSH.Crypto;
+
+namespace DodoSSH.Client.Auth.Tests;
+
+///
+/// The authorization code flow end to end, and the rejections that make it safe.
+///
+///
+/// A loopback redirect is reachable by every process on the machine, so most of the value here is in
+/// the negative cases: a response with the wrong state, a provider whose metadata disagrees with its
+/// own authority, an ID token that binds a different key statement.
+///
+public sealed class OidcClientTests : IDisposable
+{
+ private readonly StubProvider provider = new();
+ private readonly HttpClient http = new();
+
+ ///
+ public void Dispose()
+ {
+ http.Dispose();
+ provider.Dispose();
+ }
+
+ // ---- The happy path ----
+
+ [Fact]
+ public async Task SignIn_CompletesTheFlowAndReturnsTokens()
+ {
+ provider.StubTokenResponse();
+ var browser = new FakeBrowser();
+
+ var tokens = await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken);
+
+ tokens.AccessToken.ShouldBe("access-token");
+ tokens.RefreshToken.ShouldBe("refresh-token");
+ tokens.ExpiresAtUtc.ShouldBeGreaterThan(TimeProvider.System.GetUtcNow());
+
+ await browser.CallbackDelivery!;
+ }
+
+ [Fact]
+ public async Task TheAuthorizationRequest_CarriesEverythingTheFlowNeeds()
+ {
+ // The client_id is asserted explicitly: an authorization request without it is rejected by
+ // the provider with an opaque error, and it is easy to drop while refactoring the builder.
+ provider.StubTokenResponse();
+ var browser = new FakeBrowser();
+
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken);
+
+ var parameters = browser.AuthorizeParameters;
+
+ parameters["response_type"].ShouldBe("code");
+ parameters["client_id"].ShouldBe("dodossh-desktop");
+ parameters["code_challenge_method"].ShouldBe("S256");
+ parameters["code_challenge"].ShouldNotBeNullOrWhiteSpace();
+ parameters["state"].ShouldNotBeNullOrWhiteSpace();
+ parameters["scope"].ShouldBe("openid profile email offline_access");
+ parameters["redirect_uri"].ShouldNotBeNull();
+ parameters["redirect_uri"]!.ShouldStartWith("http://127.0.0.1:");
+
+ await browser.CallbackDelivery!;
+ }
+
+ [Fact]
+ public async Task TheTokenExchange_SendsTheVerifierAndTheSameRedirectUri()
+ {
+ // The token endpoint compares redirect_uri byte-for-byte against what it saw at
+ // authorization time, so a reconstructed value fails in a way that is hard to diagnose.
+ provider.StubTokenResponse();
+ var browser = new FakeBrowser();
+
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken);
+
+ var form = provider.LastTokenRequestForm();
+
+ form["grant_type"].ShouldBe("authorization_code");
+ form["code"].ShouldBe("authorization-code");
+ form["client_id"].ShouldBe("dodossh-desktop");
+ form["code_verifier"].ShouldNotBeNullOrWhiteSpace();
+ form["redirect_uri"].ShouldBe(browser.AuthorizeParameters["redirect_uri"]);
+
+ await browser.CallbackDelivery!;
+ }
+
+ [Fact]
+ public async Task TheVerifierSent_MatchesTheChallengeAdvertised()
+ {
+ // Otherwise PKCE is theatre: the provider would reject it, but only in production.
+ provider.StubTokenResponse();
+ var browser = new FakeBrowser();
+
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken);
+
+ var verifier = provider.LastTokenRequestForm()["code_verifier"];
+ var challenge = browser.AuthorizeParameters["code_challenge"];
+
+ Recompute(verifier).ShouldBe(challenge);
+
+ await browser.CallbackDelivery!;
+ }
+
+ // ---- Rejections ----
+
+ [Fact]
+ public async Task AResponseWithTheWrongState_IsRejected()
+ {
+ // The CSRF defence, and on loopback the defence against any local process racing to deliver
+ // its own code to our listener.
+ provider.StubTokenResponse();
+
+ var browser = new FakeBrowser(_ => new Dictionary(StringComparer.Ordinal)
+ {
+ ["code"] = "attacker-code",
+ ["state"] = "not-the-state-we-sent",
+ });
+
+ var exception = await Should.ThrowAsync(async () =>
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken));
+
+ exception.Message.ShouldContain("state");
+ }
+
+ [Fact]
+ public async Task AResponseWithTheWrongState_NeverReachesTheTokenEndpoint()
+ {
+ // A rejection that still redeemed the code would defeat the point.
+ provider.StubTokenResponse();
+
+ var browser = new FakeBrowser(_ => new Dictionary(StringComparer.Ordinal)
+ {
+ ["code"] = "attacker-code",
+ ["state"] = "wrong",
+ });
+
+ await Should.ThrowAsync(async () =>
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken));
+
+ Should.Throw(() => provider.LastTokenRequestForm());
+ }
+
+ [Fact]
+ public async Task AProviderRefusal_SurfacesWithItsErrorCode()
+ {
+ var browser = new FakeBrowser(_ => new Dictionary(StringComparer.Ordinal)
+ {
+ ["error"] = "access_denied",
+ ["error_description"] = "The user said no",
+ });
+
+ var exception = await Should.ThrowAsync(async () =>
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken));
+
+ exception.ErrorCode.ShouldBe("access_denied");
+ exception.Message.ShouldContain("The user said no");
+ }
+
+ [Fact]
+ public async Task ACallbackWithNoCode_IsRejected()
+ {
+ var browser = new FakeBrowser(parameters =>
+ new Dictionary(StringComparer.Ordinal)
+ {
+ ["state"] = parameters["state"] ?? string.Empty,
+ });
+
+ await Should.ThrowAsync(async () =>
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken));
+ }
+
+ [Fact]
+ public async Task ATokenEndpointError_Surfaces()
+ {
+ provider.StubTokenError(400, "invalid_grant", "Code already redeemed");
+ var browser = new FakeBrowser();
+
+ var exception = await Should.ThrowAsync(async () =>
+ await CreateClient(browser).SignInAsync(TestContext.Current.CancellationToken));
+
+ exception.ErrorCode.ShouldBe("invalid_grant");
+ }
+
+ [Fact]
+ public async Task AProviderThatDoesNotOfferS256_IsRejected()
+ {
+ // Falling back to 'plain' is not an option for a public client, so this is fatal rather than
+ // a silent downgrade.
+ using var plainOnly = new StubProvider(advertiseS256: false);
+ plainOnly.StubTokenResponse();
+
+ var client = new OidcClient(
+ http,
+ new FakeBrowser(),
+ TimeProvider.System,
+ OptionsFor(plainOnly.Authority));
+
+ var exception = await Should.ThrowAsync(async () =>
+ await client.SignInAsync(TestContext.Current.CancellationToken));
+
+ exception.Message.ShouldContain("S256");
+ }
+
+ [Fact]
+ public async Task AProviderWhoseMetadataDeclaresAnotherIssuer_IsRejected()
+ {
+ // RFC 8414 §3.3. Without this check an attacker-controlled authority can serve metadata
+ // pointing at its own token endpoint, and the client hands over the code believing it is
+ // talking to the real provider.
+ using var lying = new StubProvider(issuerOverride: "https://someone-else.example");
+
+ var client = new OidcClient(
+ http,
+ new FakeBrowser(),
+ TimeProvider.System,
+ OptionsFor(lying.Authority));
+
+ var exception = await Should.ThrowAsync(async () =>
+ await client.SignInAsync(TestContext.Current.CancellationToken));
+
+ exception.Message.ShouldContain("issuer");
+ }
+
+ [Fact]
+ public async Task APlaintextAuthority_IsRejectedWhenHttpsIsRequired()
+ {
+ var client = new OidcClient(
+ http,
+ new FakeBrowser(),
+ TimeProvider.System,
+ new OidcClientOptions
+ {
+ Authority = provider.Authority,
+ ClientId = "dodossh-desktop",
+ RequireHttpsMetadata = true,
+ });
+
+ var exception = await Should.ThrowAsync(async () =>
+ await client.SignInAsync(TestContext.Current.CancellationToken));
+
+ exception.Message.ShouldContain("HTTPS");
+ }
+
+ // ---- Refresh ----
+
+ [Fact]
+ public async Task Refresh_ExchangesTheToken()
+ {
+ provider.StubTokenResponse(accessToken: "fresh-access", refreshToken: "rotated-refresh");
+
+ var tokens = await CreateClient(new FakeBrowser())
+ .RefreshAsync("old-refresh", TestContext.Current.CancellationToken);
+
+ tokens.AccessToken.ShouldBe("fresh-access");
+ tokens.RefreshToken.ShouldBe("rotated-refresh");
+
+ var form = provider.LastTokenRequestForm();
+ form["grant_type"].ShouldBe("refresh_token");
+ form["refresh_token"].ShouldBe("old-refresh");
+ form["client_id"].ShouldBe("dodossh-desktop");
+ }
+
+ [Fact]
+ public async Task Refresh_CarriesForwardATokenTheProviderDidNotRotate()
+ {
+ // Many providers omit refresh_token when they do not rotate. Returning null there would make
+ // the caller discard a token that is still valid and sign the user out on the next launch.
+ provider.StubTokenResponse(refreshToken: null);
+
+ var tokens = await CreateClient(new FakeBrowser())
+ .RefreshAsync("still-good", TestContext.Current.CancellationToken);
+
+ tokens.RefreshToken.ShouldBe("still-good");
+ }
+
+ [Fact]
+ public async Task AnAbsentExpiresIn_YieldsAShortLifetimeRatherThanNone()
+ {
+ // Treating "unspecified" as "never expires" hands the caller a token it never refreshes.
+ provider.StubTokenResponse(expiresInSeconds: null);
+
+ var before = TimeProvider.System.GetUtcNow();
+
+ var tokens = await CreateClient(new FakeBrowser())
+ .RefreshAsync("refresh", TestContext.Current.CancellationToken);
+
+ tokens.ExpiresAtUtc.ShouldBeGreaterThan(before);
+ tokens.ExpiresAtUtc.ShouldBeLessThan(before.AddMinutes(10));
+ }
+
+ // ---- The key binding ----
+
+ [Fact]
+ public async Task AuthorizeKeyBinding_SendsTheNonceAndForcesAFreshLogin()
+ {
+ // The primary public-key trust anchor. prompt=login means the assertion attests to a user
+ // present now, not to a session opened at some unknown earlier time.
+ var nonce = NonceFor("alice");
+ provider.StubTokenResponse(includeIdToken: true, idTokenNonce: nonce);
+
+ var browser = new FakeBrowser();
+
+ var idToken = await CreateClient(browser)
+ .AuthorizeKeyBindingAsync(nonce, TestContext.Current.CancellationToken);
+
+ idToken.ShouldNotBeNullOrWhiteSpace();
+
+ var parameters = browser.AuthorizeParameters;
+ parameters["nonce"].ShouldBe(nonce);
+ parameters["prompt"].ShouldBe("login");
+
+ await browser.CallbackDelivery!;
+ }
+
+ [Fact]
+ public async Task AuthorizeKeyBinding_RequestsOnlyOpenid()
+ {
+ // A second refresh token would be one more long-lived credential to store for no benefit.
+ var nonce = NonceFor("alice");
+ provider.StubTokenResponse(includeIdToken: true, idTokenNonce: nonce);
+
+ var browser = new FakeBrowser();
+
+ await CreateClient(browser).AuthorizeKeyBindingAsync(nonce, TestContext.Current.CancellationToken);
+
+ browser.AuthorizeParameters["scope"].ShouldBe("openid");
+
+ await browser.CallbackDelivery!;
+ }
+
+ [Fact]
+ public async Task AuthorizeKeyBinding_RejectsATokenBindingADifferentStatement()
+ {
+ // Accepting it would store evidence that verifies against keys we are not publishing, which
+ // is precisely the fabrication the binding exists to prevent.
+ var requested = NonceFor("alice");
+ var somethingElse = NonceFor("bob");
+
+ provider.StubTokenResponse(includeIdToken: true, idTokenNonce: somethingElse);
+
+ var exception = await Should.ThrowAsync(async () =>
+ await CreateClient(new FakeBrowser())
+ .AuthorizeKeyBindingAsync(requested, TestContext.Current.CancellationToken));
+
+ exception.Message.ShouldContain("nonce");
+ }
+
+ [Fact]
+ public async Task AuthorizeKeyBinding_RejectsATokenWithNoNonceAtAll()
+ {
+ var nonce = NonceFor("alice");
+ provider.StubTokenResponse(includeIdToken: true, idTokenNonce: null);
+
+ await Should.ThrowAsync(async () =>
+ await CreateClient(new FakeBrowser())
+ .AuthorizeKeyBindingAsync(nonce, TestContext.Current.CancellationToken));
+ }
+
+ [Fact]
+ public async Task AuthorizeKeyBinding_RejectsAResponseWithNoIdToken()
+ {
+ var nonce = NonceFor("alice");
+ provider.StubTokenResponse(includeIdToken: false);
+
+ var exception = await Should.ThrowAsync(async () =>
+ await CreateClient(new FakeBrowser())
+ .AuthorizeKeyBindingAsync(nonce, TestContext.Current.CancellationToken));
+
+ exception.Message.ShouldContain("ID token");
+ }
+
+ // ---- Helpers ----
+
+ private OidcClient CreateClient(IBrowserLauncher browser) =>
+ new(http, browser, TimeProvider.System, OptionsFor(provider.Authority));
+
+ private static OidcClientOptions OptionsFor(Uri authority) =>
+ new()
+ {
+ Authority = authority,
+ ClientId = "dodossh-desktop",
+
+ // The stub serves plaintext on a loopback port.
+ RequireHttpsMetadata = false,
+ };
+
+ /// A real binding nonce, from a real key statement.
+ private static string NonceFor(string subject) =>
+ KeyStatementCodec.ComputeNonce(new KeyStatementFields(
+ Version: 1,
+ Issuer: "https://idp.example",
+ Subject: subject,
+ Email: null,
+ EncryptionPublicKey: [.. Enumerable.Range(0, 32).Select(i => (byte)(0x40 + i))],
+ SigningPublicKey: [.. Enumerable.Range(0, 32).Select(i => (byte)(0x60 + i))],
+ KeyGeneration: 1,
+ CreatedAt: DateTimeOffset.FromUnixTimeMilliseconds(1_750_000_000_123),
+ DeviceName: "laptop"));
+
+ /// Recomputes an S256 challenge from a verifier, independently of PkcePair.
+ private static string Recompute(string? verifier) =>
+ System.Buffers.Text.Base64Url.EncodeToString(
+ System.Security.Cryptography.SHA256.HashData(
+ System.Text.Encoding.ASCII.GetBytes(verifier ?? string.Empty)));
+}
diff --git a/tests/DodoSSH.Client.Auth.Tests/StubProvider.cs b/tests/DodoSSH.Client.Auth.Tests/StubProvider.cs
new file mode 100644
index 0000000..b235770
--- /dev/null
+++ b/tests/DodoSSH.Client.Auth.Tests/StubProvider.cs
@@ -0,0 +1,175 @@
+using System.Buffers.Text;
+using System.Text;
+using System.Text.Json.Nodes;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using WireMock.Server;
+
+namespace DodoSSH.Client.Auth.Tests;
+
+/// An identity provider stub: discovery and a token endpoint.
+internal sealed class StubProvider : IDisposable
+{
+ private readonly WireMockServer server;
+
+ internal StubProvider(
+ bool advertiseS256 = true,
+ string? issuerOverride = null)
+ {
+ server = WireMockServer.Start();
+ Authority = new Uri(server.Url!.TrimEnd('/'), UriKind.Absolute);
+
+ StubDiscovery(advertiseS256, issuerOverride);
+ }
+
+ /// The provider's base URL, which is also its issuer.
+ internal Uri Authority { get; }
+
+ /// Requests the stub received, so tests can assert on what was sent.
+ internal IReadOnlyList Requests => server.LogEntries.ToList();
+
+ /// Stubs a successful token response.
+ internal void StubTokenResponse(
+ string accessToken = "access-token",
+ string? refreshToken = "refresh-token",
+ string? idTokenNonce = null,
+ bool includeIdToken = false,
+ int? expiresInSeconds = 3600)
+ {
+ var body = new JsonObject
+ {
+ ["access_token"] = accessToken,
+ ["token_type"] = "Bearer",
+ };
+
+ if (expiresInSeconds is { } seconds)
+ {
+ body["expires_in"] = seconds;
+ }
+
+ if (refreshToken is not null)
+ {
+ body["refresh_token"] = refreshToken;
+ }
+
+ if (includeIdToken)
+ {
+ body["id_token"] = BuildIdToken(idTokenNonce);
+ }
+
+ server
+ .Given(Request.Create().WithPath("/connect/token").UsingPost())
+ .RespondWith(Response.Create()
+ .WithStatusCode(200)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(body.ToJsonString()));
+ }
+
+ /// Stubs an error from the token endpoint.
+ internal void StubTokenError(int statusCode, string error, string description)
+ {
+ var body = new JsonObject
+ {
+ ["error"] = error,
+ ["error_description"] = description,
+ };
+
+ server
+ .Given(Request.Create().WithPath("/connect/token").UsingPost())
+ .RespondWith(Response.Create()
+ .WithStatusCode(statusCode)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(body.ToJsonString()));
+ }
+
+ /// The form body the token endpoint last received, parsed.
+ internal IReadOnlyDictionary LastTokenRequestForm()
+ {
+ var entries = server.LogEntries
+ .Where(e => e.RequestMessage?.Path?
+ .EndsWith("/connect/token", StringComparison.Ordinal) == true)
+ .ToList();
+
+ if (entries.Count == 0)
+ {
+ throw new InvalidOperationException("The token endpoint was never called.");
+ }
+
+ var body = entries[^1].RequestMessage?.Body ?? string.Empty;
+ var form = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var pair in body.Split('&', StringSplitOptions.RemoveEmptyEntries))
+ {
+ var separator = pair.IndexOf('=', StringComparison.Ordinal);
+ if (separator < 0)
+ {
+ continue;
+ }
+
+ form[Uri.UnescapeDataString(pair[..separator])] =
+ Uri.UnescapeDataString(pair[(separator + 1)..].Replace('+', ' '));
+ }
+
+ return form;
+ }
+
+ ///
+ public void Dispose()
+ {
+ server.Stop();
+ server.Dispose();
+ }
+
+ ///
+ /// Builds a JWT-shaped ID token with an unverifiable signature.
+ ///
+ ///
+ /// Unsigned on purpose. The client reads the nonce without validating the signature, which OIDC
+ /// Core §3.1.3.7 permits for a token received directly from the token endpoint over TLS — so a
+ /// real signature here would test nothing the client does.
+ ///
+ private static string BuildIdToken(string? nonce)
+ {
+ var header = new JsonObject { ["alg"] = "RS256", ["typ"] = "JWT" };
+ var payload = new JsonObject { ["sub"] = "alice", ["iss"] = "https://idp.example" };
+
+ if (nonce is not null)
+ {
+ payload["nonce"] = nonce;
+ }
+
+ return string.Join('.',
+ Base64Url.EncodeToString(Encoding.UTF8.GetBytes(header.ToJsonString())),
+ Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payload.ToJsonString())),
+ Base64Url.EncodeToString("not-a-real-signature"u8));
+ }
+
+ private void StubDiscovery(bool advertiseS256, string? issuerOverride)
+ {
+ var document = new JsonObject
+ {
+ ["issuer"] = issuerOverride ?? Authority.AbsoluteUri.TrimEnd('/'),
+ ["authorization_endpoint"] = $"{Authority.AbsoluteUri.TrimEnd('/')}/connect/authorize",
+ ["token_endpoint"] = $"{Authority.AbsoluteUri.TrimEnd('/')}/connect/token",
+ ["jwks_uri"] = $"{Authority.AbsoluteUri.TrimEnd('/')}/.well-known/jwks.json",
+ ["response_types_supported"] = new JsonArray("code"),
+ ["id_token_signing_alg_values_supported"] = new JsonArray("RS256"),
+ };
+
+ if (advertiseS256)
+ {
+ document["code_challenge_methods_supported"] = new JsonArray("S256");
+ }
+ else
+ {
+ document["code_challenge_methods_supported"] = new JsonArray("plain");
+ }
+
+ server
+ .Given(Request.Create().WithPath("/.well-known/openid-configuration").UsingGet())
+ .RespondWith(Response.Create()
+ .WithStatusCode(200)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(document.ToJsonString()));
+ }
+}
diff --git a/tests/DodoSSH.Client.Auth.Tests/packages.lock.json b/tests/DodoSSH.Client.Auth.Tests/packages.lock.json
new file mode 100644
index 0000000..739c6a5
--- /dev/null
+++ b/tests/DodoSSH.Client.Auth.Tests/packages.lock.json
@@ -0,0 +1,1398 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.134, )",
+ "resolved": "3.0.134",
+ "contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
+ },
+ "Microsoft.CodeAnalysis.BannedApiAnalyzers": {
+ "type": "Direct",
+ "requested": "[5.6.0, )",
+ "resolved": "5.6.0",
+ "contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
+ },
+ "NSubstitute": {
+ "type": "Direct",
+ "requested": "[6.0.0, )",
+ "resolved": "6.0.0",
+ "contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==",
+ "dependencies": {
+ "Castle.Core": "5.1.1"
+ }
+ },
+ "Shouldly": {
+ "type": "Direct",
+ "requested": "[4.3.0, )",
+ "resolved": "4.3.0",
+ "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
+ "dependencies": {
+ "DiffEngine": "11.3.0",
+ "EmptyFiles": "4.4.0"
+ }
+ },
+ "WireMock.Net": {
+ "type": "Direct",
+ "requested": "[2.13.0, )",
+ "resolved": "2.13.0",
+ "contentHash": "msedNpcc2vBHSpmdpRmKSxJUsMIIA/MgeJw1OfiOnHEbpxiXDKWNe/LSOGfyMtzzoeWmbQ4XDu1FpwyBG+8JMQ==",
+ "dependencies": {
+ "WireMock.Net.GraphQL": "2.13.0",
+ "WireMock.Net.Matchers.SystemTextJsonPath": "2.13.0",
+ "WireMock.Net.MimePart": "2.13.0",
+ "WireMock.Net.Minimal": "2.13.0",
+ "WireMock.Net.OpenTelemetry": "2.13.0",
+ "WireMock.Net.ProtoBuf": "2.13.0"
+ }
+ },
+ "xunit.v3": {
+ "type": "Direct",
+ "requested": "[3.2.2, )",
+ "resolved": "3.2.2",
+ "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==",
+ "dependencies": {
+ "xunit.v3.mtp-v1": "[3.2.2]"
+ }
+ },
+ "AnyOf": {
+ "type": "Transitive",
+ "resolved": "0.5.0.1",
+ "contentHash": "WDQw5Qos3mhCumSCgKD70TM1dmqBAuJFGv1cFtNTwTaDLZR7kGy33M5C+L0vZV/bNRNwyi5ABvRGPWHL17rkNw=="
+ },
+ "Castle.Core": {
+ "type": "Transitive",
+ "resolved": "5.1.1",
+ "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
+ "dependencies": {
+ "System.Diagnostics.EventLog": "6.0.0"
+ }
+ },
+ "DiffEngine": {
+ "type": "Transitive",
+ "resolved": "11.3.0",
+ "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
+ "dependencies": {
+ "EmptyFiles": "4.4.0",
+ "System.Management": "6.0.1"
+ }
+ },
+ "EmptyFiles": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
+ },
+ "Fare": {
+ "type": "Transitive",
+ "resolved": "2.2.1",
+ "contentHash": "21XZo/yuXK1k0EUhdLnjgRD4n0HQYmPFchV6uaORcRc65rasZ1vdm2dmJXPBKZiIBztRRYRmmg/B76W721VWkA=="
+ },
+ "GraphQL": {
+ "type": "Transitive",
+ "resolved": "8.5.0",
+ "contentHash": "BZkfH7GVacTZEkyqa4XN9mW12UA/0XYrpEkkrJnNBf0Pqw8CZWQfItLaWgG2C1Ju9YHH1UUG+LmIpo8iMP6pKA==",
+ "dependencies": {
+ "GraphQL-Parser": "9.5.0",
+ "GraphQL.Analyzers": "8.5.0"
+ }
+ },
+ "GraphQL-Parser": {
+ "type": "Transitive",
+ "resolved": "9.5.0",
+ "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ=="
+ },
+ "GraphQL.Analyzers": {
+ "type": "Transitive",
+ "resolved": "8.5.0",
+ "contentHash": "jwfvZD5agmw9J8iZEe6BUfKAY+/lC7EqDQg+6JRwXaQ6G/MCLy8jyBc2SHF/2JdAtrkygc1bVyCUP5mR2PHzVA=="
+ },
+ "GraphQL.NewtonsoftJson": {
+ "type": "Transitive",
+ "resolved": "8.5.0",
+ "contentHash": "tAeUoUhJih5fdZRCV0ue3G/gsu8YBiyNZkgLVFyk0wTk8vJGLTBDJaP5o5LVo1edVnk0bR+0/PaNXAEJxkVrTw==",
+ "dependencies": {
+ "GraphQL": "[8.5.0, 9.0.0)",
+ "Newtonsoft.Json": "13.0.3"
+ }
+ },
+ "Handlebars.Net": {
+ "type": "Transitive",
+ "resolved": "2.1.6",
+ "contentHash": "WsYWCEXsIM6hEOSOSRHtIYLjC8BnbT5MVmqhNKRqUI7qiv0t8x3nJiBTEv0ZZfvUAMAFnadGIzSsS/U2anVG1Q=="
+ },
+ "Handlebars.Net.Helpers": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "MZ0/Nvy3XdEy/igZD4fJy5HiUKcKUA170Sq+2RmJFsGiWSqlroXzp8TQqJTDCi1aBtETfQOVdBG0YNvuDs9+uQ==",
+ "dependencies": {
+ "Handlebars.Net.Helpers.Core": "2.5.5"
+ }
+ },
+ "Handlebars.Net.Helpers.Core": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "vLTL6UrLUPPiWDCKig8FLhSU+i9J4n/8RfrhadvnvxqziyK0ArxKMT2gLqQ+X/8vJaRcI9zvD5HxA8KjWbq3Dw==",
+ "dependencies": {
+ "Handlebars.Net": "2.1.6",
+ "Stef.Validation": "0.1.1"
+ }
+ },
+ "Handlebars.Net.Helpers.Humanizer": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "A7TmfLtv7x8HiVckXBmKmOAsO5GKxjSOjxymXS70upqzLLH8BjrhFl+QIGFCdVIWQRx3+yNjGcsz/JXNwt9YZg==",
+ "dependencies": {
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Core": "2.5.5",
+ "Humanizer": "[2.14.1, 4.0.0)"
+ }
+ },
+ "Handlebars.Net.Helpers.Json": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "iRBo/ik0M8M6ezJt4QzZm5KQptEdeh6bVtnDbieuxh5YPTUsPMFvtoq0gg426PwrahE+5rXoFZmIM11Oy5GwTg==",
+ "dependencies": {
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Core": "2.5.5",
+ "Newtonsoft.Json": "13.0.3"
+ }
+ },
+ "Handlebars.Net.Helpers.Random": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "zKcfFDN4QxgEjk4Em9yz/PQu0mBpIgEaqjhacg2Fl6M0oSsF7VBVflae2WRM9MtiVeRTwLkVwcy7TvJ6iqFuVQ==",
+ "dependencies": {
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Core": "2.5.5",
+ "RandomDataGenerator.Net": "1.0.19"
+ }
+ },
+ "Handlebars.Net.Helpers.Xeger": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "J+w9KalIuYlTKMeIv8eoisdoMEz44elri0UOLtfTAuDbADwnBBsJGp4kAQI107+hBcqery9OCRCXm8fvH4eCxQ==",
+ "dependencies": {
+ "Fare": "2.2.1",
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Core": "2.5.5"
+ }
+ },
+ "Handlebars.Net.Helpers.XPath": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "uUGzjR5w5YCv+BdWQ4RpWAho0tUG0zfAKG5v+abXS6+E+fjbfSshOg7LyoWTVcGTWO0PouukhSMUFaumB2K4tg==",
+ "dependencies": {
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Core": "2.5.5",
+ "XPath2.Extensions": "1.1.5"
+ }
+ },
+ "Handlebars.Net.Helpers.Xslt": {
+ "type": "Transitive",
+ "resolved": "2.5.5",
+ "contentHash": "bOaX47avO4Uja6jTZcBAgS5KjL/2ZaewCpB0Oy7cVegctPyxiiRx/T44XGSt0133hHry9f5nJVsjFKNLrYq0Pg==",
+ "dependencies": {
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Core": "2.5.5"
+ }
+ },
+ "Humanizer": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
+ "dependencies": {
+ "Humanizer.Core.af": "2.14.1",
+ "Humanizer.Core.ar": "2.14.1",
+ "Humanizer.Core.az": "2.14.1",
+ "Humanizer.Core.bg": "2.14.1",
+ "Humanizer.Core.bn-BD": "2.14.1",
+ "Humanizer.Core.cs": "2.14.1",
+ "Humanizer.Core.da": "2.14.1",
+ "Humanizer.Core.de": "2.14.1",
+ "Humanizer.Core.el": "2.14.1",
+ "Humanizer.Core.es": "2.14.1",
+ "Humanizer.Core.fa": "2.14.1",
+ "Humanizer.Core.fi-FI": "2.14.1",
+ "Humanizer.Core.fr": "2.14.1",
+ "Humanizer.Core.fr-BE": "2.14.1",
+ "Humanizer.Core.he": "2.14.1",
+ "Humanizer.Core.hr": "2.14.1",
+ "Humanizer.Core.hu": "2.14.1",
+ "Humanizer.Core.hy": "2.14.1",
+ "Humanizer.Core.id": "2.14.1",
+ "Humanizer.Core.is": "2.14.1",
+ "Humanizer.Core.it": "2.14.1",
+ "Humanizer.Core.ja": "2.14.1",
+ "Humanizer.Core.ko-KR": "2.14.1",
+ "Humanizer.Core.ku": "2.14.1",
+ "Humanizer.Core.lv": "2.14.1",
+ "Humanizer.Core.ms-MY": "2.14.1",
+ "Humanizer.Core.mt": "2.14.1",
+ "Humanizer.Core.nb": "2.14.1",
+ "Humanizer.Core.nb-NO": "2.14.1",
+ "Humanizer.Core.nl": "2.14.1",
+ "Humanizer.Core.pl": "2.14.1",
+ "Humanizer.Core.pt": "2.14.1",
+ "Humanizer.Core.ro": "2.14.1",
+ "Humanizer.Core.ru": "2.14.1",
+ "Humanizer.Core.sk": "2.14.1",
+ "Humanizer.Core.sl": "2.14.1",
+ "Humanizer.Core.sr": "2.14.1",
+ "Humanizer.Core.sr-Latn": "2.14.1",
+ "Humanizer.Core.sv": "2.14.1",
+ "Humanizer.Core.th-TH": "2.14.1",
+ "Humanizer.Core.tr": "2.14.1",
+ "Humanizer.Core.uk": "2.14.1",
+ "Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
+ "Humanizer.Core.uz-Latn-UZ": "2.14.1",
+ "Humanizer.Core.vi": "2.14.1",
+ "Humanizer.Core.zh-CN": "2.14.1",
+ "Humanizer.Core.zh-Hans": "2.14.1",
+ "Humanizer.Core.zh-Hant": "2.14.1"
+ }
+ },
+ "Humanizer.Core": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
+ },
+ "Humanizer.Core.af": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ar": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.az": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.bg": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.bn-BD": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.cs": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.da": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.de": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.el": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.es": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fa": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fi-FI": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fr-BE": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.he": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.hr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.hu": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.hy": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.id": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.is": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.it": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ja": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ko-KR": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ku": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.lv": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ms-MY": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.mt": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.nb": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.nb-NO": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.nl": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.pl": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.pt": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ro": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ru": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sk": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sl": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sr-Latn": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sv": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.th-TH": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.tr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.uk": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.uz-Cyrl-UZ": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.uz-Latn-UZ": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.vi": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.zh-CN": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.zh-Hans": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.zh-Hant": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "JmesPath.Net": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "sL1LeqBm+BWSKvgZN/T470IqkXcKQXmOYsRUZU18jDZeiIBmvUfIe9m3VhiII/jOK/6WmrQ+W8Pqwz3k28WX9g==",
+ "dependencies": {
+ "JmesPath.Net.Parser": "1.1.0",
+ "Newtonsoft.Json": "13.0.4"
+ }
+ },
+ "JmesPath.Net.Parser": {
+ "type": "Transitive",
+ "resolved": "1.1.0",
+ "contentHash": "NLTE/dPy8lMcZO6E7SL5Jw3fay8Vesll7+hkeRVSRaVNg1RRyPBV3/u6CM7QNgtnzhvyFPDyxUHUuRdh0vzCSg=="
+ },
+ "Json.More.Net": {
+ "type": "Transitive",
+ "resolved": "3.0.1",
+ "contentHash": "fRctF2J2SILYG6wqP21drmeEODmCVkVQ/b3MndDu2fT1swfySyUgq7ePCk+aENGlDcIm05fyfjh9vcuqDEfv3w=="
+ },
+ "JsonConverter.Abstractions": {
+ "type": "Transitive",
+ "resolved": "0.13.0",
+ "contentHash": "Ci3nuKx3GgMDfW9JA4dJpU+hJV5G1ve72mploQP8ivSDpOmo2QbfVAQkxsVzb3UQJCgwxxM6rdE/fPXwM0yj0g=="
+ },
+ "JsonConverter.Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "0.13.0",
+ "contentHash": "K6doeW12emLiJV4laUf58y3kkjng6/IARRtC7+20qIOBrP1pBxGRsjz2IfxCgSBkTML3q6FWe7O+/UE374lsaA==",
+ "dependencies": {
+ "JsonConverter.Abstractions": "0.13.0",
+ "Newtonsoft.Json": "13.0.4",
+ "Stef.Validation": "0.1.1"
+ }
+ },
+ "JsonConverter.System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "0.13.0",
+ "contentHash": "UtRbkZT16Z0OVZ9n/h60E0GlUDTul/DjpuuajdsCNvefsmTxf6WNB8Cq9Hjwu9pGjfqOaFOmaz8k54DaHBmc0g==",
+ "dependencies": {
+ "JsonConverter.Abstractions": "0.13.0",
+ "Stef.Validation": "0.1.1"
+ }
+ },
+ "JsonPath.Net": {
+ "type": "Transitive",
+ "resolved": "3.0.2",
+ "contentHash": "Cmt2mvPYOLljjqSfM1xUYZYTPf8MPbwv2XpCpPxxq9u23/CGrz/fljgd1fJUNujd3+E1adNOyF1TLwppbyQwxg==",
+ "dependencies": {
+ "Json.More.Net": "3.0.1"
+ }
+ },
+ "MetadataReferenceService.Abstractions": {
+ "type": "Transitive",
+ "resolved": "0.0.1",
+ "contentHash": "Sf5ip58vlqWkQIAULIOKFIIFuhtRd8lChsJRZdFo746NVApEp/qgxNf/zCLjbB/RA/8TQGXWrFPKpqjyeh3EMg==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.CSharp": "4.8.0",
+ "Stef.Validation": "0.1.1"
+ }
+ },
+ "MetadataReferenceService.Default": {
+ "type": "Transitive",
+ "resolved": "0.0.1",
+ "contentHash": "ihrchqYobpQMA9tn0W+MGD3oe5onqCttbR3lQfEiVzwF0V9/DS+K4YtvsUPGDC9XIie2Xw3lugSSk97k+OUwnQ==",
+ "dependencies": {
+ "MetadataReferenceService.Abstractions": "0.0.1"
+ }
+ },
+ "Microsoft.ApplicationInsights": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
+ },
+ "Microsoft.AspNetCore.Http": {
+ "type": "Transitive",
+ "resolved": "2.3.9",
+ "contentHash": "+CcfWi1LoKYbcxt+3toO4xbBG+qSSMbPuuow+cbZKIrITXuu1geN1traamL4jG8QaHdHGm3M0eCh+EOgdMgNPA==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
+ "Microsoft.AspNetCore.WebUtilities": "2.3.0",
+ "Microsoft.Extensions.ObjectPool": "8.0.11",
+ "Microsoft.Extensions.Options": "8.0.2",
+ "Microsoft.Net.Http.Headers": "2.3.8"
+ }
+ },
+ "Microsoft.AspNetCore.Http.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.3.0",
+ "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==",
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Features": "2.3.0"
+ }
+ },
+ "Microsoft.AspNetCore.Http.Features": {
+ "type": "Transitive",
+ "resolved": "2.3.0",
+ "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.AspNetCore.WebUtilities": {
+ "type": "Transitive",
+ "resolved": "2.3.0",
+ "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==",
+ "dependencies": {
+ "Microsoft.Net.Http.Headers": "2.3.0"
+ }
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
+ },
+ "Microsoft.CodeAnalysis.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.3.4",
+ "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
+ },
+ "Microsoft.CodeAnalysis.Common": {
+ "type": "Transitive",
+ "resolved": "4.8.0",
+ "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.3.4"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp": {
+ "type": "Transitive",
+ "resolved": "4.8.0",
+ "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Common": "[4.8.0]"
+ }
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA=="
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
+ "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "j8zcwhS6bYB6FEfaY3nYSgHdpiL2T+/V3xjpHtslVAegyI1JUbB9yAt/BFdvZdsNbY0Udm4xFtvfT/hUwcOOOg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "10.0.0",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging": "10.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "8.0.11",
+ "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w=="
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "tL9cSl3maS5FPzp/3MtlZI21ExWhni0nnUCF8HY4npTsINw45n9SNDbkKXBMtFyUFGSsQep25fHIDN4f/Vp3AQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "10.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Options": "10.0.0",
+ "Microsoft.Extensions.Primitives": "10.0.0"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.0",
+ "contentHash": "inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w=="
+ },
+ "Microsoft.IdentityModel.Abstractions": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "5nInt1KKSpKQBlhe6gXz4yKxRzRUQa21vCvSIIKKzAI2e1r9PHQOZc7aRzBA8L/JCvBxLbCxelvUqun6qwWPJg=="
+ },
+ "Microsoft.IdentityModel.JsonWebTokens": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "CZMom/ZoWcgjxLMxmCmcEkuoA0OA4swN1CGeMBQyxF/hEZgRbWK9EnWVJ9/oMUq3D1+OGJjnbN+W6gFq9kZcEg==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Tokens": "6.34.0"
+ }
+ },
+ "Microsoft.IdentityModel.Logging": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "E0AbluNkI30/VKa96PxJhhFZDx/NGYIXFrRIRq1N5/V0TToaiuc3hM90QLFszT2BBQefnp/wjm12ilSudmt9bg==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Abstractions": "6.34.0"
+ }
+ },
+ "Microsoft.IdentityModel.Protocols": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "xrqYK+V3FW+fMQ5oI7cwku2wj1RHz8qym3kh+rD+BTgCw1RmfFyWrLQ8/rVEqTl2nn4NcC0N+sHk0Q4qQ8dK9A==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Logging": "6.34.0",
+ "Microsoft.IdentityModel.Tokens": "6.34.0"
+ }
+ },
+ "Microsoft.IdentityModel.Protocols.OpenIdConnect": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "SN3eZtssgpfnTCUlKsTJn9/0UiSc/HsbGLFl5Xp8vXFLXBeweWiDu54jFngSirjtJd6lSw3GgZhK5LZvVXGGLQ==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Protocols": "6.34.0",
+ "System.IdentityModel.Tokens.Jwt": "6.34.0"
+ }
+ },
+ "Microsoft.IdentityModel.Tokens": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "PEPcGMqbEwEwbpQ6nTld9Nqq6V5BPZSOfk71qXZ7h7DuGuxa13bWvjImhJba5Ko88YvIuZuOBJWFZmjLfwbNXA==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Logging": "6.34.0"
+ }
+ },
+ "Microsoft.Net.Http.Headers": {
+ "type": "Transitive",
+ "resolved": "2.3.8",
+ "contentHash": "JO60u/VVUdaZfv4XQ//zgcH54y8rnxdpcvXnsDqWLKB4adDKaCiaozixDfQ/6H+PKYfkNV2CL8b8U+F9mciE3Q==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Testing.Extensions.Telemetry": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==",
+ "dependencies": {
+ "Microsoft.ApplicationInsights": "2.23.0",
+ "Microsoft.Testing.Platform": "1.9.1"
+ }
+ },
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==",
+ "dependencies": {
+ "Microsoft.Testing.Platform": "1.9.1"
+ }
+ },
+ "Microsoft.Testing.Platform": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA=="
+ },
+ "Microsoft.Testing.Platform.MSBuild": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==",
+ "dependencies": {
+ "Microsoft.Testing.Platform": "1.9.1"
+ }
+ },
+ "Microsoft.Win32.Registry": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg=="
+ },
+ "Namotion.Reflection": {
+ "type": "Transitive",
+ "resolved": "2.1.2",
+ "contentHash": "7tSHAzX8GWKy0qrW6OgQWD7kAZiqzhq+m1503qczuwuK6ZYhOGCQUxw+F3F4KkRM70aB6RMslsRVSCFeouIehw=="
+ },
+ "Newtonsoft.Json": {
+ "type": "Transitive",
+ "resolved": "13.0.4",
+ "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
+ },
+ "NJsonSchema": {
+ "type": "Transitive",
+ "resolved": "10.9.0",
+ "contentHash": "IBPo6Srxn2MEcIFM3HdM4QImrJbsIeujENQyzHL2Pv6wLsKSYAyAEilecRqaLOhoy3snEiPLx7hhv7opbhOxKQ==",
+ "dependencies": {
+ "Namotion.Reflection": "2.1.2",
+ "Newtonsoft.Json": "9.0.1"
+ }
+ },
+ "NJsonSchema.Extensions": {
+ "type": "Transitive",
+ "resolved": "0.2.0",
+ "contentHash": "zLHUfuCmnaaQbKxqvTALrxhXV6Pbdy4G3ZlAI+7oaXdJmSyQPpMHGcxDBmw0+qHziT7jVImxU1BjcidKJHeprg==",
+ "dependencies": {
+ "NJsonSchema": "10.9.0"
+ }
+ },
+ "NSwag.Core": {
+ "type": "Transitive",
+ "resolved": "13.16.1",
+ "contentHash": "xiX+H3Bv6zxrqJExPepO5WQVutkDUMdlUA3NqQ8VguwsYwJlkV05eF8XvmbJn/yGJWUag7vLImuXAoj0/327Bg==",
+ "dependencies": {
+ "NJsonSchema": "10.7.2",
+ "Newtonsoft.Json": "9.0.1"
+ }
+ },
+ "OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "1.15.3",
+ "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3"
+ }
+ },
+ "OpenTelemetry.Api": {
+ "type": "Transitive",
+ "resolved": "1.15.3",
+ "contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g=="
+ },
+ "OpenTelemetry.Api.ProviderBuilderExtensions": {
+ "type": "Transitive",
+ "resolved": "1.15.3",
+ "contentHash": "SYn0lqYDwLMWhv/zlNGsQcl2yX++yTumanX46bmOZE/ZDOd1WjPBO2kZaZgKLEZTZk48pavIFGJ6vOvxXgWVFQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0",
+ "OpenTelemetry.Api": "1.15.3"
+ }
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "Transitive",
+ "resolved": "1.15.3",
+ "contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.3"
+ }
+ },
+ "OpenTelemetry.Extensions.Hosting": {
+ "type": "Transitive",
+ "resolved": "1.15.3",
+ "contentHash": "u8n/W8yIlqv0BXZmvId1iVaeWXG42tGKdTkuLYg5g57Y/r9CeUNzqtrSHNdG5IoO8iPX79w3v+WsbAHgUQbfeg==",
+ "dependencies": {
+ "Microsoft.Extensions.Hosting.Abstractions": "10.0.0",
+ "OpenTelemetry": "1.15.3"
+ }
+ },
+ "OpenTelemetry.Instrumentation.AspNetCore": {
+ "type": "Transitive",
+ "resolved": "1.15.2",
+ "contentHash": "2nPd7r0ug/gd6/CNFL6Rlu+RSQ9WYGSGHAYQ1ssbSqyzKJpqTunfx2I/1O0WB5k+L0cyXbG4XVZpoSoUc3M7wg==",
+ "dependencies": {
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.3, 2.0.0)"
+ }
+ },
+ "protobuf-net": {
+ "type": "Transitive",
+ "resolved": "3.2.52",
+ "contentHash": "XbZurNU3B/VaL/5OJ0kshO+AWxsZroI1saKuLfZpDwH2ngb2K9bdF1nIW6elFOViZw7TQCmfVZapxrMKCDqecQ==",
+ "dependencies": {
+ "protobuf-net.Core": "3.2.52"
+ }
+ },
+ "protobuf-net.Core": {
+ "type": "Transitive",
+ "resolved": "3.2.52",
+ "contentHash": "zOpGtUo2QTgbsiI0D0yCe8aUTgDPov6kqIu1CDHI6isqhYcAHdirRrdnfsQXmAUfAWx1LwVYGgC6xe6fNS4UAg=="
+ },
+ "ProtoBufJsonConverter": {
+ "type": "Transitive",
+ "resolved": "0.11.0",
+ "contentHash": "lxvcZQlCtgYZpfm9hhAJVZ1jsPkb9g3fyaAOnQLyEu8MiAowEIdED6jzwfjqLqOhY4AahqnbfRvZ904Ud43X7w==",
+ "dependencies": {
+ "MetadataReferenceService.Default": "0.0.1",
+ "Microsoft.CodeAnalysis.CSharp": "4.8.0",
+ "Newtonsoft.Json": "13.0.3",
+ "Stef.Validation": "0.1.1",
+ "protobuf-net": "3.2.52"
+ }
+ },
+ "RamlToOpenApiConverter.SourceOnly": {
+ "type": "Transitive",
+ "resolved": "0.21.0",
+ "contentHash": "x0g2c4tgPC5i+ZofhlhSeiWAsOjzkatv523tKGglx0mL05JYevQ5sYPP4r0xthpRAv99/6EuBJ6TbbipsHTMzA=="
+ },
+ "RandomDataGenerator.Net": {
+ "type": "Transitive",
+ "resolved": "1.0.19.1",
+ "contentHash": "OkAqBA69VbYYg+biX2DYWcucOI/yEivkdJ/XPqife/mQAC0r/NArcAU/EI3i/1oRFUUCjibV0b7qEjK+TYTqDw==",
+ "dependencies": {
+ "Fare": "2.2.1",
+ "Stef.Validation": "0.1.1"
+ }
+ },
+ "Scriban.Signed": {
+ "type": "Transitive",
+ "resolved": "7.2.5",
+ "contentHash": "Fu1AjcAyrZIAW9LIhxVgVyo5EMVdwLhXagKKI2A1UZoI0Wvz2CiRT+VXp1tuiMq2JhlkjVyebj/JQcF4koZacg=="
+ },
+ "SharpYaml": {
+ "type": "Transitive",
+ "resolved": "2.1.4",
+ "contentHash": "/iwULhVBpTjD4wPZhLU+eUWBanDvri/2AGx5YbaAj5kp9kXzhqUfJEy56H5Yi+c+OXsdm/oKD1aTKB24BFp8cw=="
+ },
+ "SimMetrics.Net": {
+ "type": "Transitive",
+ "resolved": "1.0.5",
+ "contentHash": "LaSDYOJDh2WncgRboqiWtk/Igqoim/LV7v808qBeWY/f36Ol5oEKguEYpKrWw5ap8KYP0SRXf7/v3zil9koY6Q=="
+ },
+ "Stef.Validation": {
+ "type": "Transitive",
+ "resolved": "0.3.0",
+ "contentHash": "OfzmxQMK4eBzmobph43p1NsLTgVAC3XGTcvQS0odhsdL6uS7UsFzBMK6S9mAfIijZdLW4q98aZ3dtTOeTaQo6Q=="
+ },
+ "System.CodeDom": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
+ },
+ "System.IdentityModel.Tokens.Jwt": {
+ "type": "Transitive",
+ "resolved": "6.34.0",
+ "contentHash": "c0misfmFT3QxKY+a16PGlj+DtiUzoPaf26m2avyPZaLRc9vlIdLtmovfRY5MqN+y/SEoBSRXrgVaeZGPgFQQ6w==",
+ "dependencies": {
+ "Microsoft.IdentityModel.JsonWebTokens": "6.34.0",
+ "Microsoft.IdentityModel.Tokens": "6.34.0"
+ }
+ },
+ "System.Management": {
+ "type": "Transitive",
+ "resolved": "6.0.1",
+ "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
+ "dependencies": {
+ "System.CodeDom": "6.0.0"
+ }
+ },
+ "TinyMapper.Signed": {
+ "type": "Transitive",
+ "resolved": "4.0.0",
+ "contentHash": "W5uc9QXp8PUgP3VQ1Qyt3vK8ptyjj38tJ7nEAtRKA6R/4e6+2gsgYrAmRg9fCK1hhe3E0yeAm5acC14qx2CINg=="
+ },
+ "WireMock.Net.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "7ZmPVJxlSBj0E7d47PHLydz15w0TEJ6WH2m7T/hucT3QfxbhN07AV0mwBoyO/T6jv+Af+hgEgmlep3v9TACxPw=="
+ },
+ "WireMock.Net.GraphQL": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "3nMUB7E8ner6fpdOZe91qx/1O3ebEOke1dCeCw+wIvnmtPhlfdOuss2saICqc1nrwLv2bAOVQ29606Ayinf7kw==",
+ "dependencies": {
+ "GraphQL.NewtonsoftJson": "8.5.0",
+ "WireMock.Net.Shared": "2.13.0"
+ }
+ },
+ "WireMock.Net.Matchers.SystemTextJsonPath": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "uhuQW2mFvi2X3v5Om3nDYYaF/ApkDHsFB0A1pklyLJnBUp1iUp+6Imt3t0j1bpMggHKaXJXEhDCqoDxvGP4FIQ==",
+ "dependencies": {
+ "JsonPath.Net": "3.0.2",
+ "WireMock.Net.Shared": "2.13.0"
+ }
+ },
+ "WireMock.Net.MimePart": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "bFJsUJ+zKOZC8FMO+8kyRHW51Qt5DkKRKS7//dzuBcuJVY2XdEnOdfaAlMfnN9T73XrHCi8skU+G1xdfWD2x3g==",
+ "dependencies": {
+ "Stef.Validation": "0.3.0",
+ "WireMock.Net.Shared": "2.13.0"
+ }
+ },
+ "WireMock.Net.Minimal": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "7VIKKuEiAHa19x7rbyf1s90UymTVQVTXz1CZc9mUM7DdW1LdaYyHUqBK4mBycvzsxuam7+jkU7l+aMJGAb0kBA==",
+ "dependencies": {
+ "JmesPath.Net": "1.1.0",
+ "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.34.0",
+ "NJsonSchema.Extensions": "0.2.0",
+ "NSwag.Core": "13.16.1",
+ "Scriban.Signed": "7.2.5",
+ "SimMetrics.Net": "1.0.5",
+ "TinyMapper.Signed": "4.0.0",
+ "WireMock.Net.OpenApiParser": "2.13.0",
+ "WireMock.Net.Shared": "2.13.0",
+ "WireMock.Org.Abstractions": "2.13.0"
+ }
+ },
+ "WireMock.Net.OpenApiParser": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "XQ2hgycULdhp1F16OqixwDwz2zaPtk3rpdurmL9MLZjr8TUWMCyHfStXeaU/vHu/of/xnYAh+qB0dwtdmGwY0Q==",
+ "dependencies": {
+ "Newtonsoft.Json": "13.0.4",
+ "RamlToOpenApiConverter.SourceOnly": "0.21.0",
+ "RandomDataGenerator.Net": "1.0.19.1",
+ "SharpYaml": "2.1.4",
+ "Stef.Validation": "0.3.0",
+ "WireMock.Net.Abstractions": "2.13.0",
+ "YamlDotNet": "18.1.0"
+ }
+ },
+ "WireMock.Net.OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "3SjRQcAd1pPZXB7jtj7vx7cbWdkskQl030W2QJldIOf+P9VtvKPJ5SsNCKI43Eg5O7RyrYuy2AQVdik7lbUi/Q==",
+ "dependencies": {
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": "1.15.3",
+ "OpenTelemetry.Extensions.Hosting": "1.15.3",
+ "OpenTelemetry.Instrumentation.AspNetCore": "1.15.2",
+ "WireMock.Net.Shared": "2.13.0"
+ }
+ },
+ "WireMock.Net.ProtoBuf": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "WRG6cujOXRe3ZiStkElH82lOlDhS3YiV/cpXQbnCLpTkf+F/RBmrivzqO7ILcTccVKdwiIbT+ANdCR0vVCXZMg==",
+ "dependencies": {
+ "ProtoBufJsonConverter": "0.11.0",
+ "WireMock.Net.Shared": "2.13.0"
+ }
+ },
+ "WireMock.Net.Shared": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "vvA0ssOFv3IoQI5UL5dr3mtAgF5imyit999sWEJYtXp7lLeA/fGvmbYLEpG5CeQ1RRtEcIxnJkRdtcttYmIC4Q==",
+ "dependencies": {
+ "AnyOf": "0.5.0.1",
+ "Handlebars.Net.Helpers": "2.5.5",
+ "Handlebars.Net.Helpers.Humanizer": "2.5.5",
+ "Handlebars.Net.Helpers.Json": "2.5.5",
+ "Handlebars.Net.Helpers.Random": "2.5.5",
+ "Handlebars.Net.Helpers.XPath": "2.5.5",
+ "Handlebars.Net.Helpers.Xeger": "2.5.5",
+ "Handlebars.Net.Helpers.Xslt": "2.5.5",
+ "JsonConverter.Newtonsoft.Json": "0.13.0",
+ "JsonConverter.System.Text.Json": "0.13.0",
+ "Microsoft.AspNetCore.Http": "2.3.9",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
+ "Stef.Validation": "0.3.0",
+ "WireMock.Net.Abstractions": "2.13.0"
+ }
+ },
+ "WireMock.Org.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.13.0",
+ "contentHash": "1z7W3ryp+xufZ77ux3NODNMl/jw00Koagpj8aOaFQQgOQuxJMd8hICi1ErF1DsIXd8Ewp13s2HGntj4yWeRfRA=="
+ },
+ "XPath2": {
+ "type": "Transitive",
+ "resolved": "1.1.5",
+ "contentHash": "LQg7kZyAmmb+qvv5TiOuuijxN97rRbR05qbMkVIH+i+sx9CA2UNUKGNtdVxWEXOabS8BIwlXm6ox1OOTjvZ6jw=="
+ },
+ "XPath2.Extensions": {
+ "type": "Transitive",
+ "resolved": "1.1.5",
+ "contentHash": "oEbdGUJsF25QL3Vj1GgSlT2xdbxnka5dKcjuA9CouWCV/l9ecSfypOv78B1+YUD8a8w47prLNw2i3ofLNcrbGA==",
+ "dependencies": {
+ "Newtonsoft.Json": "13.0.3",
+ "XPath2": "1.1.5"
+ }
+ },
+ "xunit.analyzers": {
+ "type": "Transitive",
+ "resolved": "1.27.0",
+ "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g=="
+ },
+ "xunit.v3.assert": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA=="
+ },
+ "xunit.v3.common": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "6.0.0"
+ }
+ },
+ "xunit.v3.core.mtp-v1": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==",
+ "dependencies": {
+ "Microsoft.Testing.Extensions.Telemetry": "1.9.1",
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1",
+ "Microsoft.Testing.Platform": "1.9.1",
+ "Microsoft.Testing.Platform.MSBuild": "1.9.1",
+ "xunit.v3.extensibility.core": "[3.2.2]",
+ "xunit.v3.runner.inproc.console": "[3.2.2]"
+ }
+ },
+ "xunit.v3.extensibility.core": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==",
+ "dependencies": {
+ "xunit.v3.common": "[3.2.2]"
+ }
+ },
+ "xunit.v3.mtp-v1": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==",
+ "dependencies": {
+ "xunit.analyzers": "1.27.0",
+ "xunit.v3.assert": "[3.2.2]",
+ "xunit.v3.core.mtp-v1": "[3.2.2]"
+ }
+ },
+ "xunit.v3.runner.common": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==",
+ "dependencies": {
+ "Microsoft.Win32.Registry": "[5.0.0]",
+ "xunit.v3.common": "[3.2.2]"
+ }
+ },
+ "xunit.v3.runner.inproc.console": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==",
+ "dependencies": {
+ "xunit.v3.extensibility.core": "[3.2.2]",
+ "xunit.v3.runner.common": "[3.2.2]"
+ }
+ },
+ "YamlDotNet": {
+ "type": "Transitive",
+ "resolved": "18.1.0",
+ "contentHash": "5K+9KFg2TdTl7VXv88Qzi/0lqK6JFoNP3lRuImPYGRV7K/QYklDyTrj4+A+KAki1JsQi6qKY+hDyY7d6WRqjrw=="
+ },
+ "dodossh.client.auth": {
+ "type": "Project"
+ },
+ "dodossh.crypto": {
+ "type": "Project",
+ "dependencies": {
+ "NSec.Cryptography": "[26.4.0, )"
+ }
+ },
+ "libsodium": {
+ "type": "CentralTransitive",
+ "requested": "[1.0.22, )",
+ "resolved": "1.0.22",
+ "contentHash": "KPD9SloJFclrsjnhABu7dzWrcyYkwPbvx5l1gRSPAX/0n+OBtSiVCKtGFv4n+ecWUHU0tCG9LSSwoZZx673zBQ=="
+ },
+ "NSec.Cryptography": {
+ "type": "CentralTransitive",
+ "requested": "[26.4.0, )",
+ "resolved": "26.4.0",
+ "contentHash": "0vsCtY5f+YgQROiWNqzgWp+l2pddfk9FkWoGV/bEo0MuEYPKlJWuoA8aOfO6qp3f+EnObKE3zSJhn1PspJeJVg==",
+ "dependencies": {
+ "libsodium": "[1.0.22, 1.0.23)"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file