Files
DodoSSH/Directory.Packages.props
T
jaap-jan 6728a0a597 Let the desktop client replace itself, and give the repository one version
Packaging for Windows, and the updater that only exists once something is
packaged. Velopack, win-x64, fed from the project's own forge — never from the
deployment a client signs in to, which is ADR 0011 rule 2 carried over
unchanged and is why the feed address is a constant in the code rather than a
setting. See docs/adr/0012-desktop-distribution-and-updates.md.

**Nothing is ever installed while somebody is using it.** A newer build is found
on a six-hourly pass, downloaded in the background, and then waits — for a
restart the user presses, or for the next launch they were going to do anyway.
That is a policy rather than caution: this application argues at length that
locking keeps shells running, because a lock that destroyed work would stop
being used, and a restart does not keep them. Having taught that, it owes the
user the choice at the one moment it stops being true, and the sentence saying
so counts the shells it would close.

**The version is now derived from the v* tag**, by MinVer, for everything. There
was no version before this — no property anywhere, so every assembly reported
the SDK's 1.0.0 and the API served that string as its serverVersion to every
client that asked. The tag was already the version of record for the container
image; this makes it the version of record full stop. MinVer's failure mode is
answering plausibly rather than failing, and here a wrong version is a client
that never updates, so it is guarded twice: fetch-depth 0 on every checkout, and
a step that fails a tag build when the tag and the computed version disagree.

**The pack id is DodoSSH.Desktop and not DodoSSH**, which is the one decision
here that would have destroyed data. Velopack installs to %LOCALAPPDATA%\<packId>
and removes that whole directory on uninstall, and %LOCALAPPDATA%\DodoSSH is
where ClientPaths keeps the encrypted cache, the outbox of changes not yet
pushed, and the device key. The obvious id would have had the uninstaller
silently delete work the server has never seen — the thing the application
refuses to do without a counted confirmation. Velopack's own advice to move user
data to roaming %APPDATA% is declined for the reason ClientPaths already gives.

**Releases are cut by a person, and CI gains no job that could.** The tempting
argument is that a forge write token is not a signing key. It does not survive
contact with what the token does: Velopack clients trust their feed and do not
verify a package signature when they apply one, so whoever can write a release
can ship an update every install runs. That is the capability ADR 0011 rule 1
puts on a machine which is not a runner, reached through a different door. The
mechanical objection — vpk needs Windows and the runners are Linux — is the
smaller of the two and is recorded beside it, because somebody will fix one and
believe they are done.

Unsigned for now, deliberately and with the cost stated where a user reads it:
SmartScreen warns once per person, on Setup.exe, because Mark-of-the-Web is
applied by the browser that downloaded it. In-app updates are fetched by the
application and applied from a local file, and never trip it.

The banner is a fourth row of the window rather than an overlay. Anything drawn
in the terminal's rectangle is sliced by the native child window that composites
above it — the defect this window has shipped once — and a sibling row is the
arrangement TitleBar and StatusBar already prove works.

----

Three defects surfaced on the way, none of them in the feature being built.

**A settings key absent from the file came back as the CLR default, not the
declared one.** The JSON source generator builds a record through a synthesised
parameterised constructor and assigns every property from its argument array, so
a property initializer runs and is then overwritten by a default for anything the
file did not contain. A settings.json of {} read back a font size of 0, clamped
up to the 8px floor rather than the 13px the renderer draws at. It could not bite
while there was one setting, because that setting was written on every save and
so was never absent; adding a second would have turned automatic update checks
off for every existing profile, silently, the opposite of the documented default.
Reflection-based deserialisation of the same JSON answers correctly, which is why
every way of checking it by hand agrees except the one that ships. The defaults
now live on the constructor parameters, which is the only place the generator
reads them from.

