Author SHA1 Message Date
jaap-jan fdea3911c1 Name a quick-connect result's vault, and search on it
ci / build and test (pull_request) Failing after 7s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped
ci / android head (pull_request) Failing after 6s
Ctrl+K reaches every vault the session holds a key for — that is deliberate,
and it is what makes the palette worth opening from anywhere. What it did not
do was say where a result came from. A team and a person who both call a
machine prod-db got two identical rows, and Enter took whichever the ranking
happened to put first: the same three characters, two different machines,
depending on nothing anybody could see.

Nothing new is computed for this. HostRowViewModel.VaultBadge has been filled
in since the host list learned to span vaults, and it is already drawn on the
hosts board, the keychain, the snippets list and the known-hosts pane. The
palette was the one list reaching across every vault that did not print it.

◆ THE VAULT IS THE LAST OF THE FOUR RANKS, AND THAT IS THE POINT. A vault name
is the widest reading of the three the palette had: one word matches every host
in that vault at once, where a name or an address matches one machine. So it
sits behind name-starts-with, name-contains and address — otherwise typing a
machine's name would bury it under everybody else's.

It matches on HasVaultBadge rather than on VaultName, so the search only ever
matches what the row actually shows. A session holding one vault prints no
vault on any row, and matching it there would answer "personal" with the entire
keychain, ranked behind nothing and explained by nothing on screen.

In the row, the right-hand column becomes two lines against the two on the
left, in the same order: what this is above, how it is reached below. With one
vault the badge is empty, the line collapses, and the kind word stays centred
exactly where it was. The name is capped and ellipsised because that column is
Auto-sized — a long vault name would otherwise take its width out of the host
name beside it.

Three tests, one per claim. VaultSharingTests puts platform-gateway in the
personal vault and prod-db in "Platform secrets" and types "platform": both come
back, the gateway first. ShellFlowTests types the personal vault's own name into
a one-vault session and gets nothing. QuickConnectTests is the markup's half —
the shared vault's row draws PLATFORM SECRETS, and a one-vault row draws no
vault at all — with the shared vault put into the session through the layout
suite's own StubTeamServer, as the settings pages' suite does it.

