Public Access
Wire the Avalonia shell to the vault
The host list now comes from the vault instead of from a form. A fresh machine takes a server URL, signs in through the browser, enrolls, and from then on opens with the passphrase alone. DodoSSH.Client.Session is the composition layer: where a profile lives, how it unlocks, and how a machine gets one. ClientPaths picks a non-roaming per-OS directory — %LOCALAPPDATA% and never %APPDATA%, because a SQLite cache that roams between two machines is a corrupt one, and each machine's outbox is its own. SessionOpener needs no transport at all and could not reach one if it wanted to; that is the offline unlock, asserted rather than asserted about. A wrong passphrase, a stale KDF and a grant revoked by a rekey are three different answers, because the remedies are three different things and telling someone to retype a passphrase that was never the problem is worse than saying nothing. The shell's states are the onboarding story. The recovery code gets its own state that cannot be clicked past: it exists for one moment, losing it with the passphrase loses the vault, and there is no server-side reset by design. It is dropped from memory on confirmation rather than merely hidden. Sign-in is a delegate over IVaultServer, so the whole state machine runs in a test against an in-memory server — no browser, no identity provider, no toolkit. The view models are plain observable objects, which is what makes that possible. What it does not cover is whether the XAML binds to the right names; that needs a rendered tree and Avalonia.Headless, and is its own piece of work. Three things found by doing it rather than by reading it: - Pooled SQLite connections keep the database file open after the last context is disposed. On Windows that means locked, so the application could never replace its own cache — and a test could not clean up after itself, which is how it surfaced. Dispose now clears the pool. - EF's SQLite provider puts the database in WAL mode, so the cache is three files. A comment in ClientCacheFactory claimed the opposite; reading PRAGMA journal_mode off a real launch settled it. WAL is the right mode here — a sync pass writes while the interface reads — so the comment was wrong on the merits as well as on the fact. - Enrolling a device key with nowhere to keep the private half would put a wrap on the server nobody can open and make the device list claim this machine can unlock without a passphrase. Device binding is now optional and the shell declines it until the OS keystore is wired. Verified on Windows: the client created %LOCALAPPDATA%\DodoSSH\cache.db and migrated it on first launch, and msedgewebview2 held an established connection to the data plane while the unlock overlay covered it — which is the point of covering the WebView rather than collapsing it, since a NativeWebView that is never laid out is never realised. 630 tests, up from 593. The recovery-code gate and the offline unlock were each verified by breaking them and watching the right test fail. Still to do for M1's actual definition of done: the manual run against the real API and a real Keycloak. Credentials are not a synced entity type yet, so a connection still asks for a password, and the interface says so rather than implying otherwise.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
The shell's state machine, without Avalonia.
|
||||
|
||||
The view models are plain CommunityToolkit.Mvvm objects, so the whole path a user walks — sign in,
|
||||
enroll, keep the recovery code, unlock, add a host — runs here as ordinary code against an in-memory
|
||||
server and a real SQLite cache. No headless renderer, no identity provider, no clicking.
|
||||
|
||||
What this deliberately does not cover is whether the XAML binds to the right names. That needs a
|
||||
rendered visual tree; Avalonia.Headless is the tool for it and is its own piece of work.
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/DodoSSH.Client.App/DodoSSH.Client.App.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,200 @@
|
||||
using DodoSSH.Client.Api;
|
||||
using DodoSSH.Client.Auth;
|
||||
using DodoSSH.Client.Session;
|
||||
using DodoSSH.Client.Sync;
|
||||
using DodoSSH.Contracts;
|
||||
|
||||
namespace DodoSSH.Client.App.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// A signed-in server, without the signing in.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Stands in for a <c>ServerConnection</c> so the shell's state machine can be driven end to end. The
|
||||
/// account half stores what it is given and reports it back, because the provisioner re-reads <c>/me</c>
|
||||
/// after enrolling and a stub that echoed the request would make that check meaningless. The sync half
|
||||
/// applies pushes and serves them back as a change log, which is enough for the shell — the interesting
|
||||
/// conflict behaviour is covered in <c>DodoSSH.Client.Sync.Tests</c> against a server that enforces
|
||||
/// version checks.
|
||||
/// </remarks>
|
||||
internal sealed class FakeVaultServer : IVaultServer, IAccountApi, ISyncApi, IKeyBindingAuthorizer
|
||||
{
|
||||
private readonly List<SyncChange> log = [];
|
||||
private readonly Dictionary<Guid, SyncChange> rows = [];
|
||||
|
||||
private KeyStatement? statement;
|
||||
private byte[]? wrappedPrivateKey;
|
||||
private KdfParameters? kdfParameters;
|
||||
private VaultSummary? personalVault;
|
||||
|
||||
internal Guid UserId { get; } = Guid.Parse("0192f0c8-4444-7aaa-8bbb-dddddddddddd");
|
||||
|
||||
internal int EnrollmentCount { get; private set; }
|
||||
|
||||
internal int PushCount { get; private set; }
|
||||
|
||||
internal bool IsEnrolled => statement is not null;
|
||||
|
||||
internal int LiveRowCount => rows.Values.Count(row => row.Operation != SyncOperation.Delete);
|
||||
|
||||
/// <summary>When set, the next sign-in throws — how an unreachable server is exercised.</summary>
|
||||
internal Exception? SignInFailure { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri ServerUrl { get; } = new("https://dodossh.example");
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAccountApi Account => this;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISyncApi Sync => this;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IKeyBindingAuthorizer KeyBinding => this;
|
||||
|
||||
/// <inheritdoc />
|
||||
public SyncOptions SyncOptions => SyncOptions.Default;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// Nothing to release; the shell disposes this on lock and on shutdown, and both paths have to be
|
||||
// safe to run more than once.
|
||||
}
|
||||
|
||||
// ---- Identity provider ----
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> AuthorizeKeyBindingAsync(string bindingNonce, CancellationToken cancellationToken) =>
|
||||
Task.FromResult("stub-id-token");
|
||||
|
||||
// ---- Account ----
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MeResponse> GetMeAsync(CancellationToken cancellationToken) =>
|
||||
Task.FromResult(new MeResponse(
|
||||
UserId,
|
||||
"https://idp.example/realms/dodossh",
|
||||
"alice",
|
||||
"alice@example.com",
|
||||
"Alice Example",
|
||||
EnrollmentRequired: !IsEnrolled,
|
||||
KeyGeneration: statement?.KeyGeneration,
|
||||
WrappedPrivateKey: wrappedPrivateKey,
|
||||
KdfParameters: kdfParameters,
|
||||
Vaults: personalVault is null ? [] : [personalVault]));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<EnrollmentResponse> EnrollAsync(
|
||||
EnrollmentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EnrollmentCount++;
|
||||
|
||||
statement = request.Statement;
|
||||
wrappedPrivateKey = request.WrappedPrivateKey;
|
||||
kdfParameters = request.KdfParameters;
|
||||
|
||||
personalVault = new VaultSummary(
|
||||
request.PersonalVault.VaultId,
|
||||
request.PersonalVault.Name,
|
||||
IsPersonal: true,
|
||||
TeamId: null,
|
||||
KeyGeneration: 1,
|
||||
Permissions: 31,
|
||||
request.PersonalVault.WrappedVaultKey,
|
||||
RekeyRequired: false);
|
||||
|
||||
return Task.FromResult(new EnrollmentResponse(
|
||||
UserId,
|
||||
KeyGeneration: 1,
|
||||
Fingerprint: new byte[32],
|
||||
request.PersonalVault.VaultId,
|
||||
DeviceId: null,
|
||||
KeyLogSequence: 1));
|
||||
}
|
||||
|
||||
// ---- Sync ----
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SyncPullResponse> SyncPullAsync(
|
||||
Guid vaultId,
|
||||
SyncPullRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var after = request.Cursor is null
|
||||
? 0
|
||||
: long.Parse(request.Cursor.AsSpan("app-v1:".Length), provider: null);
|
||||
|
||||
var page = log.Where(change => change.ChangeSequence > after).ToList();
|
||||
var next = page.Count > 0 ? page[^1].ChangeSequence : after;
|
||||
|
||||
return Task.FromResult(new SyncPullResponse(
|
||||
page,
|
||||
$"app-v1:{next}",
|
||||
HasMore: false,
|
||||
ServerTime: DateTimeOffset.FromUnixTimeSeconds(1_750_000_000),
|
||||
CurrentKeyGeneration: 1));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SyncPushResponse> SyncPushAsync(
|
||||
Guid vaultId,
|
||||
SyncPushRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PushCount++;
|
||||
|
||||
var results = new List<SyncPushResult>(request.Operations.Count);
|
||||
|
||||
foreach (var operation in request.Operations)
|
||||
{
|
||||
results.Add(Apply(operation));
|
||||
}
|
||||
|
||||
return Task.FromResult(new SyncPushResponse(results, $"app-v1:{log.Count}"));
|
||||
}
|
||||
|
||||
private SyncPushResult Apply(SyncPushOperation operation)
|
||||
{
|
||||
rows.TryGetValue(operation.EntityId, out var existing);
|
||||
|
||||
var current = existing?.Operation == SyncOperation.Delete ? null : existing;
|
||||
|
||||
if (operation.ExpectedVersion != current?.Version)
|
||||
{
|
||||
return new SyncPushResult(
|
||||
operation.OperationId,
|
||||
SyncOperationStatus.Conflict,
|
||||
current?.Version,
|
||||
current?.ChangeSequence,
|
||||
current,
|
||||
null);
|
||||
}
|
||||
|
||||
var sequence = log.Count + 1;
|
||||
|
||||
var change = new SyncChange(
|
||||
operation.EntityType,
|
||||
operation.EntityId,
|
||||
operation.Operation,
|
||||
Version: (current?.Version ?? 0) + 1,
|
||||
ChangeSequence: sequence,
|
||||
Payload: operation.Operation == SyncOperation.Delete ? null : operation.Payload,
|
||||
PlaintextFields: operation.Operation == SyncOperation.Delete
|
||||
? null
|
||||
: operation.PlaintextFields,
|
||||
UpdatedAt: DateTimeOffset.FromUnixTimeSeconds(1_750_000_000 + sequence));
|
||||
|
||||
rows[operation.EntityId] = change;
|
||||
log.Add(change);
|
||||
|
||||
return new SyncPushResult(
|
||||
operation.OperationId,
|
||||
SyncOperationStatus.Applied,
|
||||
change.Version,
|
||||
sequence,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
using DodoSSH.Client.App.ViewModels;
|
||||
using DodoSSH.Client.Session;
|
||||
using DodoSSH.Client.Ssh;
|
||||
using DodoSSH.Client.Storage;
|
||||
using DodoSSH.Client.Terminal;
|
||||
using DodoSSH.Crypto;
|
||||
|
||||
namespace DodoSSH.Client.App.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The whole path a user walks: sign in, enroll, keep the recovery code, unlock, add a host, sync.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Runs with no Avalonia, no browser and no identity provider, because the view models are plain
|
||||
/// observable objects and sign-in is a delegate. What that buys is that the states most likely to be got
|
||||
/// wrong — the one that must not be skipped, and the one that has to work offline — are checked by a test
|
||||
/// rather than by remembering to click through them.
|
||||
/// </remarks>
|
||||
public sealed class ShellFlowTests : IAsyncLifetime
|
||||
{
|
||||
private const string Passphrase = "a sufficiently long passphrase";
|
||||
|
||||
/// <remarks>
|
||||
/// Far below the shipped profile, for the same reason as everywhere else: these tests are about the
|
||||
/// state machine, not about how expensive the passphrase is to attack.
|
||||
/// </remarks>
|
||||
private static readonly Argon2Profile CheapProfile =
|
||||
Argon2Profile.FromStoredParameters(memoryKibibytes: 8 * 1024, passes: 1, parallelism: 1);
|
||||
|
||||
private readonly FakeVaultServer server = new();
|
||||
|
||||
private string directory = null!;
|
||||
private ClientPaths paths = null!;
|
||||
private ClientCacheFactory caches = null!;
|
||||
private TerminalWorkspace workspace = null!;
|
||||
private MainWindowViewModel shell = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask InitializeAsync()
|
||||
{
|
||||
// A real directory and a real SQLite file, because the production path is what StartAsync runs and
|
||||
// an in-memory database would skip the migration that creates the file.
|
||||
directory = Path.Combine(Path.GetTempPath(), $"dodossh-shell-{Guid.CreateVersion7():N}");
|
||||
paths = new ClientPaths(directory);
|
||||
caches = ClientCacheFactory.ForFile(paths.CacheFile);
|
||||
|
||||
var knownHosts = new InMemoryKnownHostStore();
|
||||
|
||||
// In-memory assets rather than the application's Avalonia-resource provider, which reads the
|
||||
// resource system at construction and needs an initialised toolkit. This is what
|
||||
// ITerminalAssetProvider is for; nothing in this suite renders anything.
|
||||
workspace = new TerminalWorkspace(
|
||||
new InMemoryTerminalAssetProvider(
|
||||
new Dictionary<string, TerminalAsset>(StringComparer.Ordinal)
|
||||
{
|
||||
["/terminal"] = new("text/html; charset=utf-8", "<!doctype html>"u8.ToArray()),
|
||||
}),
|
||||
new SshNetConnectionFactory(knownHosts),
|
||||
TimeProvider.System);
|
||||
|
||||
shell = new MainWindowViewModel(
|
||||
paths,
|
||||
caches,
|
||||
workspace,
|
||||
knownHosts,
|
||||
SignInAsync,
|
||||
TimeProvider.System,
|
||||
CheapProfile);
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await shell.DisposeAsync();
|
||||
await workspace.DisposeAsync();
|
||||
caches.Dispose();
|
||||
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AFreshMachine_AsksForAServer()
|
||||
{
|
||||
await shell.StartAsync(Token);
|
||||
|
||||
shell.State.ShouldBe(ShellState.NeedsServer);
|
||||
shell.IsNeedingServer.ShouldBeTrue();
|
||||
shell.IsOnline.ShouldBeFalse();
|
||||
|
||||
// The migration ran, so the file exists before anyone has signed in to anything.
|
||||
File.Exists(paths.CacheFile).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SigningInToAnUnenrolledAccount_AsksForAPassphrase()
|
||||
{
|
||||
await shell.StartAsync(Token);
|
||||
await shell.SignInCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.NeedsEnrollment);
|
||||
shell.IsOnline.ShouldBeTrue();
|
||||
shell.AccountName.ShouldBe("Alice Example");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnUnreachableServer_ReportsAndStaysPut()
|
||||
{
|
||||
server.SignInFailure = new HttpRequestException("No such host is known.");
|
||||
|
||||
await shell.StartAsync(Token);
|
||||
await shell.SignInCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.NeedsServer);
|
||||
shell.StatusMessage.ShouldContain("No such host");
|
||||
shell.IsBusy.ShouldBeFalse("a failed command must not leave the window disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnInvalidServerUrl_IsRejectedWithoutTouchingTheNetwork()
|
||||
{
|
||||
await shell.StartAsync(Token);
|
||||
|
||||
shell.ServerUrl = "not a url";
|
||||
await shell.SignInCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.NeedsServer);
|
||||
shell.IsOnline.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("short", "short")]
|
||||
[InlineData("a sufficiently long passphrase", "a different one")]
|
||||
public async Task AWeakOrMismatchedPassphrase_DoesNotEnroll(string entered, string confirmation)
|
||||
{
|
||||
await SignedInAsync();
|
||||
|
||||
shell.Passphrase = entered;
|
||||
shell.ConfirmPassphrase = confirmation;
|
||||
|
||||
await shell.EnrollCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.NeedsEnrollment);
|
||||
server.EnrollmentCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TheRecoveryCodeScreen_CannotBeSkipped()
|
||||
{
|
||||
// The only moment the code exists. Losing it along with the passphrase means the vault is
|
||||
// unrecoverable and there is no server-side reset, so this is the one screen that has to insist.
|
||||
await EnrolledAsync();
|
||||
|
||||
shell.State.ShouldBe(ShellState.ShowingRecoveryCode);
|
||||
shell.RecoveryCode.ShouldNotBeNullOrWhiteSpace();
|
||||
|
||||
// Trying to continue without confirming gets nowhere.
|
||||
shell.ConfirmRecoveryCodeCommand.Execute(null);
|
||||
shell.State.ShouldBe(ShellState.ShowingRecoveryCode);
|
||||
shell.RecoveryCode.ShouldNotBeNull();
|
||||
|
||||
shell.RecoveryCodeWrittenDown = true;
|
||||
shell.ConfirmRecoveryCodeCommand.Execute(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.Locked);
|
||||
|
||||
// And it is dropped from memory, not merely hidden. It was never persisted; keeping it in a view
|
||||
// model for the rest of the session would undo that.
|
||||
shell.RecoveryCode.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AWrongPassphrase_KeepsTheVaultLocked()
|
||||
{
|
||||
await ReadyToUnlockAsync();
|
||||
|
||||
shell.Passphrase = "not the passphrase";
|
||||
await shell.UnlockCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.Locked);
|
||||
shell.Vault.ShouldBeNull();
|
||||
shell.StatusMessage.ShouldContain("did not open");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnlockingOpensTheVault()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
shell.State.ShouldBe(ShellState.Unlocked);
|
||||
shell.Vault.ShouldNotBeNull();
|
||||
shell.Vault.VaultName.ShouldBe("Personal");
|
||||
|
||||
// Cleared once used, so it is not sitting in a bound property for the rest of the session.
|
||||
shell.Passphrase.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ARestartUnlocksWithNoNetworkAtAll()
|
||||
{
|
||||
// The property the whole storage layer exists for, from the shell's point of view. The second
|
||||
// shell is given a sign-in delegate that fails if called.
|
||||
await EnrolledAndConfirmedAsync();
|
||||
await shell.LockCommand.ExecuteAsync(null);
|
||||
|
||||
var offline = new MainWindowViewModel(
|
||||
paths,
|
||||
caches,
|
||||
workspace,
|
||||
new InMemoryKnownHostStore(),
|
||||
(_, _) => throw new InvalidOperationException("The shell went to the network to unlock."),
|
||||
TimeProvider.System,
|
||||
CheapProfile);
|
||||
|
||||
await using var _ = offline.ConfigureAwait(false);
|
||||
|
||||
await offline.StartAsync(Token);
|
||||
|
||||
offline.State.ShouldBe(ShellState.Locked);
|
||||
offline.AccountName.ShouldBe("Alice Example");
|
||||
offline.IsOnline.ShouldBeFalse();
|
||||
|
||||
offline.Passphrase = Passphrase;
|
||||
await offline.UnlockCommand.ExecuteAsync(null);
|
||||
|
||||
offline.State.ShouldBe(ShellState.Unlocked);
|
||||
offline.Vault.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddingAHost_ShowsItImmediatelyAndQueuesIt()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
vault.NewHostCommand.Execute(null);
|
||||
vault.IsEditing.ShouldBeTrue();
|
||||
|
||||
vault.EditorLabel = "prod-db";
|
||||
vault.EditorHostname = "db.internal";
|
||||
vault.EditorUsername = "deploy";
|
||||
vault.EditorPort = 2222;
|
||||
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
vault.IsEditing.ShouldBeFalse();
|
||||
|
||||
var row = vault.Hosts.ShouldHaveSingleItem();
|
||||
row.Label.ShouldBe("prod-db");
|
||||
row.Address.ShouldBe("deploy@db.internal:2222");
|
||||
row.HasUnsyncedChanges.ShouldBeTrue();
|
||||
row.Badge.ShouldBe("not synced");
|
||||
|
||||
vault.PendingChanges.ShouldBe(1);
|
||||
server.LiveRowCount.ShouldBe(0, "nothing should have been pushed yet");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnInvalidHost_IsRefusedWithAReason()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
vault.NewHostCommand.Execute(null);
|
||||
vault.EditorLabel = " ";
|
||||
vault.EditorHostname = "db.internal";
|
||||
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
vault.IsEditing.ShouldBeTrue("the editor should stay open so the user can fix it");
|
||||
vault.Hosts.ShouldBeEmpty();
|
||||
vault.Status.ShouldContain("needs a name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncingSendsTheQueueAndClearsIt()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await vault.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
server.LiveRowCount.ShouldBe(1);
|
||||
vault.PendingChanges.ShouldBe(0);
|
||||
vault.Hosts.ShouldHaveSingleItem().HasUnsyncedChanges.ShouldBeFalse();
|
||||
vault.Status.ShouldContain("Synchronised");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditingAHostRoundTripsThroughTheEditor()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await vault.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
vault.SelectedHost = vault.Hosts[0];
|
||||
vault.EditSelectedHostCommand.Execute(null);
|
||||
|
||||
vault.EditorLabel.ShouldBe("prod-db");
|
||||
vault.EditorHostname.ShouldBe("db.internal");
|
||||
|
||||
vault.EditorNotes = "rotate quarterly";
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
await vault.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
vault.Hosts.ShouldHaveSingleItem().Host.Notes.ShouldBe("rotate quarterly");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeletingAHostRemovesItLocallyBeforeTheServerAgrees()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
await vault.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
vault.SelectedHost = vault.Hosts[0];
|
||||
await vault.DeleteHostCommand.ExecuteAsync(null);
|
||||
|
||||
vault.Hosts.ShouldBeEmpty();
|
||||
server.LiveRowCount.ShouldBe(1, "the tombstone has not been pushed yet");
|
||||
|
||||
await vault.SyncCommand.ExecuteAsync(null);
|
||||
server.LiveRowCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncingWhileOffline_QueuesRatherThanFailing()
|
||||
{
|
||||
await EnrolledAndConfirmedAsync();
|
||||
await shell.LockCommand.ExecuteAsync(null);
|
||||
|
||||
// Locking does not drop the connection, so take a fresh shell that never signed in.
|
||||
var offline = new MainWindowViewModel(
|
||||
paths,
|
||||
caches,
|
||||
workspace,
|
||||
new InMemoryKnownHostStore(),
|
||||
(_, _) => throw new InvalidOperationException("unreachable"),
|
||||
TimeProvider.System,
|
||||
CheapProfile);
|
||||
|
||||
await using var _ = offline.ConfigureAwait(false);
|
||||
|
||||
await offline.StartAsync(Token);
|
||||
offline.Passphrase = Passphrase;
|
||||
await offline.UnlockCommand.ExecuteAsync(null);
|
||||
|
||||
var vault = offline.Vault!;
|
||||
await AddHostAsync(vault, "offline-host");
|
||||
|
||||
await vault.SyncCommand.ExecuteAsync(null);
|
||||
|
||||
vault.Status.ShouldContain("Offline");
|
||||
vault.PendingChanges.ShouldBe(1, "the change is kept, not discarded");
|
||||
server.PushCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockingForgetsTheVault()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
await shell.LockCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.Locked);
|
||||
shell.Vault.ShouldBeNull();
|
||||
|
||||
// And unlocking again works, so locking released rather than corrupted anything.
|
||||
shell.Passphrase = Passphrase;
|
||||
await shell.UnlockCommand.ExecuteAsync(null);
|
||||
shell.State.ShouldBe(ShellState.Unlocked);
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
||||
|
||||
private Task<IVaultServer> SignInAsync(Uri serverUrl, CancellationToken cancellationToken) =>
|
||||
server.SignInFailure is { } failure
|
||||
? Task.FromException<IVaultServer>(failure)
|
||||
: Task.FromResult<IVaultServer>(server);
|
||||
|
||||
private async Task SignedInAsync()
|
||||
{
|
||||
await shell.StartAsync(Token);
|
||||
await shell.SignInCommand.ExecuteAsync(null);
|
||||
shell.State.ShouldBe(ShellState.NeedsEnrollment);
|
||||
}
|
||||
|
||||
private async Task EnrolledAsync()
|
||||
{
|
||||
await SignedInAsync();
|
||||
|
||||
shell.Passphrase = Passphrase;
|
||||
shell.ConfirmPassphrase = Passphrase;
|
||||
|
||||
await shell.EnrollCommand.ExecuteAsync(null);
|
||||
}
|
||||
|
||||
private async Task EnrolledAndConfirmedAsync()
|
||||
{
|
||||
await EnrolledAsync();
|
||||
|
||||
shell.RecoveryCodeWrittenDown = true;
|
||||
shell.ConfirmRecoveryCodeCommand.Execute(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.Locked);
|
||||
}
|
||||
|
||||
private Task ReadyToUnlockAsync() => EnrolledAndConfirmedAsync();
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
await EnrolledAndConfirmedAsync();
|
||||
|
||||
shell.Passphrase = Passphrase;
|
||||
await shell.UnlockCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.Unlocked, shell.StatusMessage);
|
||||
}
|
||||
|
||||
private static async Task AddHostAsync(VaultViewModel vault, string label)
|
||||
{
|
||||
vault.NewHostCommand.Execute(null);
|
||||
vault.EditorLabel = label;
|
||||
vault.EditorHostname = "db.internal";
|
||||
vault.EditorUsername = "deploy";
|
||||
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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]"
|
||||
}
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.27548.20260419",
|
||||
"contentHash": "l17nI3XVDN3oMnpjf2pnmJg0YTwK4m6NLsn/itAjDMdObTFxN77D5F1M9sRMSfViSY3KKcse1ROczwgoWLJsnA=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "89mrS7dSYtisJrjQufCOomHeAynlVVKZ+dq4leKtzHXXKVoWsE0Nb2ymiNYPMlirwzwpemflGj+K34opwyLeJQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0",
|
||||
"Tmds.DBus.Protocol": "0.94.1"
|
||||
}
|
||||
},
|
||||
"Avalonia.FreeDesktop.AtSpi": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "WWahMjzKtDl2PGHa8mS6NHIVMT+JNKVIeT5xMLp9SBTMvjHNpEbXTX3PNbbIQ7hRMSETAJ/PAnvgOzatGktEKQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.HarfBuzz": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "uWPa/kg+fmqhrUR5GzFC2ZL2MPszxcEy14hTSwu3VhjwTnaVarUozoPmggQjZG4A4s6w2bcGepsYYaCM/eOoMA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0",
|
||||
"HarfBuzzSharp": "8.3.1.3",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "mfNMtGP7rEVWSZoii0l40mlNhgNR6ZISTvHP7OAMn5YQHiK66EjfN26SL/kLnz1buy2VtMIiGVBbIttL8FYCZg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "p6OKt6O7vOub4TS2pAjaeW0Y13oxrPs4uixeVZpJByiSQKKk+LyApN5yRy2JerpfTMtI86Y5pNwugyKTHZJnAw=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "K63pwExQkcjVbsYJOiOq0hYAO4G5d7T42yK8MGNrvwBKv/bJVlV14jGvV4wXcsuYAU8IWlOHgqq5sMUiDfj4vw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0",
|
||||
"HarfBuzzSharp": "8.3.1.3",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.3",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.3",
|
||||
"SkiaSharp": "3.119.4",
|
||||
"SkiaSharp.NativeAssets.Linux": "3.119.4",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "3.119.4"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "D0xxPtFeOK8cKK991ul92rlFtDO3II0E44dHM0ix4/8LVc9+1LaMdbf1eMnp1RclFwX2bt7HPhbySnrG5uArxA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.27548.20260419"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "6+YHVGf44ictmGj88diMCw9pC9tiwnMlUgosDi0VDmyUQFuy/mJIa4J0rZu6G+UMbjGuyLDQLmtvU4tTOhLMPg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0",
|
||||
"Avalonia.FreeDesktop": "12.1.0",
|
||||
"Avalonia.FreeDesktop.AtSpi": "12.1.0",
|
||||
"Avalonia.Skia": "12.1.0"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.3",
|
||||
"contentHash": "NGZ2+ZVNPM+NdHB/asW0/ykWngyHWwcqjrbN2nDeH1B/aptPGlCUl8wkQ2cSJxw5fdWgdmIPmNuTPWpLwNVXWg==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.3",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.3"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.3",
|
||||
"contentHash": "RI6A1LgmooU30+4QIyFt5rmBCzP0VzTR+587IJSGvYIsHHWlahFufihYxtraLfsIhW7I8dn6+xX+DZGygOPKWQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.3",
|
||||
"contentHash": "KPTq0xnslkI6nAo0jh3ptcQPJvZZr7MWYXa2jUe4SnHc9q+JlHElmNXp0sfFoiTgoCX7WOYpYsurypuH9Gehxw=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.3",
|
||||
"contentHash": "w2QfdNm9Uz/sUa0B5D+OnVQhyq3G/fBq6ibQMdWBlQqqwh0g0/5j3RFvYqZAmRZ5+RzvjVe8o8SFFnWYUSkuxA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.3",
|
||||
"contentHash": "bx8CE8Js+XGX8PUxAHCBDEORt5aaBYtMN4Hr9QFs57Xithh6yjUyYqksizH6eRDhJkwsGI+SXWmPmMm8lZC9Pw=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.6",
|
||||
"contentHash": "NdNWGDiZ6eS/Mf/9+QHR91cj1K7Hy+PX9yrHI/zM7xFYuj9IWT2uxtB6sCHjrnxAeLV9fut1R6zHDUGKX6f9lQ=="
|
||||
},
|
||||
"Microsoft.ApplicationInsights": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
|
||||
},
|
||||
"Microsoft.Bcl.AsyncInterfaces": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "TPCs0ldm7AWqcKmp6f/Xr+14sat7hx4rHfRlS4RgCURBH2thEWbAKEyX7cCWr63zVJVOJIJZTg2cBiUXa8ys6g==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "YbVWMIouzwTKBiLms8boa7xeRT88wI14R1msv3XExFk9n0/sa8nU7MwDa1CKtfLGMJs7O7QWuS9/xhcQ72AD2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "10.0.10",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "10.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.10",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.DependencyModel": "10.0.10",
|
||||
"Microsoft.Extensions.Logging": "10.0.10",
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Caching.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Caching.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.Options": "10.0.10",
|
||||
"Microsoft.Extensions.Primitives": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Primitives": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA=="
|
||||
},
|
||||
"Microsoft.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection": "10.0.10",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.Options": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.Primitives": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Primitives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ=="
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.119.4",
|
||||
"contentHash": "53NOSUZ1Us+91Sm0uCkIivh/k7jOowRErZT2sIWwPFN9mLUvdxnE6rS4sWo4255+Rd2MWUSF+j0NMZHD6Cke+Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "3.119.4",
|
||||
"SkiaSharp.NativeAssets.macOS": "3.119.4"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.119.4",
|
||||
"contentHash": "UAyVzbqNfZsZbKbzj68zXLyUyF/SbTKmzTfOO6qDu++dtIUMMTzPBe8oOuzU/DiewpfKoUUlOSsJmqWc6blxBw=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.119.4",
|
||||
"contentHash": "fgBOWEqbY012x7gMfJU4ezgz6dfhJb30Z6YdW35h85Zoe39+a8YNbAAwL29ihPfWoppg5AjvyKNzD1oCvlqWwA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.119.4",
|
||||
"contentHash": "S1HOxtBbD4bYDtA2e9WH5TX+lxqRrTPvKjrjttRhxnHNNu7YY8VFo/LeCP7tNqoTA6PV+8vsvNbmRUEC2ip8RQ=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.119.4",
|
||||
"contentHash": "XOpbx/4CReO2wYsq2s6rbvdauc6dntG4Zv499sHGTJ87bwZaFXszFkwql3+FIZMc8kUPeaj3Mx2ezIJmo8a1Kg=="
|
||||
},
|
||||
"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.Management": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.1",
|
||||
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
|
||||
"dependencies": {
|
||||
"System.CodeDom": "6.0.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.94.1",
|
||||
"contentHash": "11YMr7FnAbL83bQmVxlhbIKHvSLxjO81D12Ej0QMSGXMDTxNA9MTOa4MQxx43nv5el/efuPHwzyrj6a5ha2gug=="
|
||||
},
|
||||
"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]"
|
||||
}
|
||||
},
|
||||
"dodossh.client.api": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"DodoSSH.Client.Auth": "[1.0.0, )",
|
||||
"DodoSSH.Contracts": "[1.0.0, )",
|
||||
"DodoSSH.Crypto": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"dodossh.client.app": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[12.1.0, )",
|
||||
"Avalonia.Controls.WebView": "[12.0.1, )",
|
||||
"Avalonia.Desktop": "[12.1.0, )",
|
||||
"Avalonia.Fonts.Inter": "[12.1.0, )",
|
||||
"Avalonia.Themes.Fluent": "[12.1.0, )",
|
||||
"CommunityToolkit.Mvvm": "[8.4.2, )",
|
||||
"DodoSSH.Client.Session": "[1.0.0, )",
|
||||
"DodoSSH.Client.Ssh": "[1.0.0, )",
|
||||
"DodoSSH.Client.Terminal": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"dodossh.client.auth": {
|
||||
"type": "Project"
|
||||
},
|
||||
"dodossh.client.domain": {
|
||||
"type": "Project"
|
||||
},
|
||||
"dodossh.client.session": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"DodoSSH.Client.Api": "[1.0.0, )",
|
||||
"DodoSSH.Client.Auth": "[1.0.0, )",
|
||||
"DodoSSH.Client.Domain": "[1.0.0, )",
|
||||
"DodoSSH.Client.Storage": "[1.0.0, )",
|
||||
"DodoSSH.Client.Sync": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"dodossh.client.ssh": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SSH.NET": "[2025.1.0, )"
|
||||
}
|
||||
},
|
||||
"dodossh.client.storage": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"DodoSSH.Contracts": "[1.0.0, )",
|
||||
"DodoSSH.Crypto": "[1.0.0, )",
|
||||
"EFCore.NamingConventions": "[10.0.1, )",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": "[10.0.10, )"
|
||||
}
|
||||
},
|
||||
"dodossh.client.sync": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"DodoSSH.Client.Api": "[1.0.0, )",
|
||||
"DodoSSH.Client.Domain": "[1.0.0, )",
|
||||
"DodoSSH.Client.Storage": "[1.0.0, )",
|
||||
"DodoSSH.Contracts": "[1.0.0, )",
|
||||
"DodoSSH.Crypto": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"dodossh.client.terminal": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"DodoSSH.Client.Ssh": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"dodossh.contracts": {
|
||||
"type": "Project"
|
||||
},
|
||||
"dodossh.crypto": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"NSec.Cryptography": "[26.4.0, )"
|
||||
}
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[12.1.0, )",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "an4ugAy2q6GTdaFl635V8W/LKrWNL+mnSFUprbgyf8m1Zzf9WgoGaF5ajGv5i4gXefMZrzsUPV1QU+kKrgwCWg==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "12.1.0",
|
||||
"MicroCom.Runtime": "0.11.6"
|
||||
}
|
||||
},
|
||||
"Avalonia.Controls.WebView": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[12.0.1, )",
|
||||
"resolved": "12.0.1",
|
||||
"contentHash": "GrCIpIIBL7ueFDsNu3lyYc1mgO3QGGl1c1MCK8YAgjaNZwF9PV5PF2UB3lm1uuqj/MWOKNhemLwcSDLyYv0JjQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.0.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[12.1.0, )",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "mxhz50At61IBQbB/bCo5JGp53rPi3GerGO9mFo/v93uBHa4J3cz3NSSqnVWSyQXKbPCwQhrogfEFWbKBgecy1w==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0",
|
||||
"Avalonia.HarfBuzz": "12.1.0",
|
||||
"Avalonia.Native": "12.1.0",
|
||||
"Avalonia.Skia": "12.1.0",
|
||||
"Avalonia.Win32": "12.1.0",
|
||||
"Avalonia.X11": "12.1.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[12.1.0, )",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "2mK5Rv6aMWgXfQ2JZOq1Wo2bTNAfiidg2GO4b3MgLRN89ezfvfsSfp4P5Pl4ssRcWWwdV0jGzIw8n0xc9B26VA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[12.1.0, )",
|
||||
"resolved": "12.1.0",
|
||||
"contentHash": "MVi5L9HymnNm+gP2aNXNcyrP2iKGJWFubQ5Bv8/Przflxca1aIT9QBpTUV6c3olA9rLYFY7MJRR/C/BZaUhemQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "12.1.0"
|
||||
}
|
||||
},
|
||||
"BouncyCastle.Cryptography": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.6.2, )",
|
||||
"resolved": "2.6.2",
|
||||
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
|
||||
},
|
||||
"CommunityToolkit.Mvvm": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[8.4.2, )",
|
||||
"resolved": "8.4.2",
|
||||
"contentHash": "WadCzGEc2U+3e20avRLng4qNtt4zoOGWrdUISqJWrHe3/FSnrYjuM5Sb4yQb09LhkBXrrI4Zt3dLKgRMbItsrg=="
|
||||
},
|
||||
"EFCore.NamingConventions": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[10.0.1, )",
|
||||
"resolved": "10.0.1",
|
||||
"contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)",
|
||||
"Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1"
|
||||
}
|
||||
},
|
||||
"libsodium": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.0.22, )",
|
||||
"resolved": "1.0.22",
|
||||
"contentHash": "KPD9SloJFclrsjnhABu7dzWrcyYkwPbvx5l1gRSPAX/0n+OBtSiVCKtGFv4n+ecWUHU0tCG9LSSwoZZx673zBQ=="
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.10",
|
||||
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.10",
|
||||
"Microsoft.Extensions.Logging": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Relational": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore": "10.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.10",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.Logging": "10.0.10"
|
||||
}
|
||||
},
|
||||
"Microsoft.EntityFrameworkCore.Sqlite": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "kzg9MuQNJvZQxAU+piSkEzc7/1tpW6n1nVSGGMObu2GgxLK8Nf+6fvZundaznTZ+O2KhfPZ8HFNCzMH3PWDUmA==",
|
||||
"dependencies": {
|
||||
"Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.10",
|
||||
"Microsoft.Extensions.Caching.Memory": "10.0.10",
|
||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
|
||||
"Microsoft.Extensions.DependencyModel": "10.0.10",
|
||||
"Microsoft.Extensions.Logging": "10.0.10",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"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)"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.1.12, )",
|
||||
"resolved": "2.1.12",
|
||||
"contentHash": "mAgscpQMLw5/nfA1Q5oJVAT29yROUo1ifZGbbTpx/lwZpSxMUGoYbKfmvdm8oXER+RzxqBmmQzeBEVKfeHv2nw==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.12",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.12"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.1.12, )",
|
||||
"resolved": "2.1.12",
|
||||
"contentHash": "ETpNw9DY3ckWLgRRAeCHj+GKOuPi61aeczkXhgHexUvqoZBAYg8RYESE2J7O1M7+o6QbdSEZwrw9bfqztUVWXg=="
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.1.12, )",
|
||||
"resolved": "2.1.12",
|
||||
"contentHash": "fWi8Dbknuhgg72fWinIdjXVaqO1hHL4YBBwVLnr7e1c9TAZwJ0QE38j9syW1hwx6HaqEVTwI+O07WPdZn8Rp0w=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.1.12, )",
|
||||
"resolved": "2.1.12",
|
||||
"contentHash": "W3oH4XIfCzFrgUSDKHhN6N+dgzA5YHOR2VxX8GB6Qy7CyrJJgxPEG8NirgYWlPQC5P2jz2knSsexWu4tDUL33g==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.12"
|
||||
}
|
||||
},
|
||||
"SSH.NET": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2025.1.0, )",
|
||||
"resolved": "2025.1.0",
|
||||
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
||||
"dependencies": {
|
||||
"BouncyCastle.Cryptography": "2.6.2",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user