**Declaring a RuntimeIdentifier on the desktop head broke the server's image
build.** It is the obvious way to let a self-contained publish restore under
locked mode, and it writes a net10.0/win-x64 target into the lock file of every
project the head references transitively — including DodoSSH.Contracts and
DodoSSH.Crypto, which the API builds too. The Dockerfile restores those with no
RID and fails NU1004. Found by running docker build rather than by reading. The
RID stays out of the committed state; the two commands that need one ask for it
unlocked, and the release script puts the lock files back.

**A Docker ARG named VERSION silently sets MSBuild's Version.** An ARG is an
environment variable for the rest of the stage, MSBuild reads environment
variables as properties, and property names are case-insensitive. With the
workflow passing main-<short sha> on a main build the publish died with
NETSDK1018 pointing at DodoSSH.Contracts, a project nobody had touched. The build
stage's argument is ASSEMBLY_VERSION now, empty except on a tag build.

All three are in docs/platform-flags.md, which is where the next person will look.

----

Verified: the whole solution builds and restores locked; 289 shell, 93 layout and
54 session tests pass, including the regression test for the settings defect and
a measurement of the banner at the window's minimum width. vpk pack runs end to
end and reports "Verified VelopackApp.Run()" against Program.Main. The API image
builds correctly both as a main build and as a tag build, carrying 1.0.0 and
0.1.0 respectively.

Not verified, and it needs a published release to be: installing, updating and
uninstalling on a real machine. That is Phase 15 of docs/manual-checks.md, and
the pack id and the WebView2 profile fix are reasoned and commented but only
proved by walking it. Two things to watch at the first upload — the reverse
proxy's body-size limit for a 64 MB asset, and whether vpk upload gitea is happy
with Gitea 1.27.1.
2026-08-04 17:04:41 +02:00

215 lines
13 KiB
XML