449 App tests and 155 layout tests pass. Both suites were run with
-p:NuGetAudit=false: SSH.NET 2025.1.0 has picked up GHSA-q939-rpr3-3284 and
NU1903 fails restore repo-wide, which predates this branch and is nothing to do
with it. No package or lock file is touched here.
2026-08-14 09:35:44 +02:00
26 changed files with 364 additions and 167 deletions
+3 -13
View File
@@ -88,12 +88,8 @@
-->
<PackageVersion Include="NSec.Cryptography" Version="26.4.0" />
<PackageVersion Include="libsodium" Version="1.0.22" />
<!--
Managed differential oracle for the crypto test suite only. Also what SSH.NET pulls in,
so central transitive pinning makes this pin its floor too: 2.7.0 is what SSH.NET
2026.0.0 asks for, and pinning below that is a downgrade error rather than a preference.
-->
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.7.0" />
<!-- Managed differential oracle for the crypto test suite only. -->
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
</ItemGroup>
<ItemGroup Label="Desktop client">
<!--
@@ -102,14 +98,8 @@
agent forwarding (needs an upstream change; de-scoped from v1) and being handed a
pre-connected Stream — it performs its own socket connect, which is why the relay and
ProxyJump both go through a loopback TCP bridge. See docs/adr/.
2026.0.0 is the fix for GHSA-q939-rpr3-3284, a path traversal in ScpClient.Download's
recursive mode that trusts server-supplied names (everything up to and including
2025.1.0 is affected). Nothing here uses ScpClient — transfers go through SftpClient —
so this was an upgrade on principle rather than an exposure, and the release notes
list no breaking changes over 2025.1.0.
-->
<PackageVersion Include="SSH.NET" Version="2026.0.0" />
<PackageVersion Include="SSH.NET" Version="2025.1.0" />
<!--
The S3 client, for buckets as a remote in the file browser. First-party, Apache-2.0, and
managed only — no native assets — which is the bar this file sets for anything that gets
@@ -984,7 +984,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -1044,9 +1044,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -1140,11 +1140,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -87,12 +87,27 @@
TextTrimming="CharacterEllipsis" />
</StackPanel>
<!--
Two readings stacked against the two on the left, and in the same order: what this row
IS above, how it is reached below.
◆ THE VAULT, and only when there is more than one to be in — HostRowViewModel.VaultBadge
is empty otherwise, which collapses the line and leaves the kind word centred as it was.
The palette searches every readable vault at once, so without it two hosts a team and a
person both call "prod-db" are two identical rows, and Enter takes whichever the ranking
happened to put first. Trimmed against a cap because the column is Auto-sized: a long
vault name would otherwise take its width out of the host name beside it.
credential / key / password — never the mock's SSH/SFTP kind column. Every palette
connect here is SSH, so printing that word would be a constant dressed up as a reading;
see hosts-v5-design-spec.md's deviations and HostRowViewModel.Authentication.
-->
<TextBlock Grid.Column="2" Classes="mono qcrow-subtext" Text="{Binding Authentication}"
FontSize="11" Margin="10,0,0,0" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" Spacing="1" Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock Classes="mono qcrow-subtext" Text="{Binding VaultBadge}" FontSize="10"
HorizontalAlignment="Right" MaxWidth="150"
TextTrimming="CharacterEllipsis" IsVisible="{Binding HasVaultBadge}" />
<TextBlock Classes="mono qcrow-subtext" Text="{Binding Authentication}"
FontSize="11" HorizontalAlignment="Right" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
+8 -8
View File
@@ -405,7 +405,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -465,9 +465,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -567,11 +567,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -55,14 +55,14 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -81,11 +81,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -148,7 +148,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -187,9 +187,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -289,11 +289,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -2141,8 +2141,9 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
/// <remarks>
/// Ranked rather than merely filtered: a host whose name starts with what was typed comes before one
/// that merely contains it, and both come before a match found only in the address. Typing three
/// characters of a name people use daily should not put that host third.
/// that merely contains it, both come before a match found only in the address, and all three come
/// before one found only in the vault's name. Typing three characters of a name people use daily should
/// not put that host third. See <see cref="Rank"/>.
/// </remarks>
private void RefreshSearchResults()
{
@@ -2175,6 +2176,21 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
OnPropertyChanged(nameof(HasSearchResults));
}
/// <summary>Where one host places against what was typed, or <see cref="int.MaxValue"/> for no match.</summary>
/// <remarks>
/// <para>
/// <b>The vault comes last, and only when the row prints it.</b> Its name is the widest of the three
/// readings — one word can match every host in a team's vault at once — so a host whose own name or
/// address answers the query has to come first, or typing a machine's name would bury it under everybody
/// else's.
/// </para>
/// <para>
/// <see cref="HostRowViewModel.HasVaultBadge"/> rather than the name itself, so this matches exactly what
/// the row shows: a session holding one vault prints no vault on any row, and matching a name nothing
/// displays would answer a query with a list whose reason for existing is invisible — type "personal"
/// there and every host comes back for no reason anybody can see.
/// </para>
/// </remarks>
private static int Rank(HostRowViewModel host, string query)
{
if (host.Label.StartsWith(query, StringComparison.CurrentCultureIgnoreCase))
@@ -2187,8 +2203,14 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
return 1;
}
return host.Address.Contains(query, StringComparison.CurrentCultureIgnoreCase)
? 2
if (host.Address.Contains(query, StringComparison.CurrentCultureIgnoreCase))
{
return 2;
}
return host.HasVaultBadge
&& host.VaultName.Contains(query, StringComparison.CurrentCultureIgnoreCase)
? 3
: int.MaxValue;
}
+8 -8
View File
@@ -207,7 +207,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -267,9 +267,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -369,11 +369,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -304,11 +304,10 @@ internal sealed class SshNetConnection(SshClient client, HostKeyPresentation hos
/// Read at construction rather than lazily: by the time an <see cref="SshNetConnection"/> exists,
/// <see cref="SshNetConnectionFactory.ConnectAsync"/> has already awaited <c>client.ConnectAsync</c>, so
/// <c>ConnectionInfo</c> is already populated and there is no earlier moment reading it would race. SSH.NET
/// types the property as nullable all the same — it is null before the key exchange completes — so the
/// coalesce is here to satisfy that annotation, not because this call site expects to see it. Empty is the
/// value this codebase already uses for a cipher that could not be read; see <c>MainWindowViewModel.NullIfEmpty</c>.
/// types the property as a non-nullable <c>string</c>, so this reads straight through rather than coalescing
/// a null that the library's own contract says cannot occur.
/// </remarks>
public string Cipher { get; } = client.ConnectionInfo.CurrentServerEncryption ?? string.Empty;
public string Cipher { get; } = client.ConnectionInfo.CurrentServerEncryption;
/// <inheritdoc />
public Task<ISshShellSession> OpenShellAsync(TerminalSize size, CancellationToken cancellationToken)
+2 -3
View File
@@ -24,10 +24,9 @@ internal sealed class SshNetSftpSession(SftpClient client, HostKeyPresentation h
/// <remarks>
/// Read at construction, the same way and for the same reason as <c>SshNetConnection.Cipher</c>: this type
/// is only ever built after <c>SshNetConnectionFactory.OpenSftpAsync</c> has awaited <c>client.ConnectAsync</c>,
/// so <c>ConnectionInfo</c> is already populated by the time there is a session to read it from. The coalesce
/// is there for SSH.NET's nullable annotation, which covers the pre-key-exchange window this never sees.
/// so <c>ConnectionInfo</c> is already populated by the time there is a session to read it from.
/// </remarks>
public string Cipher { get; } = client.ConnectionInfo.CurrentServerEncryption ?? string.Empty;
public string Cipher { get; } = client.ConnectionInfo.CurrentServerEncryption;
/// <inheritdoc />
public string HomeDirectory { get; } = homeDirectory;
+7 -7
View File
@@ -31,11 +31,11 @@
},
"SSH.NET": {
"type": "Direct",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
},
@@ -54,9 +54,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -37,14 +37,14 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -63,11 +63,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -37,14 +37,14 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -63,11 +63,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
+7 -7
View File
@@ -1744,9 +1744,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -1847,11 +1847,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
},
@@ -236,6 +236,51 @@ public sealed class QuickConnectTests : IAsyncLifetime
});
}
/// <remarks>
/// The palette searches every vault the session holds a key for, so a row has to say which one it came
/// out of. Two machines a team and a person both call <c>prod-db</c> are otherwise two identical rows,
/// and Enter takes whichever the ranking happened to put first.
/// <para>
/// Typed into rather than read off the unfiltered list, because the shared vault's host sorts last — the
/// active vault's rows come first — and the list virtualises, so the row this is about might never be
/// realised. Narrowing to it also proves the search reaches past the active vault at all.
/// </para>
/// </remarks>
[Fact]
public async Task AResultSaysWhichVaultItCameOutOf()
{
await SeedSharedHostAsync();
await OnThePaletteAsync((palette, window) =>
{
shell.SearchText = "prod-db";
Relayout(window);
var found = shell.SearchResults.ShouldHaveSingleItem();
found.VaultId.ShouldNotBe(session.ActiveVaultId, "the palette reaches past the active vault");
VisibleTexts(RowFor(palette, found)).ShouldContain("PLATFORM SECRETS", StringComparer.Ordinal);
});
}
/// <remarks>
/// The other half of the rule, and the reason the name is a badge rather than a column: a vault named on
/// every row of a session that has only one is the same fact repeated, which is noise rather than a
/// reading. <c>HostRowViewModel.VaultBadge</c> is empty there, and an empty line has to collapse rather
/// than leave a gap above the kind word.
/// </remarks>
[Fact]
public async Task AResultNamesNoVaultWhenThereIsOnlyOneToBeIn()
{
await OnThePaletteAsync((palette, _) =>
{
var first = shell.SearchResults[0];
first.HasVaultBadge.ShouldBeFalse("this fixture's session holds the personal vault alone");
VisibleTexts(RowFor(palette, first)).ShouldNotContain("PERSONAL", StringComparer.Ordinal);
});
}
/// <remarks>
/// The palette is a box somebody is expected to start typing into, and for a while it was not: the window
/// focused it from the view model's <c>PropertyChanged</c>, which runs before the binding that reveals the
@@ -307,6 +352,25 @@ public sealed class QuickConnectTests : IAsyncLifetime
.OfType<ListBoxItem>()
.First(item => ReferenceEquals(item.DataContext, host));
/// <summary>What one row actually draws, in order, ignoring the lines that collapsed.</summary>
private static List<string> VisibleTexts(Visual row) =>
row.GetVisualDescendants()
.OfType<TextBlock>()
.Where(text => text.IsEffectivelyVisible)
.Select(text => text.Text ?? string.Empty)
.ToList();
/// <summary>Runs the layout pass the application's dispatcher would run after the list changed.</summary>
/// <remarks>
/// Without it the new rows are in the collection but not in the visual tree, so <see cref="RowFor"/>
/// finds nothing to look at.
/// </remarks>
private static void Relayout(Window window)
{
Dispatcher.UIThread.RunJobs();
window.UpdateLayout();
}
private static Point Centre(Visual control, Visual window) =>
control.TranslatePoint(new Point(control.Bounds.Width / 2, control.Bounds.Height / 2), window)
?? throw new InvalidOperationException("the control is not in this window's tree");
@@ -325,4 +389,37 @@ public sealed class QuickConnectTests : IAsyncLifetime
await vault.LoadAsync(Token);
}
/// <summary>
/// Adds a second, shared vault to the fixture's session and files one host into it.
/// </summary>
/// <remarks>
/// Straight into the session rather than through <c>VaultsViewModel</c>, which is the technique the
/// settings pages' suite uses and for the same reason: creating a vault needs a connection, and this
/// shell has none. The key is generated on this machine either way, so what the session ends up holding
/// is the same thing a real creation leaves behind — see <see cref="StubTeamServer"/>.
/// </remarks>
private async Task SeedSharedHostAsync()
{
using var teamServer = new StubTeamServer();
var shared = await session.CreateTeamVaultAsync(
teamServer.Teams, StubTeamServer.SharedTeamId, "Platform secrets", Token);
await vault.LoadAsync(Token);
vault.SelectedTargetVault =
vault.TargetVaults.Single(choice => choice.VaultId == shared.VaultId);
vault.NewHostCommand.Execute(null);
vault.EditorLabel = "prod-db";
vault.EditorHostname = "db.internal";
vault.EditorUsername = "deploy";
await vault.SaveHostCommand.ExecuteAsync(null);
vault.IsEditing.ShouldBeFalse(vault.Status);
await vault.LoadAsync(Token);
}
}
@@ -551,7 +551,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -663,9 +663,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"CommunityToolkit.Mvvm": {
"type": "CentralTransitive",
@@ -771,11 +771,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
},
@@ -1765,6 +1765,28 @@ public sealed class ShellFlowTests : IAsyncLifetime
shell.IsTerminalShowing.ShouldBeTrue();
}
/// <remarks>
/// The palette searches a host's vault name, and only where the row prints it — which a session holding
/// one vault never does. Matching it anyway would answer "personal" with the entire keychain, ranked
/// behind nothing and explained by nothing on screen. See <c>MainWindowViewModel.Rank</c>.
/// </remarks>
[Fact]
public async Task ThePalette_DoesNotMatchTheVaultNameWhenThereIsOnlyOneVaultToBeIn()
{
var vault = await ReadyToConnectAsync();
vault.VaultName.ShouldBe("Personal", "which is the word this test types");
shell.ToggleSearchCommand.Execute(null);
shell.SearchResults.ShouldHaveSingleItem()
.HasVaultBadge.ShouldBeFalse("one vault is nothing to tell apart");
shell.SearchText = "personal";
shell.SearchResults.ShouldBeEmpty();
}
/// <remarks>
/// The rail marks where you are, and a terminal is not one of its destinations. Lighting HOSTS while a
/// terminal fills the window would point at a screen that is not showing — and the selected tab already
@@ -766,6 +766,59 @@ public sealed class VaultSharingTests : IAsyncLifetime
vault.Status.ShouldContain("left alone", Case.Insensitive);
}
/// <remarks>
/// <para>
/// The palette reaches every vault the session holds a key for, so the vault's name is one of the things
/// somebody can reasonably type into it: "show me the team's machines" is a question a list spanning two
/// vaults invites, and until now the only answer was to know one of the machines by name already.
/// </para>
/// <para>
/// The order is the assertion that matters. A vault name is the widest of the three readings — one word
/// matches every host in that vault at once — so the host whose own name carries the word has to come
/// first, or typing a machine's name would bury it under everybody else's.
/// </para>
/// </remarks>
[Fact]
public async Task ThePalette_FindsAHostByItsVaultsName_BehindOneCalledThatItself()
{
await UnlockedAsync();
var vaults = shell.Vaults;
await CreateVaultAsync(vaults, "Platform secrets");
var vault = shell.Vault!;
var sharedVaultId = vaults.SelectedVault!.VaultId;
await vault.LoadAsync(Token);
vault.NewHostCommand.Execute(null);
vault.EditorLabel = "platform-gateway";
vault.EditorHostname = "gateway.internal";
await vault.SaveHostCommand.ExecuteAsync(null);
vault.NewHostCommand.Execute(null);
vault.EditorSelectedVault =
vault.EditorVaultChoices.Single(choice => choice.VaultId == sharedVaultId);
vault.EditorLabel = "prod-db";
vault.EditorHostname = "db.internal";
await vault.SaveHostCommand.ExecuteAsync(null);
vault.IsEditing.ShouldBeFalse(vault.Status);
shell.ToggleSearchCommand.Execute(null);
shell.SearchText = "platform";
shell.SearchResults
.Select(row => row.Label)
.ShouldBe(["platform-gateway", "prod-db"], "a host's own name outranks its vault's");
shell.SearchResults[1].VaultBadge.ShouldBe(
"PLATFORM SECRETS", "and the row says what it was found by");
}
/// <summary>
/// Two hosts for the run above: one in the personal vault under a group, one already in the destination.
/// </summary>
@@ -540,7 +540,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -652,9 +652,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"CommunityToolkit.Mvvm": {
"type": "CentralTransitive",
@@ -760,11 +760,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
},
@@ -229,7 +229,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"AWSSDK.Core": {
@@ -249,9 +249,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -270,11 +270,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -340,7 +340,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -379,9 +379,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -481,11 +481,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -41,11 +41,11 @@
},
"SSH.NET": {
"type": "Direct",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
},
@@ -306,7 +306,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.terminal": {
@@ -317,9 +317,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -217,7 +217,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.terminal": {
@@ -228,9 +228,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -249,11 +249,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -217,7 +217,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.transfer": {
@@ -228,9 +228,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"libsodium": {
"type": "CentralTransitive",
@@ -249,11 +249,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}
@@ -390,9 +390,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -431,11 +431,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
},
+8 -8
View File
@@ -436,7 +436,7 @@
"type": "Project",
"dependencies": {
"NSec.Cryptography": "[26.4.0, )",
"SSH.NET": "[2026.0.0, )"
"SSH.NET": "[2025.1.0, )"
}
},
"dodossh.client.storage": {
@@ -486,9 +486,9 @@
},
"BouncyCastle.Cryptography": {
"type": "CentralTransitive",
"requested": "[2.7.0, )",
"resolved": "2.7.0",
"contentHash": "U+12df8UEWHgBi04YVf/Lgi2dy3SItlIYvHjjEVa/BngCQIzDCDRBk50DDByCfDvSbe5pRNFr3b7UrVK2kMcLw=="
"requested": "[2.6.2, )",
"resolved": "2.6.2",
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
},
"EFCore.NamingConventions": {
"type": "CentralTransitive",
@@ -599,11 +599,11 @@
},
"SSH.NET": {
"type": "CentralTransitive",
"requested": "[2026.0.0, )",
"resolved": "2026.0.0",
"contentHash": "Yu9dirPq8l3oaat0+OQ7K0nUf5MmYltpia5UGqsApTG4zTPvBC1cxbNnC3NERij26dUST0A3Ef1QdHSn5ArbWQ==",
"requested": "[2025.1.0, )",
"resolved": "2025.1.0",
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
"dependencies": {
"BouncyCastle.Cryptography": "2.7.0",
"BouncyCastle.Cryptography": "2.6.2",
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
}
}