<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<!--
Versions are pinned here for the whole solution. Packages are added per milestone
rather than all at once, so that every entry is one we have actually verified and
restored. See docs/adr/ for the choices behind the notable ones.
-->
<ItemGroup Label="ASP.NET Core">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
</ItemGroup>
<ItemGroup Label="Endpoints">
<!--
FastEndpoints drags FluentValidation, JobQueues and Messaging in behind it. None of the
three are used: validation lives in the feature services and is banned from moving into a
Validator<T> (see BannedSymbols.txt and ADR 0008), and there is no message bus. They are
left as plain transitives rather than declared here, because declaring a transitive under
central transitive pinning is a standing promise to keep its version current, and these are
not ours to steer. Declare one only to force a version forward for an advisory, as the
group below does.
-->
<PackageVersion Include="FastEndpoints" Version="8.2.0" />
</ItemGroup>
<ItemGroup Label="Pinned transitive dependencies">
<!--
Microsoft.AspNetCore.OpenApi 10.0.10 resolves Microsoft.OpenApi 2.0.0, which is
covered by GHSA-v5pm-xwqc-g5wc (high: circular schema references can terminate
OpenAPI parsing; vulnerable <= 2.7.4, patched in 2.7.5). Pinned forward within the
2.x major that ASP.NET Core 10 targets. Revisit when the ASP.NET Core package
itself moves off 2.0.0.
-->
<PackageVersion Include="Microsoft.OpenApi" Version="2.11.0" />
<!--
Microsoft.EntityFrameworkCore.Sqlite 10.0.10 resolves SQLitePCLRaw 2.1.11, whose bundled
SQLite build is covered by GHSA-2m69-gcr7-jv3q (high). 2.1.12 is the fix and is a patch bump
inside the minor EF asks for, so nothing needs to move. Pinned as a family: the bundle, the
core, the provider and the native library ship in lockstep and a mixed set is a loader error
at runtime rather than a build failure.
SQLitePCLRaw 3.x exists and is deliberately not used here. EF Core 10 is built against 2.1.x,
and 3.0 is also where bundle_e_sqlcipher was deprecated — which is one of the reasons the
local cache does not use SQLCipher at all. See DodoSSH.Client.Storage.
-->
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.12" />
<PackageVersion Include="SQLitePCLRaw.core" Version="2.1.12" />
<PackageVersion Include="SQLitePCLRaw.lib.e_sqlite3" Version="2.1.12" />
<PackageVersion Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.12" />
</ItemGroup>
<ItemGroup Label="Persistence">
<!--
EF Core pinned explicitly. The Npgsql provider asks only for 10.0.4 while
Microsoft.EntityFrameworkCore.Design pulls 10.0.10, and because Design is
PrivateAssets=all that higher version does not flow to referencing projects — which
produces a CS1705 in any test project that references Infrastructure. Pinning here lifts
every project to one version via central transitive pinning.
-->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<!--
Verified compatible with EF 10 before adopting; the plan flagged this package as
historically lagging EF majors. Fallback if it ever blocks an upgrade is explicit
HasColumnName in every IEntityTypeConfiguration: more code, zero risk.
-->
<PackageVersion Include="EFCore.NamingConventions" Version="10.0.1" />
<!--
The client's local cache. Plain SQLite, deliberately not SQLCipher: the rows are already
ciphertext, so an encrypted database file would add a native dependency and a licence
obligation to protect bytes that are protected already. SQLitePCLRaw's own
bundle_e_sqlcipher is deprecated as of 3.0 besides. See DodoSSH.Client.Storage.
-->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
</ItemGroup>
<ItemGroup Label="Cryptography">
<!--
NSec wraps libsodium. Chosen over the BCL because .NET has no X25519 or Ed25519, and
because ChaCha20Poly1305.IsSupported is false on macOS, which rules out the in-box
AEAD for a cross-platform client. NSec also holds key material in libsodium's
guarded, non-swappable memory, which a byte[] cannot do. See docs/crypto.md.
26.4.0 targets net9.0; net10.0 consumes it by forward compatibility. Native binaries
arrive via the libsodium package, pinned here because central transitive pinning
requires it to be declared.
-->
<PackageVersion Include="NSec.Cryptography" Version="26.4.0" />
<PackageVersion Include="libsodium" Version="1.0.22" />
<!-- Managed differential oracle for the crypto test suite only. -->
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
</ItemGroup>
<ItemGroup Label="Desktop client">
<!--
SSH.NET already covers PTY shells, all three auth methods, ed25519/RSA/ECDSA, encrypted
keys including PuTTY .ppk, SFTP, and local/remote/dynamic forwarding. The gaps are
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/.
-->
<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
pinned. Taken rather than hand-rolled because the alternative here is implementing SigV4
request signing, and unlike the openssh-key-v1 container (which had no library at all) a
maintained implementation of this exists and is the one every S3-compatible service tests
against.
AWSSDK.Core is declared and pinned forward. What AWSSDK.S3 4.0.101.6 resolves on its own is
4.0.1, which is covered by GHSA-9cvc-h2w8-phrp — low severity, and this repository builds
with NuGet audit as errors, so "low" is not a reason to carry it. 4.0.100.9 is past it and
inside the same major. Same treatment as the OpenApi and SQLitePCLRaw entries above, and the
same standing obligation: this is now ours to keep current.
-->
<PackageVersion Include="AWSSDK.S3" Version="4.0.101.6" />
<PackageVersion Include="AWSSDK.Core" Version="4.0.100.9" />
<!--
Avalonia 12.1.0, with the WebView control on 12.0.1 — the latest it has shipped. Its
dependency is Avalonia >= 12.0.0 with no upper bound and it targets net10.0, so the skew
is fine. Checked rather than assumed, because a control package lagging the core version
is exactly where a silent runtime mismatch would hide.
-->
<PackageVersion Include="Avalonia" Version="12.1.1" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
<!--
The Android head. Same core version as the desktop one, which is not a courtesy: the two heads
share every view model, so a version skew between them would be a skew inside one object graph.
-->
<PackageVersion Include="Avalonia.Android" Version="12.1.1" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.1" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.1" />
<PackageVersion Include="Avalonia.Controls.WebView" Version="12.0.1" />
<!--
Lets a test lay out real XAML and measure it, which is the only way this repository can catch a
control clipped off the bottom of a column — the defect this window has already shipped once. Pinned
to the core version exactly rather than allowed to drift: the whole value of the harness is that the
numbers it measures are the numbers the application renders.
-->
<PackageVersion Include="Avalonia.Headless" Version="12.1.1" />
<!--
Source-generated MVVM, so there is no reflection and trimming stays viable. ReactiveUI's one
real advantage is observable composition over streams, and the place that would help — the
terminal data plane — is Pipelines and channel code rather than view models.
-->
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<!--
Packaging and self-update for the Windows desktop head. MIT, and on net10.0 it declares no
dependencies at all — the whole package is one managed assembly, so it restores and compiles
on the Linux runner that builds the solution even though the thing it produces only runs on
Windows. That mattered enough to check: a per-OS conditional PackageReference is not available
here, because it would make packages.lock.json depend on the operating system and CI's locked
restore would then fail on whichever platform did not write it.
MSIX would have been the platform-native choice and is ruled out rather than deprioritised: a
packaged app runs WebView2 in an AppContainer where loopback is blocked, and the terminal data
plane is a loopback WebSocket. See docs/platform-flags.md.
The update feed this is pointed at is the project's own forge and never a DodoSSH deployment.
That is ADR 0011 rule 2, and it is the reason the repository URL in VelopackUpdateSource is a
constant rather than a setting: an operator who could answer the update check could pin a
chosen user to a known-vulnerable build. See docs/adr/0012-desktop-distribution-and-updates.md.
-->
<PackageVersion Include="Velopack" Version="1.2.0" />
</ItemGroup>
<ItemGroup Label="Versioning">
<!--
One version for the whole repository, derived from the nearest v* git tag. The tag was already
the version of record — ci.yml's "work out the tags" step parses refs/tags/v* for the docker
image — and nothing set an assembly version at all, so every binary reported the SDK's default
1.0.0 and the API served that as its ServerVersion. Deriving from the tag makes those one
number instead of two that can disagree.
MinVer's one real failure mode is that it answers plausibly rather than failing: a shallow
clone with no tags yields 0.0.0-alpha.0.N. Here that is not cosmetic — Velopack compares the
version baked into a package against the one it is running, so a wrong answer is a client that
never updates. Hence two guards in ci.yml: fetch-depth 0 on every checkout, and a step on tag
builds that fails if the computed version and the tag disagree.
-->
<PackageVersion Include="MinVer" Version="7.0.0" />
</ItemGroup>
<ItemGroup Label="Analyzers">
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.137" />
</ItemGroup>
<ItemGroup Label="Testing">
<!--
xunit.v3 runs on Microsoft.Testing.Platform, not VSTest. Microsoft.NET.Test.Sdk and
coverlet.collector are VSTest components: referencing them alongside MTP raises
MTP0001 and their collector never runs, so neither is referenced.
No coverage collector yet. Microsoft.Testing.Extensions.CodeCoverage 18.9.0 pulls
Microsoft.Testing.Platform.MSBuild 1.9.1, which is built against MTP 1.x and throws
TypeLoadException on IDataConsumer against the MTP 2.3.x that xunit.v3 3.2.2 brings.
Coverage gates are an M3 concern (90% on Domain and Authorization); pick a version
aligned with MTP 2.x then rather than carrying a broken dependency until it matters.
-->
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="NSubstitute" Version="6.0.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.13.0" />
<!-- Generic container, for the OpenSSH server the SSH suite talks to. -->
<PackageVersion Include="Testcontainers" Version="4.13.0" />
<PackageVersion Include="Respawn" Version="7.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<!--
Stands in for the identity provider so integration tests exercise the real JwtBearer
pipeline. A TestAuthHandler that bypasses it would hide exactly the claim-mapping
mistakes that cause real authorization holes.
-->
<PackageVersion Include="WireMock.Net" Version="2.13.0" />
</ItemGroup>
</Project>