diff --git a/.editorconfig b/.editorconfig
index 511f8d5..04c606b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -118,6 +118,11 @@ dotnet_diagnostic.CA1711.severity = none
# a non-issue in practice and it heavily constrains domain naming.
dotnet_diagnostic.CA1724.severity = none
+# MA0048 requires one type per file. Good for large types, actively worse for small DTO
+# clusters: splitting SyncPullRequest from SyncPullResponse means a reviewer opens two files
+# to understand one endpoint's contract. The BCL groups related types the same way.
+dotnet_diagnostic.MA0048.severity = none
+
# We use file-scoped namespaces and modern C#; these fire on deliberate style choices.
dotnet_diagnostic.CA1812.severity = none # internal types instantiated by DI
dotnet_diagnostic.CA1849.severity = warning # sync call in async method
@@ -132,3 +137,15 @@ dotnet_diagnostic.CA1034.severity = none
generated_code = true
dotnet_analyzer_diagnostic.severity = none
dotnet_diagnostic.IDE0055.severity = none
+
+[*.{g,g.i,generated,designer}.cs]
+# Source-generator output. In particular the System.Text.Json generator emits a public
+# JsonTypeInfo member per serialisable type, which PublicApiAnalyzers would otherwise demand
+# be tracked in PublicAPI.txt — hundreds of entries derived mechanically from the
+# [JsonSerializable] list, drowning the entries that describe the actual wire contract.
+generated_code = true
+dotnet_analyzer_diagnostic.severity = none
+dotnet_diagnostic.RS0016.severity = none
+dotnet_diagnostic.RS0017.severity = none
+dotnet_diagnostic.RS0041.severity = none
+dotnet_diagnostic.IDE0055.severity = none
diff --git a/src/DodoSSH.Contracts/DodoSshJsonContext.cs b/src/DodoSSH.Contracts/DodoSshJsonContext.cs
new file mode 100644
index 0000000..8624b39
--- /dev/null
+++ b/src/DodoSSH.Contracts/DodoSshJsonContext.cs
@@ -0,0 +1,86 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace DodoSSH.Contracts;
+
+///
+/// Source-generated JSON contracts, shared by the API and the client so both sides serialise
+/// identically.
+///
+///
+///
+/// Source generation rather than reflection: no startup reflection cost, and it keeps the
+/// assembly trim- and AOT-compatible.
+///
+///
+/// Always use or . Constructing
+/// by hand and merely pointing its resolver at this context
+/// silently discards every setting configured here — notably
+/// , which
+/// otherwise replaces with . That failure
+/// is invisible until two implementations disagree about whether "1" is a valid number.
+///
+///
+[JsonSourceGenerationOptions(
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ NumberHandling = JsonNumberHandling.Strict,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ UseStringEnumConverter = true)]
+[JsonSerializable(typeof(MetaResponse))]
+[JsonSerializable(typeof(DodoSshConfiguration))]
+[JsonSerializable(typeof(MeResponse))]
+[JsonSerializable(typeof(EnrollmentRequest))]
+[JsonSerializable(typeof(EnrollmentResponse))]
+[JsonSerializable(typeof(DirectoryEntry))]
+[JsonSerializable(typeof(IReadOnlyList))]
+[JsonSerializable(typeof(VaultSummary))]
+[JsonSerializable(typeof(SyncPullRequest))]
+[JsonSerializable(typeof(SyncPullResponse))]
+[JsonSerializable(typeof(SyncPushRequest))]
+[JsonSerializable(typeof(SyncPushResponse))]
+[JsonSerializable(typeof(SyncChange))]
+[JsonSerializable(typeof(RelayTicketRequest))]
+[JsonSerializable(typeof(RelayTicketResponse))]
+[JsonSerializable(typeof(RelaySessionSummary))]
+[JsonSerializable(typeof(IReadOnlyList))]
+public sealed partial class DodoSshJsonContext : JsonSerializerContext
+{
+ ///
+ /// The canonical options for writing responses and for reading them on the client.
+ ///
+ ///
+ /// Unmapped members are tolerated here on purpose: an older client must still be able to
+ /// read a newer server's response rather than failing on a field it does not know about.
+ ///
+ public static JsonSerializerOptions ResponseOptions => Default.Options;
+
+ ///
+ /// Options for deserialising inbound request bodies.
+ ///
+ ///
+ /// Identical to except that unmapped members are rejected, so
+ /// a client bug — a renamed or misspelled property — surfaces as a 400 rather than as a
+ /// silently missing value that later looks like data loss. Derived by copying rather than
+ /// constructed fresh, so it inherits every source-generated setting.
+ ///
+ public static JsonSerializerOptions StrictRequestOptions => LazyStrictRequestOptions.Value;
+
+ ///
+ /// Lazy, not a static initialiser. The generated Default property is a static of this
+ /// same class, so reading it from this type's initialiser is a cycle: the accessor runs
+ /// before its backing field is assigned and yields null.
+ ///
+ private static readonly Lazy LazyStrictRequestOptions =
+ new(CreateStrictRequestOptions, LazyThreadSafetyMode.ExecutionAndPublication);
+
+ private static JsonSerializerOptions CreateStrictRequestOptions()
+ {
+ var options = new JsonSerializerOptions(Default.Options)
+ {
+ UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
+ };
+
+ options.MakeReadOnly();
+ return options;
+ }
+}
diff --git a/src/DodoSSH.Contracts/EncryptedPayload.cs b/src/DodoSSH.Contracts/EncryptedPayload.cs
new file mode 100644
index 0000000..0d166fe
--- /dev/null
+++ b/src/DodoSSH.Contracts/EncryptedPayload.cs
@@ -0,0 +1,25 @@
+namespace DodoSSH.Contracts;
+
+///
+/// An opaque ciphertext blob together with the plaintext metadata needed to recompute its AAD.
+///
+///
+///
+/// The server stores and returns these verbatim. It cannot decrypt one, cannot validate the
+/// contents, and cannot merge two. See docs/crypto.md.
+///
+///
+/// is a complete DSH1 envelope and already carries its own algorithm
+/// identifier and nonce, so neither is repeated here. and
+/// are stored as columns because the AAD is recomputed from the row
+/// rather than transmitted, and because they are what makes a lazy re-encrypt-on-write
+/// migration possible later.
+///
+///
+/// The complete DSH1 envelope, base64 on the wire.
+/// Vault key generation this payload was encrypted under.
+/// Version of the AAD derivation rule used.
+public sealed record EncryptedPayload(
+ byte[] Envelope,
+ uint KeyGeneration,
+ byte AadVersion);
diff --git a/src/DodoSSH.Contracts/Enrollment.cs b/src/DodoSSH.Contracts/Enrollment.cs
new file mode 100644
index 0000000..fecf5bc
--- /dev/null
+++ b/src/DodoSSH.Contracts/Enrollment.cs
@@ -0,0 +1,176 @@
+namespace DodoSSH.Contracts;
+
+///
+/// Argon2id parameters as stored and transmitted.
+///
+///
+/// Stored in plaintext per wrap row. Salts are not secrets, and keeping the parameters with the
+/// wrap makes raising them later a per-user, unlock-time migration rather than a breaking
+/// change — an older client can still open its own wrap.
+///
+/// is kibibytes, matching both the storage column and libsodium.
+/// Client code should go through Argon2Profile rather than handling the number directly;
+/// see docs/crypto.md §2 for why that unit needs guarding.
+///
+///
+/// KDF identifier. Currently always argon2id.
+/// 16-byte salt.
+/// Memory cost in KiB.
+/// Number of passes.
+/// Lanes. Always 1; libsodium supports no other value.
+public sealed record KdfParameters(
+ string Algorithm,
+ byte[] Salt,
+ int MemoryKibibytes,
+ int Passes,
+ int Parallelism);
+
+///
+/// The self-describing, signed statement binding a user to their public keys.
+///
+///
+/// Its SHA-256 is used as the nonce of a fresh OIDC authorization, so the resulting ID
+/// token is signed by the identity provider over exactly these keys. That is what stops the
+/// DodoSSH server fabricating a key for a user who never enrolled. See ADR 0001.
+///
+/// Statement format version.
+/// OIDC issuer.
+/// OIDC subject.
+/// Email at enrollment time, for display only.
+/// X25519 public key, 32 bytes.
+/// Ed25519 public key, 32 bytes.
+/// Generation of this key pair, starting at 1.
+/// When the client generated the keys.
+/// Human-readable name of the enrolling device.
+public sealed record KeyStatement(
+ int Version,
+ string Issuer,
+ string Subject,
+ string? Email,
+ byte[] EncryptionPublicKey,
+ byte[] SigningPublicKey,
+ int KeyGeneration,
+ DateTimeOffset CreatedAt,
+ string DeviceName);
+
+/// A request to enroll a user's first identity key pair.
+/// The key statement.
+/// Ed25519 self-signature over the statement.
+///
+/// An ID token whose nonce equals the SHA-256 of the statement. The server verifies it
+/// against the provider's JWKS.
+///
+///
+/// The user's secret bundle sealed under the passphrase-derived key. Opaque to the server.
+///
+/// Parameters needed to re-derive the wrapping key.
+///
+/// X25519 public key of this device, so the bundle can also be wrapped to the device and
+/// unlocked without re-entering the passphrase.
+///
+///
+/// The same bundle sealed under a recovery-code-derived key.
+///
+/// Parameters for the recovery wrap.
+public sealed record EnrollmentRequest(
+ KeyStatement Statement,
+ byte[] StatementSignature,
+ string IdentityProviderToken,
+ byte[] WrappedPrivateKey,
+ KdfParameters KdfParameters,
+ byte[]? DevicePublicKey,
+ byte[]? RecoveryWrappedPrivateKey,
+ KdfParameters? RecoveryKdfParameters);
+
+/// Result of a successful enrollment.
+/// The user's identifier.
+/// The generation now current.
+/// Identity fingerprint over both public keys.
+/// The automatically created personal vault.
+/// The enrolled device, when a device key was supplied.
+///
+/// Position of this statement in the append-only key log. Clients cache the log head and
+/// include it in signed operations, which is what makes a forked view detectable.
+///
+public sealed record EnrollmentResponse(
+ Guid UserId,
+ int KeyGeneration,
+ byte[] Fingerprint,
+ Guid PersonalVaultId,
+ Guid? DeviceId,
+ long KeyLogSequence);
+
+/// A public-key directory entry.
+///
+/// Before wrapping a vault key to one of these, a client must verify it: check the identity
+/// provider binding, compare against its pinned fingerprint, and confirm the key log head is
+/// consistent. Wrapping to an unverified key is the one mistake that undoes end-to-end
+/// encryption entirely.
+///
+/// The user.
+/// Email, for display.
+/// Display name.
+/// X25519 public key.
+/// Ed25519 public key.
+/// Identity fingerprint.
+/// Generation of this key pair.
+/// Key log position of the statement that introduced it.
+public sealed record DirectoryEntry(
+ Guid UserId,
+ string? Email,
+ string? DisplayName,
+ byte[] EncryptionPublicKey,
+ byte[] SigningPublicKey,
+ byte[] Fingerprint,
+ int KeyGeneration,
+ long KeyLogSequence);
+
+/// The caller's own profile and unlock state.
+/// The user.
+/// OIDC issuer.
+/// OIDC subject.
+/// Email.
+/// Display name.
+///
+/// True when no identity key exists yet, so the client must run enrollment before anything else.
+///
+/// Current key generation, when enrolled.
+///
+/// The passphrase wrap of the secret bundle, for unlocking on this device.
+///
+/// Parameters to re-derive the wrapping key.
+/// Vaults the caller can reach.
+public sealed record MeResponse(
+ Guid UserId,
+ string Issuer,
+ string Subject,
+ string? Email,
+ string? DisplayName,
+ bool EnrollmentRequired,
+ int? KeyGeneration,
+ byte[]? WrappedPrivateKey,
+ KdfParameters? KdfParameters,
+ IReadOnlyList Vaults);
+
+/// A vault the caller can reach, with the wrapped key needed to open it.
+/// The vault.
+/// Display name. Plaintext, and only for vaults, not for items.
+/// Whether this is the caller's personal vault.
+/// Owning team, for a team vault.
+/// Current key generation.
+/// The caller's effective permissions, as a flags value.
+///
+/// The vault key sealed to the caller's X25519 key. Absent when a grant is awaiting re-wrap
+/// after a rekey, in which case the vault is temporarily unreadable and a member holding Share
+/// must complete it.
+///
+/// Whether a membership change has left this vault needing a rekey.
+public sealed record VaultSummary(
+ Guid VaultId,
+ string Name,
+ bool IsPersonal,
+ Guid? TeamId,
+ uint KeyGeneration,
+ int Permissions,
+ byte[]? WrappedVaultKey,
+ bool RekeyRequired);
diff --git a/src/DodoSSH.Contracts/Meta.cs b/src/DodoSSH.Contracts/Meta.cs
new file mode 100644
index 0000000..05335b6
--- /dev/null
+++ b/src/DodoSSH.Contracts/Meta.cs
@@ -0,0 +1,76 @@
+namespace DodoSSH.Contracts;
+
+///
+/// Server capabilities, versions and limits.
+///
+///
+/// This replaces URL-based API versioning. In a self-hosted product the client and server
+/// upgrade independently, so skew is normal rather than exceptional, and a client needs to ask
+/// what this particular server supports rather than assume. See ADR 0002.
+///
+/// Informational build version.
+/// Supported API major versions.
+///
+/// Sync semantics version. A mismatch means the client must not push.
+///
+///
+/// DSH1 specification version the server's stored data conforms to.
+///
+///
+/// Enabled optional features, such as relay or teams. Absence must be treated as
+/// unsupported rather than as an error.
+///
+///
+/// Oldest client this server will serve. Clients below this must show a remediation screen
+/// rather than failing obscurely.
+///
+/// Cap on operations in one push.
+/// Cap on total ciphertext in one push.
+/// Cap on a single item's ciphertext.
+public sealed record MetaResponse(
+ string ServerVersion,
+ IReadOnlyList ApiVersions,
+ int SyncProtocolVersion,
+ int CryptoSpecVersion,
+ IReadOnlyList Features,
+ string? MinClientVersion,
+ int MaxOperationsPerPush,
+ long MaxPayloadBytes,
+ long MaxItemPayloadBytes);
+
+///
+/// Everything a client needs to begin authenticating, from one URL.
+///
+///
+/// Served anonymously at /.well-known/dodossh-configuration. This is the onboarding
+/// story: the user types a server URL and the client discovers the rest.
+///
+/// Base URL for API calls.
+/// Identity provider settings.
+/// Relay settings.
+public sealed record DodoSshConfiguration(
+ Uri ApiBaseUrl,
+ OidcConfiguration Oidc,
+ RelayConfiguration Relay);
+
+/// Identity provider settings for a public native client.
+/// Issuer URL, from which discovery and JWKS are fetched.
+/// Public client identifier.
+/// Scopes to request.
+///
+/// Registered loopback redirect pattern. RFC 8252: a native client uses a loopback redirect on
+/// an ephemeral port with the system browser, never a custom scheme and never an embedded
+/// browser, so the user can see the real address bar.
+///
+public sealed record OidcConfiguration(
+ Uri Authority,
+ string ClientId,
+ IReadOnlyList Scopes,
+ string LoopbackRedirectPattern);
+
+/// Relay settings.
+/// Whether this server offers a relay at all.
+/// Relay endpoint, when enabled.
+public sealed record RelayConfiguration(
+ bool Enabled,
+ Uri? WebSocketUrl);
diff --git a/src/DodoSSH.Contracts/PublicAPI.Unshipped.txt b/src/DodoSSH.Contracts/PublicAPI.Unshipped.txt
index ad0d54b..695cce1 100644
--- a/src/DodoSSH.Contracts/PublicAPI.Unshipped.txt
+++ b/src/DodoSSH.Contracts/PublicAPI.Unshipped.txt
@@ -1,5 +1,415 @@
#nullable enable
+DodoSSH.Contracts.DirectoryEntry
+DodoSSH.Contracts.DirectoryEntry.$() -> DodoSSH.Contracts.DirectoryEntry!
+DodoSSH.Contracts.DirectoryEntry.Deconstruct(out System.Guid UserId, out string? Email, out string? DisplayName, out byte[]! EncryptionPublicKey, out byte[]! SigningPublicKey, out byte[]! Fingerprint, out int KeyGeneration, out long KeyLogSequence) -> void
+DodoSSH.Contracts.DirectoryEntry.DirectoryEntry(System.Guid UserId, string? Email, string? DisplayName, byte[]! EncryptionPublicKey, byte[]! SigningPublicKey, byte[]! Fingerprint, int KeyGeneration, long KeyLogSequence) -> void
+DodoSSH.Contracts.DirectoryEntry.DisplayName.get -> string?
+DodoSSH.Contracts.DirectoryEntry.DisplayName.init -> void
+DodoSSH.Contracts.DirectoryEntry.Email.get -> string?
+DodoSSH.Contracts.DirectoryEntry.Email.init -> void
+DodoSSH.Contracts.DirectoryEntry.EncryptionPublicKey.get -> byte[]!
+DodoSSH.Contracts.DirectoryEntry.EncryptionPublicKey.init -> void
+DodoSSH.Contracts.DirectoryEntry.Equals(DodoSSH.Contracts.DirectoryEntry? other) -> bool
+DodoSSH.Contracts.DirectoryEntry.Fingerprint.get -> byte[]!
+DodoSSH.Contracts.DirectoryEntry.Fingerprint.init -> void
+DodoSSH.Contracts.DirectoryEntry.KeyGeneration.get -> int
+DodoSSH.Contracts.DirectoryEntry.KeyGeneration.init -> void
+DodoSSH.Contracts.DirectoryEntry.KeyLogSequence.get -> long
+DodoSSH.Contracts.DirectoryEntry.KeyLogSequence.init -> void
+DodoSSH.Contracts.DirectoryEntry.SigningPublicKey.get -> byte[]!
+DodoSSH.Contracts.DirectoryEntry.SigningPublicKey.init -> void
+DodoSSH.Contracts.DirectoryEntry.UserId.get -> System.Guid
+DodoSSH.Contracts.DirectoryEntry.UserId.init -> void
+DodoSSH.Contracts.DodoSshConfiguration
+DodoSSH.Contracts.DodoSshConfiguration.$() -> DodoSSH.Contracts.DodoSshConfiguration!
+DodoSSH.Contracts.DodoSshConfiguration.ApiBaseUrl.get -> System.Uri!
+DodoSSH.Contracts.DodoSshConfiguration.ApiBaseUrl.init -> void
+DodoSSH.Contracts.DodoSshConfiguration.Deconstruct(out System.Uri! ApiBaseUrl, out DodoSSH.Contracts.OidcConfiguration! Oidc, out DodoSSH.Contracts.RelayConfiguration! Relay) -> void
+DodoSSH.Contracts.DodoSshConfiguration.DodoSshConfiguration(System.Uri! ApiBaseUrl, DodoSSH.Contracts.OidcConfiguration! Oidc, DodoSSH.Contracts.RelayConfiguration! Relay) -> void
+DodoSSH.Contracts.DodoSshConfiguration.Equals(DodoSSH.Contracts.DodoSshConfiguration? other) -> bool
+DodoSSH.Contracts.DodoSshConfiguration.Oidc.get -> DodoSSH.Contracts.OidcConfiguration!
+DodoSSH.Contracts.DodoSshConfiguration.Oidc.init -> void
+DodoSSH.Contracts.DodoSshConfiguration.Relay.get -> DodoSSH.Contracts.RelayConfiguration!
+DodoSSH.Contracts.DodoSshConfiguration.Relay.init -> void
+DodoSSH.Contracts.DodoSshJsonContext
+DodoSSH.Contracts.EncryptedPayload
+DodoSSH.Contracts.EncryptedPayload.$() -> DodoSSH.Contracts.EncryptedPayload!
+DodoSSH.Contracts.EncryptedPayload.AadVersion.get -> byte
+DodoSSH.Contracts.EncryptedPayload.AadVersion.init -> void
+DodoSSH.Contracts.EncryptedPayload.Deconstruct(out byte[]! Envelope, out uint KeyGeneration, out byte AadVersion) -> void
+DodoSSH.Contracts.EncryptedPayload.EncryptedPayload(byte[]! Envelope, uint KeyGeneration, byte AadVersion) -> void
+DodoSSH.Contracts.EncryptedPayload.Envelope.get -> byte[]!
+DodoSSH.Contracts.EncryptedPayload.Envelope.init -> void
+DodoSSH.Contracts.EncryptedPayload.Equals(DodoSSH.Contracts.EncryptedPayload? other) -> bool
+DodoSSH.Contracts.EncryptedPayload.KeyGeneration.get -> uint
+DodoSSH.Contracts.EncryptedPayload.KeyGeneration.init -> void
+DodoSSH.Contracts.EnrollmentRequest
+DodoSSH.Contracts.EnrollmentRequest.$() -> DodoSSH.Contracts.EnrollmentRequest!
+DodoSSH.Contracts.EnrollmentRequest.Deconstruct(out DodoSSH.Contracts.KeyStatement! Statement, out byte[]! StatementSignature, out string! IdentityProviderToken, out byte[]! WrappedPrivateKey, out DodoSSH.Contracts.KdfParameters! KdfParameters, out byte[]? DevicePublicKey, out byte[]? RecoveryWrappedPrivateKey, out DodoSSH.Contracts.KdfParameters? RecoveryKdfParameters) -> void
+DodoSSH.Contracts.EnrollmentRequest.DevicePublicKey.get -> byte[]?
+DodoSSH.Contracts.EnrollmentRequest.DevicePublicKey.init -> void
+DodoSSH.Contracts.EnrollmentRequest.EnrollmentRequest(DodoSSH.Contracts.KeyStatement! Statement, byte[]! StatementSignature, string! IdentityProviderToken, byte[]! WrappedPrivateKey, DodoSSH.Contracts.KdfParameters! KdfParameters, byte[]? DevicePublicKey, byte[]? RecoveryWrappedPrivateKey, DodoSSH.Contracts.KdfParameters? RecoveryKdfParameters) -> void
+DodoSSH.Contracts.EnrollmentRequest.Equals(DodoSSH.Contracts.EnrollmentRequest? other) -> bool
+DodoSSH.Contracts.EnrollmentRequest.IdentityProviderToken.get -> string!
+DodoSSH.Contracts.EnrollmentRequest.IdentityProviderToken.init -> void
+DodoSSH.Contracts.EnrollmentRequest.KdfParameters.get -> DodoSSH.Contracts.KdfParameters!
+DodoSSH.Contracts.EnrollmentRequest.KdfParameters.init -> void
+DodoSSH.Contracts.EnrollmentRequest.RecoveryKdfParameters.get -> DodoSSH.Contracts.KdfParameters?
+DodoSSH.Contracts.EnrollmentRequest.RecoveryKdfParameters.init -> void
+DodoSSH.Contracts.EnrollmentRequest.RecoveryWrappedPrivateKey.get -> byte[]?
+DodoSSH.Contracts.EnrollmentRequest.RecoveryWrappedPrivateKey.init -> void
+DodoSSH.Contracts.EnrollmentRequest.Statement.get -> DodoSSH.Contracts.KeyStatement!
+DodoSSH.Contracts.EnrollmentRequest.Statement.init -> void
+DodoSSH.Contracts.EnrollmentRequest.StatementSignature.get -> byte[]!
+DodoSSH.Contracts.EnrollmentRequest.StatementSignature.init -> void
+DodoSSH.Contracts.EnrollmentRequest.WrappedPrivateKey.get -> byte[]!
+DodoSSH.Contracts.EnrollmentRequest.WrappedPrivateKey.init -> void
+DodoSSH.Contracts.EnrollmentResponse
+DodoSSH.Contracts.EnrollmentResponse.$() -> DodoSSH.Contracts.EnrollmentResponse!
+DodoSSH.Contracts.EnrollmentResponse.Deconstruct(out System.Guid UserId, out int KeyGeneration, out byte[]! Fingerprint, out System.Guid PersonalVaultId, out System.Guid? DeviceId, out long KeyLogSequence) -> void
+DodoSSH.Contracts.EnrollmentResponse.DeviceId.get -> System.Guid?
+DodoSSH.Contracts.EnrollmentResponse.DeviceId.init -> void
+DodoSSH.Contracts.EnrollmentResponse.EnrollmentResponse(System.Guid UserId, int KeyGeneration, byte[]! Fingerprint, System.Guid PersonalVaultId, System.Guid? DeviceId, long KeyLogSequence) -> void
+DodoSSH.Contracts.EnrollmentResponse.Equals(DodoSSH.Contracts.EnrollmentResponse? other) -> bool
+DodoSSH.Contracts.EnrollmentResponse.Fingerprint.get -> byte[]!
+DodoSSH.Contracts.EnrollmentResponse.Fingerprint.init -> void
+DodoSSH.Contracts.EnrollmentResponse.KeyGeneration.get -> int
+DodoSSH.Contracts.EnrollmentResponse.KeyGeneration.init -> void
+DodoSSH.Contracts.EnrollmentResponse.KeyLogSequence.get -> long
+DodoSSH.Contracts.EnrollmentResponse.KeyLogSequence.init -> void
+DodoSSH.Contracts.EnrollmentResponse.PersonalVaultId.get -> System.Guid
+DodoSSH.Contracts.EnrollmentResponse.PersonalVaultId.init -> void
+DodoSSH.Contracts.EnrollmentResponse.UserId.get -> System.Guid
+DodoSSH.Contracts.EnrollmentResponse.UserId.init -> void
+DodoSSH.Contracts.KdfParameters
+DodoSSH.Contracts.KdfParameters.$() -> DodoSSH.Contracts.KdfParameters!
+DodoSSH.Contracts.KdfParameters.Algorithm.get -> string!
+DodoSSH.Contracts.KdfParameters.Algorithm.init -> void
+DodoSSH.Contracts.KdfParameters.Deconstruct(out string! Algorithm, out byte[]! Salt, out int MemoryKibibytes, out int Passes, out int Parallelism) -> void
+DodoSSH.Contracts.KdfParameters.Equals(DodoSSH.Contracts.KdfParameters? other) -> bool
+DodoSSH.Contracts.KdfParameters.KdfParameters(string! Algorithm, byte[]! Salt, int MemoryKibibytes, int Passes, int Parallelism) -> void
+DodoSSH.Contracts.KdfParameters.MemoryKibibytes.get -> int
+DodoSSH.Contracts.KdfParameters.MemoryKibibytes.init -> void
+DodoSSH.Contracts.KdfParameters.Parallelism.get -> int
+DodoSSH.Contracts.KdfParameters.Parallelism.init -> void
+DodoSSH.Contracts.KdfParameters.Passes.get -> int
+DodoSSH.Contracts.KdfParameters.Passes.init -> void
+DodoSSH.Contracts.KdfParameters.Salt.get -> byte[]!
+DodoSSH.Contracts.KdfParameters.Salt.init -> void
+DodoSSH.Contracts.KeyStatement
+DodoSSH.Contracts.KeyStatement.$() -> DodoSSH.Contracts.KeyStatement!
+DodoSSH.Contracts.KeyStatement.CreatedAt.get -> System.DateTimeOffset
+DodoSSH.Contracts.KeyStatement.CreatedAt.init -> void
+DodoSSH.Contracts.KeyStatement.Deconstruct(out int Version, out string! Issuer, out string! Subject, out string? Email, out byte[]! EncryptionPublicKey, out byte[]! SigningPublicKey, out int KeyGeneration, out System.DateTimeOffset CreatedAt, out string! DeviceName) -> void
+DodoSSH.Contracts.KeyStatement.DeviceName.get -> string!
+DodoSSH.Contracts.KeyStatement.DeviceName.init -> void
+DodoSSH.Contracts.KeyStatement.Email.get -> string?
+DodoSSH.Contracts.KeyStatement.Email.init -> void
+DodoSSH.Contracts.KeyStatement.EncryptionPublicKey.get -> byte[]!
+DodoSSH.Contracts.KeyStatement.EncryptionPublicKey.init -> void
+DodoSSH.Contracts.KeyStatement.Equals(DodoSSH.Contracts.KeyStatement? other) -> bool
+DodoSSH.Contracts.KeyStatement.Issuer.get -> string!
+DodoSSH.Contracts.KeyStatement.Issuer.init -> void
+DodoSSH.Contracts.KeyStatement.KeyGeneration.get -> int
+DodoSSH.Contracts.KeyStatement.KeyGeneration.init -> void
+DodoSSH.Contracts.KeyStatement.KeyStatement(int Version, string! Issuer, string! Subject, string? Email, byte[]! EncryptionPublicKey, byte[]! SigningPublicKey, int KeyGeneration, System.DateTimeOffset CreatedAt, string! DeviceName) -> void
+DodoSSH.Contracts.KeyStatement.SigningPublicKey.get -> byte[]!
+DodoSSH.Contracts.KeyStatement.SigningPublicKey.init -> void
+DodoSSH.Contracts.KeyStatement.Subject.get -> string!
+DodoSSH.Contracts.KeyStatement.Subject.init -> void
+DodoSSH.Contracts.KeyStatement.Version.get -> int
+DodoSSH.Contracts.KeyStatement.Version.init -> void
+DodoSSH.Contracts.MeResponse
+DodoSSH.Contracts.MeResponse.$() -> DodoSSH.Contracts.MeResponse!
+DodoSSH.Contracts.MeResponse.Deconstruct(out System.Guid UserId, out string! Issuer, out string! Subject, out string? Email, out string? DisplayName, out bool EnrollmentRequired, out int? KeyGeneration, out byte[]? WrappedPrivateKey, out DodoSSH.Contracts.KdfParameters? KdfParameters, out System.Collections.Generic.IReadOnlyList! Vaults) -> void
+DodoSSH.Contracts.MeResponse.DisplayName.get -> string?
+DodoSSH.Contracts.MeResponse.DisplayName.init -> void
+DodoSSH.Contracts.MeResponse.Email.get -> string?
+DodoSSH.Contracts.MeResponse.Email.init -> void
+DodoSSH.Contracts.MeResponse.EnrollmentRequired.get -> bool
+DodoSSH.Contracts.MeResponse.EnrollmentRequired.init -> void
+DodoSSH.Contracts.MeResponse.Equals(DodoSSH.Contracts.MeResponse? other) -> bool
+DodoSSH.Contracts.MeResponse.Issuer.get -> string!
+DodoSSH.Contracts.MeResponse.Issuer.init -> void
+DodoSSH.Contracts.MeResponse.KdfParameters.get -> DodoSSH.Contracts.KdfParameters?
+DodoSSH.Contracts.MeResponse.KdfParameters.init -> void
+DodoSSH.Contracts.MeResponse.KeyGeneration.get -> int?
+DodoSSH.Contracts.MeResponse.KeyGeneration.init -> void
+DodoSSH.Contracts.MeResponse.MeResponse(System.Guid UserId, string! Issuer, string! Subject, string? Email, string? DisplayName, bool EnrollmentRequired, int? KeyGeneration, byte[]? WrappedPrivateKey, DodoSSH.Contracts.KdfParameters? KdfParameters, System.Collections.Generic.IReadOnlyList! Vaults) -> void
+DodoSSH.Contracts.MeResponse.Subject.get -> string!
+DodoSSH.Contracts.MeResponse.Subject.init -> void
+DodoSSH.Contracts.MeResponse.UserId.get -> System.Guid
+DodoSSH.Contracts.MeResponse.UserId.init -> void
+DodoSSH.Contracts.MeResponse.Vaults.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.MeResponse.Vaults.init -> void
+DodoSSH.Contracts.MeResponse.WrappedPrivateKey.get -> byte[]?
+DodoSSH.Contracts.MeResponse.WrappedPrivateKey.init -> void
+DodoSSH.Contracts.MetaResponse
+DodoSSH.Contracts.MetaResponse.$() -> DodoSSH.Contracts.MetaResponse!
+DodoSSH.Contracts.MetaResponse.ApiVersions.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.MetaResponse.ApiVersions.init -> void
+DodoSSH.Contracts.MetaResponse.CryptoSpecVersion.get -> int
+DodoSSH.Contracts.MetaResponse.CryptoSpecVersion.init -> void
+DodoSSH.Contracts.MetaResponse.Deconstruct(out string! ServerVersion, out System.Collections.Generic.IReadOnlyList! ApiVersions, out int SyncProtocolVersion, out int CryptoSpecVersion, out System.Collections.Generic.IReadOnlyList! Features, out string? MinClientVersion, out int MaxOperationsPerPush, out long MaxPayloadBytes, out long MaxItemPayloadBytes) -> void
+DodoSSH.Contracts.MetaResponse.Equals(DodoSSH.Contracts.MetaResponse? other) -> bool
+DodoSSH.Contracts.MetaResponse.Features.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.MetaResponse.Features.init -> void
+DodoSSH.Contracts.MetaResponse.MaxItemPayloadBytes.get -> long
+DodoSSH.Contracts.MetaResponse.MaxItemPayloadBytes.init -> void
+DodoSSH.Contracts.MetaResponse.MaxOperationsPerPush.get -> int
+DodoSSH.Contracts.MetaResponse.MaxOperationsPerPush.init -> void
+DodoSSH.Contracts.MetaResponse.MaxPayloadBytes.get -> long
+DodoSSH.Contracts.MetaResponse.MaxPayloadBytes.init -> void
+DodoSSH.Contracts.MetaResponse.MetaResponse(string! ServerVersion, System.Collections.Generic.IReadOnlyList! ApiVersions, int SyncProtocolVersion, int CryptoSpecVersion, System.Collections.Generic.IReadOnlyList! Features, string? MinClientVersion, int MaxOperationsPerPush, long MaxPayloadBytes, long MaxItemPayloadBytes) -> void
+DodoSSH.Contracts.MetaResponse.MinClientVersion.get -> string?
+DodoSSH.Contracts.MetaResponse.MinClientVersion.init -> void
+DodoSSH.Contracts.MetaResponse.ServerVersion.get -> string!
+DodoSSH.Contracts.MetaResponse.ServerVersion.init -> void
+DodoSSH.Contracts.MetaResponse.SyncProtocolVersion.get -> int
+DodoSSH.Contracts.MetaResponse.SyncProtocolVersion.init -> void
+DodoSSH.Contracts.OidcConfiguration
+DodoSSH.Contracts.OidcConfiguration.$() -> DodoSSH.Contracts.OidcConfiguration!
+DodoSSH.Contracts.OidcConfiguration.Authority.get -> System.Uri!
+DodoSSH.Contracts.OidcConfiguration.Authority.init -> void
+DodoSSH.Contracts.OidcConfiguration.ClientId.get -> string!
+DodoSSH.Contracts.OidcConfiguration.ClientId.init -> void
+DodoSSH.Contracts.OidcConfiguration.Deconstruct(out System.Uri! Authority, out string! ClientId, out System.Collections.Generic.IReadOnlyList! Scopes, out string! LoopbackRedirectPattern) -> void
+DodoSSH.Contracts.OidcConfiguration.Equals(DodoSSH.Contracts.OidcConfiguration? other) -> bool
+DodoSSH.Contracts.OidcConfiguration.LoopbackRedirectPattern.get -> string!
+DodoSSH.Contracts.OidcConfiguration.LoopbackRedirectPattern.init -> void
+DodoSSH.Contracts.OidcConfiguration.OidcConfiguration(System.Uri! Authority, string! ClientId, System.Collections.Generic.IReadOnlyList! Scopes, string! LoopbackRedirectPattern) -> void
+DodoSSH.Contracts.OidcConfiguration.Scopes.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.OidcConfiguration.Scopes.init -> void
DodoSSH.Contracts.ProblemCodes
+DodoSSH.Contracts.RelayConfiguration
+DodoSSH.Contracts.RelayConfiguration.$() -> DodoSSH.Contracts.RelayConfiguration!
+DodoSSH.Contracts.RelayConfiguration.Deconstruct(out bool Enabled, out System.Uri? WebSocketUrl) -> void
+DodoSSH.Contracts.RelayConfiguration.Enabled.get -> bool
+DodoSSH.Contracts.RelayConfiguration.Enabled.init -> void
+DodoSSH.Contracts.RelayConfiguration.Equals(DodoSSH.Contracts.RelayConfiguration? other) -> bool
+DodoSSH.Contracts.RelayConfiguration.RelayConfiguration(bool Enabled, System.Uri? WebSocketUrl) -> void
+DodoSSH.Contracts.RelayConfiguration.WebSocketUrl.get -> System.Uri?
+DodoSSH.Contracts.RelayConfiguration.WebSocketUrl.init -> void
+DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.ClientClosed = 1 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.DurationLimit = 4 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.Error = 7 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.IdleTimeout = 3 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.ServerShutdown = 5 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.TargetClosed = 2 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.TargetUnreachable = 6 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionCloseReason.Unspecified = 0 -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionSummary
+DodoSSH.Contracts.RelaySessionSummary.$() -> DodoSSH.Contracts.RelaySessionSummary!
+DodoSSH.Contracts.RelaySessionSummary.BytesClientToTarget.get -> long
+DodoSSH.Contracts.RelaySessionSummary.BytesClientToTarget.init -> void
+DodoSSH.Contracts.RelaySessionSummary.BytesTargetToClient.get -> long
+DodoSSH.Contracts.RelaySessionSummary.BytesTargetToClient.init -> void
+DodoSSH.Contracts.RelaySessionSummary.CloseReason.get -> DodoSSH.Contracts.RelaySessionCloseReason
+DodoSSH.Contracts.RelaySessionSummary.CloseReason.init -> void
+DodoSSH.Contracts.RelaySessionSummary.Deconstruct(out System.Guid SessionId, out System.Guid HostId, out string! TargetHost, out int TargetPort, out System.DateTimeOffset StartedAt, out System.DateTimeOffset? EndedAt, out long BytesClientToTarget, out long BytesTargetToClient, out DodoSSH.Contracts.RelaySessionCloseReason CloseReason) -> void
+DodoSSH.Contracts.RelaySessionSummary.EndedAt.get -> System.DateTimeOffset?
+DodoSSH.Contracts.RelaySessionSummary.EndedAt.init -> void
+DodoSSH.Contracts.RelaySessionSummary.Equals(DodoSSH.Contracts.RelaySessionSummary? other) -> bool
+DodoSSH.Contracts.RelaySessionSummary.HostId.get -> System.Guid
+DodoSSH.Contracts.RelaySessionSummary.HostId.init -> void
+DodoSSH.Contracts.RelaySessionSummary.RelaySessionSummary(System.Guid SessionId, System.Guid HostId, string! TargetHost, int TargetPort, System.DateTimeOffset StartedAt, System.DateTimeOffset? EndedAt, long BytesClientToTarget, long BytesTargetToClient, DodoSSH.Contracts.RelaySessionCloseReason CloseReason) -> void
+DodoSSH.Contracts.RelaySessionSummary.SessionId.get -> System.Guid
+DodoSSH.Contracts.RelaySessionSummary.SessionId.init -> void
+DodoSSH.Contracts.RelaySessionSummary.StartedAt.get -> System.DateTimeOffset
+DodoSSH.Contracts.RelaySessionSummary.StartedAt.init -> void
+DodoSSH.Contracts.RelaySessionSummary.TargetHost.get -> string!
+DodoSSH.Contracts.RelaySessionSummary.TargetHost.init -> void
+DodoSSH.Contracts.RelaySessionSummary.TargetPort.get -> int
+DodoSSH.Contracts.RelaySessionSummary.TargetPort.init -> void
+DodoSSH.Contracts.RelayTicketRequest
+DodoSSH.Contracts.RelayTicketRequest.$() -> DodoSSH.Contracts.RelayTicketRequest!
+DodoSSH.Contracts.RelayTicketRequest.Deconstruct(out System.Guid HostId, out System.Guid? PortForwardId) -> void
+DodoSSH.Contracts.RelayTicketRequest.Equals(DodoSSH.Contracts.RelayTicketRequest? other) -> bool
+DodoSSH.Contracts.RelayTicketRequest.HostId.get -> System.Guid
+DodoSSH.Contracts.RelayTicketRequest.HostId.init -> void
+DodoSSH.Contracts.RelayTicketRequest.PortForwardId.get -> System.Guid?
+DodoSSH.Contracts.RelayTicketRequest.PortForwardId.init -> void
+DodoSSH.Contracts.RelayTicketRequest.RelayTicketRequest(System.Guid HostId, System.Guid? PortForwardId) -> void
+DodoSSH.Contracts.RelayTicketResponse
+DodoSSH.Contracts.RelayTicketResponse.$() -> DodoSSH.Contracts.RelayTicketResponse!
+DodoSSH.Contracts.RelayTicketResponse.Deconstruct(out string! Ticket, out System.Uri! WebSocketUrl, out string! SubProtocol, out System.Guid SessionId, out System.DateTimeOffset ExpiresAt) -> void
+DodoSSH.Contracts.RelayTicketResponse.Equals(DodoSSH.Contracts.RelayTicketResponse? other) -> bool
+DodoSSH.Contracts.RelayTicketResponse.ExpiresAt.get -> System.DateTimeOffset
+DodoSSH.Contracts.RelayTicketResponse.ExpiresAt.init -> void
+DodoSSH.Contracts.RelayTicketResponse.RelayTicketResponse(string! Ticket, System.Uri! WebSocketUrl, string! SubProtocol, System.Guid SessionId, System.DateTimeOffset ExpiresAt) -> void
+DodoSSH.Contracts.RelayTicketResponse.SessionId.get -> System.Guid
+DodoSSH.Contracts.RelayTicketResponse.SessionId.init -> void
+DodoSSH.Contracts.RelayTicketResponse.SubProtocol.get -> string!
+DodoSSH.Contracts.RelayTicketResponse.SubProtocol.init -> void
+DodoSSH.Contracts.RelayTicketResponse.Ticket.get -> string!
+DodoSSH.Contracts.RelayTicketResponse.Ticket.init -> void
+DodoSSH.Contracts.RelayTicketResponse.WebSocketUrl.get -> System.Uri!
+DodoSSH.Contracts.RelayTicketResponse.WebSocketUrl.init -> void
+DodoSSH.Contracts.SyncChange
+DodoSSH.Contracts.SyncChange.$() -> DodoSSH.Contracts.SyncChange!
+DodoSSH.Contracts.SyncChange.ChangeSequence.get -> long
+DodoSSH.Contracts.SyncChange.ChangeSequence.init -> void
+DodoSSH.Contracts.SyncChange.Deconstruct(out DodoSSH.Contracts.SyncEntityType EntityType, out System.Guid EntityId, out DodoSSH.Contracts.SyncOperation Operation, out int Version, out long ChangeSequence, out DodoSSH.Contracts.EncryptedPayload? Payload, out DodoSSH.Contracts.SyncPlaintextFields? PlaintextFields, out System.DateTimeOffset UpdatedAt) -> void
+DodoSSH.Contracts.SyncChange.EntityId.get -> System.Guid
+DodoSSH.Contracts.SyncChange.EntityId.init -> void
+DodoSSH.Contracts.SyncChange.EntityType.get -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncChange.EntityType.init -> void
+DodoSSH.Contracts.SyncChange.Equals(DodoSSH.Contracts.SyncChange? other) -> bool
+DodoSSH.Contracts.SyncChange.Operation.get -> DodoSSH.Contracts.SyncOperation
+DodoSSH.Contracts.SyncChange.Operation.init -> void
+DodoSSH.Contracts.SyncChange.Payload.get -> DodoSSH.Contracts.EncryptedPayload?
+DodoSSH.Contracts.SyncChange.Payload.init -> void
+DodoSSH.Contracts.SyncChange.PlaintextFields.get -> DodoSSH.Contracts.SyncPlaintextFields?
+DodoSSH.Contracts.SyncChange.PlaintextFields.init -> void
+DodoSSH.Contracts.SyncChange.SyncChange(DodoSSH.Contracts.SyncEntityType EntityType, System.Guid EntityId, DodoSSH.Contracts.SyncOperation Operation, int Version, long ChangeSequence, DodoSSH.Contracts.EncryptedPayload? Payload, DodoSSH.Contracts.SyncPlaintextFields? PlaintextFields, System.DateTimeOffset UpdatedAt) -> void
+DodoSSH.Contracts.SyncChange.UpdatedAt.get -> System.DateTimeOffset
+DodoSSH.Contracts.SyncChange.UpdatedAt.init -> void
+DodoSSH.Contracts.SyncChange.Version.get -> int
+DodoSSH.Contracts.SyncChange.Version.init -> void
+DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.Credential = 2 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.Host = 1 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.HostCredential = 7 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.HostGroup = 4 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.HostTag = 6 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.KnownHostKey = 10 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.PortForward = 9 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.Snippet = 8 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.SshKey = 3 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.Tag = 5 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncEntityType.Unspecified = 0 -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncOperation
+DodoSSH.Contracts.SyncOperation.Delete = 2 -> DodoSSH.Contracts.SyncOperation
+DodoSSH.Contracts.SyncOperation.Unspecified = 0 -> DodoSSH.Contracts.SyncOperation
+DodoSSH.Contracts.SyncOperation.Upsert = 1 -> DodoSSH.Contracts.SyncOperation
+DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncOperationStatus.Applied = 1 -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncOperationStatus.Conflict = 2 -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncOperationStatus.Duplicate = 5 -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncOperationStatus.Forbidden = 3 -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncOperationStatus.Invalid = 4 -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncOperationStatus.Unspecified = 0 -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncPlaintextFields
+DodoSSH.Contracts.SyncPlaintextFields.$() -> DodoSSH.Contracts.SyncPlaintextFields!
+DodoSSH.Contracts.SyncPlaintextFields.Deconstruct(out bool RelayEnabled, out string? Hostname, out int? Port, out System.Guid? GroupId, out System.Guid? ParentId, out System.Guid? RelatedId, out int? Kind, out string? PublicKeyFingerprint) -> void
+DodoSSH.Contracts.SyncPlaintextFields.Equals(DodoSSH.Contracts.SyncPlaintextFields? other) -> bool
+DodoSSH.Contracts.SyncPlaintextFields.GroupId.get -> System.Guid?
+DodoSSH.Contracts.SyncPlaintextFields.GroupId.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.Hostname.get -> string?
+DodoSSH.Contracts.SyncPlaintextFields.Hostname.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.Kind.get -> int?
+DodoSSH.Contracts.SyncPlaintextFields.Kind.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.ParentId.get -> System.Guid?
+DodoSSH.Contracts.SyncPlaintextFields.ParentId.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.Port.get -> int?
+DodoSSH.Contracts.SyncPlaintextFields.Port.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.PublicKeyFingerprint.get -> string?
+DodoSSH.Contracts.SyncPlaintextFields.PublicKeyFingerprint.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.RelatedId.get -> System.Guid?
+DodoSSH.Contracts.SyncPlaintextFields.RelatedId.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.RelayEnabled.get -> bool
+DodoSSH.Contracts.SyncPlaintextFields.RelayEnabled.init -> void
+DodoSSH.Contracts.SyncPlaintextFields.SyncPlaintextFields(bool RelayEnabled = false, string? Hostname = null, int? Port = null, System.Guid? GroupId = null, System.Guid? ParentId = null, System.Guid? RelatedId = null, int? Kind = null, string? PublicKeyFingerprint = null) -> void
+DodoSSH.Contracts.SyncPullRequest
+DodoSSH.Contracts.SyncPullRequest.$() -> DodoSSH.Contracts.SyncPullRequest!
+DodoSSH.Contracts.SyncPullRequest.Cursor.get -> string?
+DodoSSH.Contracts.SyncPullRequest.Cursor.init -> void
+DodoSSH.Contracts.SyncPullRequest.Deconstruct(out string? Cursor, out int? Limit, out System.Collections.Generic.IReadOnlyList? EntityTypes) -> void
+DodoSSH.Contracts.SyncPullRequest.EntityTypes.get -> System.Collections.Generic.IReadOnlyList?
+DodoSSH.Contracts.SyncPullRequest.EntityTypes.init -> void
+DodoSSH.Contracts.SyncPullRequest.Equals(DodoSSH.Contracts.SyncPullRequest? other) -> bool
+DodoSSH.Contracts.SyncPullRequest.Limit.get -> int?
+DodoSSH.Contracts.SyncPullRequest.Limit.init -> void
+DodoSSH.Contracts.SyncPullRequest.SyncPullRequest(string? Cursor, int? Limit, System.Collections.Generic.IReadOnlyList? EntityTypes) -> void
+DodoSSH.Contracts.SyncPullResponse
+DodoSSH.Contracts.SyncPullResponse.$() -> DodoSSH.Contracts.SyncPullResponse!
+DodoSSH.Contracts.SyncPullResponse.Changes.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.SyncPullResponse.Changes.init -> void
+DodoSSH.Contracts.SyncPullResponse.CurrentKeyGeneration.get -> uint
+DodoSSH.Contracts.SyncPullResponse.CurrentKeyGeneration.init -> void
+DodoSSH.Contracts.SyncPullResponse.Deconstruct(out System.Collections.Generic.IReadOnlyList! Changes, out string! NextCursor, out bool HasMore, out System.DateTimeOffset ServerTime, out uint CurrentKeyGeneration) -> void
+DodoSSH.Contracts.SyncPullResponse.Equals(DodoSSH.Contracts.SyncPullResponse? other) -> bool
+DodoSSH.Contracts.SyncPullResponse.HasMore.get -> bool
+DodoSSH.Contracts.SyncPullResponse.HasMore.init -> void
+DodoSSH.Contracts.SyncPullResponse.NextCursor.get -> string!
+DodoSSH.Contracts.SyncPullResponse.NextCursor.init -> void
+DodoSSH.Contracts.SyncPullResponse.ServerTime.get -> System.DateTimeOffset
+DodoSSH.Contracts.SyncPullResponse.ServerTime.init -> void
+DodoSSH.Contracts.SyncPullResponse.SyncPullResponse(System.Collections.Generic.IReadOnlyList! Changes, string! NextCursor, bool HasMore, System.DateTimeOffset ServerTime, uint CurrentKeyGeneration) -> void
+DodoSSH.Contracts.SyncPushOperation
+DodoSSH.Contracts.SyncPushOperation.$() -> DodoSSH.Contracts.SyncPushOperation!
+DodoSSH.Contracts.SyncPushOperation.Deconstruct(out System.Guid OperationId, out DodoSSH.Contracts.SyncEntityType EntityType, out System.Guid EntityId, out DodoSSH.Contracts.SyncOperation Operation, out int? ExpectedVersion, out DodoSSH.Contracts.EncryptedPayload? Payload, out DodoSSH.Contracts.SyncPlaintextFields? PlaintextFields) -> void
+DodoSSH.Contracts.SyncPushOperation.EntityId.get -> System.Guid
+DodoSSH.Contracts.SyncPushOperation.EntityId.init -> void
+DodoSSH.Contracts.SyncPushOperation.EntityType.get -> DodoSSH.Contracts.SyncEntityType
+DodoSSH.Contracts.SyncPushOperation.EntityType.init -> void
+DodoSSH.Contracts.SyncPushOperation.Equals(DodoSSH.Contracts.SyncPushOperation? other) -> bool
+DodoSSH.Contracts.SyncPushOperation.ExpectedVersion.get -> int?
+DodoSSH.Contracts.SyncPushOperation.ExpectedVersion.init -> void
+DodoSSH.Contracts.SyncPushOperation.Operation.get -> DodoSSH.Contracts.SyncOperation
+DodoSSH.Contracts.SyncPushOperation.Operation.init -> void
+DodoSSH.Contracts.SyncPushOperation.OperationId.get -> System.Guid
+DodoSSH.Contracts.SyncPushOperation.OperationId.init -> void
+DodoSSH.Contracts.SyncPushOperation.Payload.get -> DodoSSH.Contracts.EncryptedPayload?
+DodoSSH.Contracts.SyncPushOperation.Payload.init -> void
+DodoSSH.Contracts.SyncPushOperation.PlaintextFields.get -> DodoSSH.Contracts.SyncPlaintextFields?
+DodoSSH.Contracts.SyncPushOperation.PlaintextFields.init -> void
+DodoSSH.Contracts.SyncPushOperation.SyncPushOperation(System.Guid OperationId, DodoSSH.Contracts.SyncEntityType EntityType, System.Guid EntityId, DodoSSH.Contracts.SyncOperation Operation, int? ExpectedVersion, DodoSSH.Contracts.EncryptedPayload? Payload, DodoSSH.Contracts.SyncPlaintextFields? PlaintextFields) -> void
+DodoSSH.Contracts.SyncPushRequest
+DodoSSH.Contracts.SyncPushRequest.$() -> DodoSSH.Contracts.SyncPushRequest!
+DodoSSH.Contracts.SyncPushRequest.Deconstruct(out System.Collections.Generic.IReadOnlyList! Operations) -> void
+DodoSSH.Contracts.SyncPushRequest.Equals(DodoSSH.Contracts.SyncPushRequest? other) -> bool
+DodoSSH.Contracts.SyncPushRequest.Operations.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.SyncPushRequest.Operations.init -> void
+DodoSSH.Contracts.SyncPushRequest.SyncPushRequest(System.Collections.Generic.IReadOnlyList! Operations) -> void
+DodoSSH.Contracts.SyncPushResponse
+DodoSSH.Contracts.SyncPushResponse.$() -> DodoSSH.Contracts.SyncPushResponse!
+DodoSSH.Contracts.SyncPushResponse.Cursor.get -> string!
+DodoSSH.Contracts.SyncPushResponse.Cursor.init -> void
+DodoSSH.Contracts.SyncPushResponse.Deconstruct(out System.Collections.Generic.IReadOnlyList! Results, out string! Cursor) -> void
+DodoSSH.Contracts.SyncPushResponse.Equals(DodoSSH.Contracts.SyncPushResponse? other) -> bool
+DodoSSH.Contracts.SyncPushResponse.Results.get -> System.Collections.Generic.IReadOnlyList!
+DodoSSH.Contracts.SyncPushResponse.Results.init -> void
+DodoSSH.Contracts.SyncPushResponse.SyncPushResponse(System.Collections.Generic.IReadOnlyList! Results, string! Cursor) -> void
+DodoSSH.Contracts.SyncPushResult
+DodoSSH.Contracts.SyncPushResult.$() -> DodoSSH.Contracts.SyncPushResult!
+DodoSSH.Contracts.SyncPushResult.ChangeSequence.get -> long?
+DodoSSH.Contracts.SyncPushResult.ChangeSequence.init -> void
+DodoSSH.Contracts.SyncPushResult.Deconstruct(out System.Guid OperationId, out DodoSSH.Contracts.SyncOperationStatus Status, out int? Version, out long? ChangeSequence, out DodoSSH.Contracts.SyncChange? ServerEntity, out string? Detail) -> void
+DodoSSH.Contracts.SyncPushResult.Detail.get -> string?
+DodoSSH.Contracts.SyncPushResult.Detail.init -> void
+DodoSSH.Contracts.SyncPushResult.Equals(DodoSSH.Contracts.SyncPushResult? other) -> bool
+DodoSSH.Contracts.SyncPushResult.OperationId.get -> System.Guid
+DodoSSH.Contracts.SyncPushResult.OperationId.init -> void
+DodoSSH.Contracts.SyncPushResult.ServerEntity.get -> DodoSSH.Contracts.SyncChange?
+DodoSSH.Contracts.SyncPushResult.ServerEntity.init -> void
+DodoSSH.Contracts.SyncPushResult.Status.get -> DodoSSH.Contracts.SyncOperationStatus
+DodoSSH.Contracts.SyncPushResult.Status.init -> void
+DodoSSH.Contracts.SyncPushResult.SyncPushResult(System.Guid OperationId, DodoSSH.Contracts.SyncOperationStatus Status, int? Version, long? ChangeSequence, DodoSSH.Contracts.SyncChange? ServerEntity, string? Detail) -> void
+DodoSSH.Contracts.SyncPushResult.Version.get -> int?
+DodoSSH.Contracts.SyncPushResult.Version.init -> void
+DodoSSH.Contracts.VaultSummary
+DodoSSH.Contracts.VaultSummary.$() -> DodoSSH.Contracts.VaultSummary!
+DodoSSH.Contracts.VaultSummary.Deconstruct(out System.Guid VaultId, out string! Name, out bool IsPersonal, out System.Guid? TeamId, out uint KeyGeneration, out int Permissions, out byte[]? WrappedVaultKey, out bool RekeyRequired) -> void
+DodoSSH.Contracts.VaultSummary.Equals(DodoSSH.Contracts.VaultSummary? other) -> bool
+DodoSSH.Contracts.VaultSummary.IsPersonal.get -> bool
+DodoSSH.Contracts.VaultSummary.IsPersonal.init -> void
+DodoSSH.Contracts.VaultSummary.KeyGeneration.get -> uint
+DodoSSH.Contracts.VaultSummary.KeyGeneration.init -> void
+DodoSSH.Contracts.VaultSummary.Name.get -> string!
+DodoSSH.Contracts.VaultSummary.Name.init -> void
+DodoSSH.Contracts.VaultSummary.Permissions.get -> int
+DodoSSH.Contracts.VaultSummary.Permissions.init -> void
+DodoSSH.Contracts.VaultSummary.RekeyRequired.get -> bool
+DodoSSH.Contracts.VaultSummary.RekeyRequired.init -> void
+DodoSSH.Contracts.VaultSummary.TeamId.get -> System.Guid?
+DodoSSH.Contracts.VaultSummary.TeamId.init -> void
+DodoSSH.Contracts.VaultSummary.VaultId.get -> System.Guid
+DodoSSH.Contracts.VaultSummary.VaultId.init -> void
+DodoSSH.Contracts.VaultSummary.VaultSummary(System.Guid VaultId, string! Name, bool IsPersonal, System.Guid? TeamId, uint KeyGeneration, int Permissions, byte[]? WrappedVaultKey, bool RekeyRequired) -> void
+DodoSSH.Contracts.VaultSummary.WrappedVaultKey.get -> byte[]?
+DodoSSH.Contracts.VaultSummary.WrappedVaultKey.init -> void
const DodoSSH.Contracts.ProblemCodes.AlreadyEnrolled = "already-enrolled" -> string!
const DodoSSH.Contracts.ProblemCodes.ClientTooOld = "client-too-old" -> string!
const DodoSSH.Contracts.ProblemCodes.EnrollmentRequired = "enrollment-required" -> string!
@@ -12,3 +422,120 @@ const DodoSSH.Contracts.ProblemCodes.RelayTargetRejected = "relay-target-rejecte
const DodoSSH.Contracts.ProblemCodes.RelayTicketInvalid = "relay-ticket-invalid" -> string!
const DodoSSH.Contracts.ProblemCodes.TypeBaseUri = "https://dodossh.dev/problems/" -> string!
const DodoSSH.Contracts.ProblemCodes.VaultConflict = "vault-conflict" -> string!
+override DodoSSH.Contracts.DirectoryEntry.Equals(object? obj) -> bool
+override DodoSSH.Contracts.DirectoryEntry.GetHashCode() -> int
+override DodoSSH.Contracts.DirectoryEntry.ToString() -> string!
+override DodoSSH.Contracts.DodoSshConfiguration.Equals(object? obj) -> bool
+override DodoSSH.Contracts.DodoSshConfiguration.GetHashCode() -> int
+override DodoSSH.Contracts.DodoSshConfiguration.ToString() -> string!
+override DodoSSH.Contracts.EncryptedPayload.Equals(object? obj) -> bool
+override DodoSSH.Contracts.EncryptedPayload.GetHashCode() -> int
+override DodoSSH.Contracts.EncryptedPayload.ToString() -> string!
+override DodoSSH.Contracts.EnrollmentRequest.Equals(object? obj) -> bool
+override DodoSSH.Contracts.EnrollmentRequest.GetHashCode() -> int
+override DodoSSH.Contracts.EnrollmentRequest.ToString() -> string!
+override DodoSSH.Contracts.EnrollmentResponse.Equals(object? obj) -> bool
+override DodoSSH.Contracts.EnrollmentResponse.GetHashCode() -> int
+override DodoSSH.Contracts.EnrollmentResponse.ToString() -> string!
+override DodoSSH.Contracts.KdfParameters.Equals(object? obj) -> bool
+override DodoSSH.Contracts.KdfParameters.GetHashCode() -> int
+override DodoSSH.Contracts.KdfParameters.ToString() -> string!
+override DodoSSH.Contracts.KeyStatement.Equals(object? obj) -> bool
+override DodoSSH.Contracts.KeyStatement.GetHashCode() -> int
+override DodoSSH.Contracts.KeyStatement.ToString() -> string!
+override DodoSSH.Contracts.MeResponse.Equals(object? obj) -> bool
+override DodoSSH.Contracts.MeResponse.GetHashCode() -> int
+override DodoSSH.Contracts.MeResponse.ToString() -> string!
+override DodoSSH.Contracts.MetaResponse.Equals(object? obj) -> bool
+override DodoSSH.Contracts.MetaResponse.GetHashCode() -> int
+override DodoSSH.Contracts.MetaResponse.ToString() -> string!
+override DodoSSH.Contracts.OidcConfiguration.Equals(object? obj) -> bool
+override DodoSSH.Contracts.OidcConfiguration.GetHashCode() -> int
+override DodoSSH.Contracts.OidcConfiguration.ToString() -> string!
+override DodoSSH.Contracts.RelayConfiguration.Equals(object? obj) -> bool
+override DodoSSH.Contracts.RelayConfiguration.GetHashCode() -> int
+override DodoSSH.Contracts.RelayConfiguration.ToString() -> string!
+override DodoSSH.Contracts.RelaySessionSummary.Equals(object? obj) -> bool
+override DodoSSH.Contracts.RelaySessionSummary.GetHashCode() -> int
+override DodoSSH.Contracts.RelaySessionSummary.ToString() -> string!
+override DodoSSH.Contracts.RelayTicketRequest.Equals(object? obj) -> bool
+override DodoSSH.Contracts.RelayTicketRequest.GetHashCode() -> int
+override DodoSSH.Contracts.RelayTicketRequest.ToString() -> string!
+override DodoSSH.Contracts.RelayTicketResponse.Equals(object? obj) -> bool
+override DodoSSH.Contracts.RelayTicketResponse.GetHashCode() -> int
+override DodoSSH.Contracts.RelayTicketResponse.ToString() -> string!
+override DodoSSH.Contracts.SyncChange.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncChange.GetHashCode() -> int
+override DodoSSH.Contracts.SyncChange.ToString() -> string!
+override DodoSSH.Contracts.SyncPlaintextFields.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPlaintextFields.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPlaintextFields.ToString() -> string!
+override DodoSSH.Contracts.SyncPullRequest.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPullRequest.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPullRequest.ToString() -> string!
+override DodoSSH.Contracts.SyncPullResponse.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPullResponse.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPullResponse.ToString() -> string!
+override DodoSSH.Contracts.SyncPushOperation.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPushOperation.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPushOperation.ToString() -> string!
+override DodoSSH.Contracts.SyncPushRequest.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPushRequest.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPushRequest.ToString() -> string!
+override DodoSSH.Contracts.SyncPushResponse.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPushResponse.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPushResponse.ToString() -> string!
+override DodoSSH.Contracts.SyncPushResult.Equals(object? obj) -> bool
+override DodoSSH.Contracts.SyncPushResult.GetHashCode() -> int
+override DodoSSH.Contracts.SyncPushResult.ToString() -> string!
+override DodoSSH.Contracts.VaultSummary.Equals(object? obj) -> bool
+override DodoSSH.Contracts.VaultSummary.GetHashCode() -> int
+override DodoSSH.Contracts.VaultSummary.ToString() -> string!
+static DodoSSH.Contracts.DirectoryEntry.operator !=(DodoSSH.Contracts.DirectoryEntry? left, DodoSSH.Contracts.DirectoryEntry? right) -> bool
+static DodoSSH.Contracts.DirectoryEntry.operator ==(DodoSSH.Contracts.DirectoryEntry? left, DodoSSH.Contracts.DirectoryEntry? right) -> bool
+static DodoSSH.Contracts.DodoSshConfiguration.operator !=(DodoSSH.Contracts.DodoSshConfiguration? left, DodoSSH.Contracts.DodoSshConfiguration? right) -> bool
+static DodoSSH.Contracts.DodoSshConfiguration.operator ==(DodoSSH.Contracts.DodoSshConfiguration? left, DodoSSH.Contracts.DodoSshConfiguration? right) -> bool
+static DodoSSH.Contracts.DodoSshJsonContext.ResponseOptions.get -> System.Text.Json.JsonSerializerOptions!
+static DodoSSH.Contracts.DodoSshJsonContext.StrictRequestOptions.get -> System.Text.Json.JsonSerializerOptions!
+static DodoSSH.Contracts.EncryptedPayload.operator !=(DodoSSH.Contracts.EncryptedPayload? left, DodoSSH.Contracts.EncryptedPayload? right) -> bool
+static DodoSSH.Contracts.EncryptedPayload.operator ==(DodoSSH.Contracts.EncryptedPayload? left, DodoSSH.Contracts.EncryptedPayload? right) -> bool
+static DodoSSH.Contracts.EnrollmentRequest.operator !=(DodoSSH.Contracts.EnrollmentRequest? left, DodoSSH.Contracts.EnrollmentRequest? right) -> bool
+static DodoSSH.Contracts.EnrollmentRequest.operator ==(DodoSSH.Contracts.EnrollmentRequest? left, DodoSSH.Contracts.EnrollmentRequest? right) -> bool
+static DodoSSH.Contracts.EnrollmentResponse.operator !=(DodoSSH.Contracts.EnrollmentResponse? left, DodoSSH.Contracts.EnrollmentResponse? right) -> bool
+static DodoSSH.Contracts.EnrollmentResponse.operator ==(DodoSSH.Contracts.EnrollmentResponse? left, DodoSSH.Contracts.EnrollmentResponse? right) -> bool
+static DodoSSH.Contracts.KdfParameters.operator !=(DodoSSH.Contracts.KdfParameters? left, DodoSSH.Contracts.KdfParameters? right) -> bool
+static DodoSSH.Contracts.KdfParameters.operator ==(DodoSSH.Contracts.KdfParameters? left, DodoSSH.Contracts.KdfParameters? right) -> bool
+static DodoSSH.Contracts.KeyStatement.operator !=(DodoSSH.Contracts.KeyStatement? left, DodoSSH.Contracts.KeyStatement? right) -> bool
+static DodoSSH.Contracts.KeyStatement.operator ==(DodoSSH.Contracts.KeyStatement? left, DodoSSH.Contracts.KeyStatement? right) -> bool
+static DodoSSH.Contracts.MeResponse.operator !=(DodoSSH.Contracts.MeResponse? left, DodoSSH.Contracts.MeResponse? right) -> bool
+static DodoSSH.Contracts.MeResponse.operator ==(DodoSSH.Contracts.MeResponse? left, DodoSSH.Contracts.MeResponse? right) -> bool
+static DodoSSH.Contracts.MetaResponse.operator !=(DodoSSH.Contracts.MetaResponse? left, DodoSSH.Contracts.MetaResponse? right) -> bool
+static DodoSSH.Contracts.MetaResponse.operator ==(DodoSSH.Contracts.MetaResponse? left, DodoSSH.Contracts.MetaResponse? right) -> bool
+static DodoSSH.Contracts.OidcConfiguration.operator !=(DodoSSH.Contracts.OidcConfiguration? left, DodoSSH.Contracts.OidcConfiguration? right) -> bool
+static DodoSSH.Contracts.OidcConfiguration.operator ==(DodoSSH.Contracts.OidcConfiguration? left, DodoSSH.Contracts.OidcConfiguration? right) -> bool
+static DodoSSH.Contracts.RelayConfiguration.operator !=(DodoSSH.Contracts.RelayConfiguration? left, DodoSSH.Contracts.RelayConfiguration? right) -> bool
+static DodoSSH.Contracts.RelayConfiguration.operator ==(DodoSSH.Contracts.RelayConfiguration? left, DodoSSH.Contracts.RelayConfiguration? right) -> bool
+static DodoSSH.Contracts.RelaySessionSummary.operator !=(DodoSSH.Contracts.RelaySessionSummary? left, DodoSSH.Contracts.RelaySessionSummary? right) -> bool
+static DodoSSH.Contracts.RelaySessionSummary.operator ==(DodoSSH.Contracts.RelaySessionSummary? left, DodoSSH.Contracts.RelaySessionSummary? right) -> bool
+static DodoSSH.Contracts.RelayTicketRequest.operator !=(DodoSSH.Contracts.RelayTicketRequest? left, DodoSSH.Contracts.RelayTicketRequest? right) -> bool
+static DodoSSH.Contracts.RelayTicketRequest.operator ==(DodoSSH.Contracts.RelayTicketRequest? left, DodoSSH.Contracts.RelayTicketRequest? right) -> bool
+static DodoSSH.Contracts.RelayTicketResponse.operator !=(DodoSSH.Contracts.RelayTicketResponse? left, DodoSSH.Contracts.RelayTicketResponse? right) -> bool
+static DodoSSH.Contracts.RelayTicketResponse.operator ==(DodoSSH.Contracts.RelayTicketResponse? left, DodoSSH.Contracts.RelayTicketResponse? right) -> bool
+static DodoSSH.Contracts.SyncChange.operator !=(DodoSSH.Contracts.SyncChange? left, DodoSSH.Contracts.SyncChange? right) -> bool
+static DodoSSH.Contracts.SyncChange.operator ==(DodoSSH.Contracts.SyncChange? left, DodoSSH.Contracts.SyncChange? right) -> bool
+static DodoSSH.Contracts.SyncPlaintextFields.operator !=(DodoSSH.Contracts.SyncPlaintextFields? left, DodoSSH.Contracts.SyncPlaintextFields? right) -> bool
+static DodoSSH.Contracts.SyncPlaintextFields.operator ==(DodoSSH.Contracts.SyncPlaintextFields? left, DodoSSH.Contracts.SyncPlaintextFields? right) -> bool
+static DodoSSH.Contracts.SyncPullRequest.operator !=(DodoSSH.Contracts.SyncPullRequest? left, DodoSSH.Contracts.SyncPullRequest? right) -> bool
+static DodoSSH.Contracts.SyncPullRequest.operator ==(DodoSSH.Contracts.SyncPullRequest? left, DodoSSH.Contracts.SyncPullRequest? right) -> bool
+static DodoSSH.Contracts.SyncPullResponse.operator !=(DodoSSH.Contracts.SyncPullResponse? left, DodoSSH.Contracts.SyncPullResponse? right) -> bool
+static DodoSSH.Contracts.SyncPullResponse.operator ==(DodoSSH.Contracts.SyncPullResponse? left, DodoSSH.Contracts.SyncPullResponse? right) -> bool
+static DodoSSH.Contracts.SyncPushOperation.operator !=(DodoSSH.Contracts.SyncPushOperation? left, DodoSSH.Contracts.SyncPushOperation? right) -> bool
+static DodoSSH.Contracts.SyncPushOperation.operator ==(DodoSSH.Contracts.SyncPushOperation? left, DodoSSH.Contracts.SyncPushOperation? right) -> bool
+static DodoSSH.Contracts.SyncPushRequest.operator !=(DodoSSH.Contracts.SyncPushRequest? left, DodoSSH.Contracts.SyncPushRequest? right) -> bool
+static DodoSSH.Contracts.SyncPushRequest.operator ==(DodoSSH.Contracts.SyncPushRequest? left, DodoSSH.Contracts.SyncPushRequest? right) -> bool
+static DodoSSH.Contracts.SyncPushResponse.operator !=(DodoSSH.Contracts.SyncPushResponse? left, DodoSSH.Contracts.SyncPushResponse? right) -> bool
+static DodoSSH.Contracts.SyncPushResponse.operator ==(DodoSSH.Contracts.SyncPushResponse? left, DodoSSH.Contracts.SyncPushResponse? right) -> bool
+static DodoSSH.Contracts.SyncPushResult.operator !=(DodoSSH.Contracts.SyncPushResult? left, DodoSSH.Contracts.SyncPushResult? right) -> bool
+static DodoSSH.Contracts.SyncPushResult.operator ==(DodoSSH.Contracts.SyncPushResult? left, DodoSSH.Contracts.SyncPushResult? right) -> bool
+static DodoSSH.Contracts.VaultSummary.operator !=(DodoSSH.Contracts.VaultSummary? left, DodoSSH.Contracts.VaultSummary? right) -> bool
+static DodoSSH.Contracts.VaultSummary.operator ==(DodoSSH.Contracts.VaultSummary? left, DodoSSH.Contracts.VaultSummary? right) -> bool
diff --git a/src/DodoSSH.Contracts/Relay.cs b/src/DodoSSH.Contracts/Relay.cs
new file mode 100644
index 0000000..58da72e
--- /dev/null
+++ b/src/DodoSSH.Contracts/Relay.cs
@@ -0,0 +1,99 @@
+namespace DodoSSH.Contracts;
+
+///
+/// A request for permission to relay to a host.
+///
+///
+/// There is deliberately no address field. The server resolves the target from
+/// , which must belong to a vault the caller holds Connect on and must have
+/// relay enabled. Accepting a client-supplied address would turn the relay into an
+/// authenticated open TCP proxy into the operator's network; see ADR 0004.
+///
+/// The host to reach.
+///
+/// A configured port forward to use instead of the host's own SSH port. Resolved server-side the
+/// same way.
+///
+public sealed record RelayTicketRequest(
+ Guid HostId,
+ Guid? PortForwardId);
+
+///
+/// A short-lived, single-use ticket authorising exactly one relay connection.
+///
+///
+/// The ticket grants no API access at all, which is why the WebSocket endpoint can accept it
+/// alone. It is also the extraction seam: a standalone relay process needs only the data
+/// protection key ring and the replay-guard table, no authorization code.
+///
+///
+/// Opaque token. Sent as the ticket.<token> element of the
+/// Sec-WebSocket-Protocol header, because constrained WebSocket clients cannot set
+/// arbitrary headers.
+///
+/// Absolute URL of the relay endpoint.
+/// Required WebSocket subprotocol.
+/// Correlates the ticket with the resulting session record.
+/// Expiry, at most 30 seconds out.
+public sealed record RelayTicketResponse(
+ string Ticket,
+ Uri WebSocketUrl,
+ string SubProtocol,
+ Guid SessionId,
+ DateTimeOffset ExpiresAt);
+
+/// Why a relay session ended.
+public enum RelaySessionCloseReason
+{
+ /// Not a legal value.
+ Unspecified = 0,
+
+ /// The client closed the connection.
+ ClientClosed = 1,
+
+ /// The target closed the connection.
+ TargetClosed = 2,
+
+ /// No traffic within the idle timeout.
+ IdleTimeout = 3,
+
+ /// The maximum session duration was reached.
+ DurationLimit = 4,
+
+ /// The server is shutting down and drained the session.
+ ServerShutdown = 5,
+
+ /// The connection to the target failed.
+ TargetUnreachable = 6,
+
+ /// An error ended the session.
+ Error = 7,
+}
+
+///
+/// An audit record of one relay session.
+///
+///
+/// Byte counts and duration are recorded; content never is. The relay forwards SSH ciphertext,
+/// so session recording is impossible in this mode by construction — the compliance strength
+/// and the limitation are the same fact.
+///
+/// The session.
+/// Host that was reached.
+/// Resolved target hostname.
+/// Resolved target port.
+/// When the session opened.
+/// When it closed, or null while still open.
+/// Bytes forwarded from client to target.
+/// Bytes forwarded from target to client.
+/// Why it ended.
+public sealed record RelaySessionSummary(
+ Guid SessionId,
+ Guid HostId,
+ string TargetHost,
+ int TargetPort,
+ DateTimeOffset StartedAt,
+ DateTimeOffset? EndedAt,
+ long BytesClientToTarget,
+ long BytesTargetToClient,
+ RelaySessionCloseReason CloseReason);
diff --git a/src/DodoSSH.Contracts/Sync.cs b/src/DodoSSH.Contracts/Sync.cs
new file mode 100644
index 0000000..715145f
--- /dev/null
+++ b/src/DodoSSH.Contracts/Sync.cs
@@ -0,0 +1,148 @@
+namespace DodoSSH.Contracts;
+
+///
+/// Plaintext columns that the server needs in order to function, supplied alongside a payload.
+///
+///
+///
+/// Kept deliberately minimal. There is no plaintext label or name: ACL administration happens
+/// in the client, which can decrypt, so the server never needs a searchable title.
+///
+///
+/// and may only be set when
+/// is true, and the database enforces that with a CHECK constraint. The relay must resolve its
+/// target server-side or it becomes an authenticated open TCP proxy into the operator's own
+/// network; see ADR 0004. Everything else about a host — username, notes, jump chain, options —
+/// stays inside the encrypted payload.
+///
+///
+/// Whether this host may be dialled through the server relay.
+/// Target hostname. Permitted only when relay is enabled.
+/// Target port. Permitted only when relay is enabled.
+/// Owning group, for tree placement.
+/// Parent entity, for associations and nested groups.
+/// The second side of an association row.
+/// Discriminator for entity types that have one, such as credential kind.
+///
+/// Fingerprint of a public key. Deliberately plaintext: public keys are not secret, and this
+/// enables "which hosts trust this key" without weakening the threat model.
+///
+public sealed record SyncPlaintextFields(
+ bool RelayEnabled = false,
+ string? Hostname = null,
+ int? Port = null,
+ Guid? GroupId = null,
+ Guid? ParentId = null,
+ Guid? RelatedId = null,
+ int? Kind = null,
+ string? PublicKeyFingerprint = null);
+
+///
+/// One change a client wants to apply.
+///
+///
+/// Client-generated idempotency key for this single operation. A retried push with the same id
+/// returns rather than applying twice.
+///
+/// Kind of item.
+///
+/// Identifier, generated by the client with UUIDv7 so items can be created offline.
+///
+/// Upsert or delete.
+///
+/// The version the client believes the server holds; means create. A
+/// mismatch yields — never last-writer-wins.
+///
+/// Ciphertext. Required for an upsert, omitted for a delete.
+/// Plaintext columns the server needs.
+public sealed record SyncPushOperation(
+ Guid OperationId,
+ SyncEntityType EntityType,
+ Guid EntityId,
+ SyncOperation Operation,
+ int? ExpectedVersion,
+ EncryptedPayload? Payload,
+ SyncPlaintextFields? PlaintextFields);
+
+/// A batch of changes to apply to one vault.
+///
+/// The batch. Capped by the server; see .
+///
+public sealed record SyncPushRequest(IReadOnlyList Operations);
+
+/// Outcome of one operation in a push.
+/// Echoes the request's operation id.
+/// What happened.
+/// The stored version after a successful apply.
+/// The change-log sequence assigned, for cursor comparison.
+///
+/// The server's current state, present only on so
+/// the client can perform a three-way merge and re-push.
+///
+/// Human-readable explanation for an invalid operation. Never a secret.
+public sealed record SyncPushResult(
+ Guid OperationId,
+ SyncOperationStatus Status,
+ int? Version,
+ long? ChangeSequence,
+ SyncChange? ServerEntity,
+ string? Detail);
+
+/// Per-operation outcomes for a push.
+/// One entry per submitted operation, in request order.
+///
+/// A cursor positioned after every change this push produced, so the client can continue
+/// pulling without re-reading its own writes.
+///
+public sealed record SyncPushResponse(
+ IReadOnlyList Results,
+ string Cursor);
+
+/// A request for changes since a cursor.
+///
+/// An opaque, integrity-tagged cursor from a previous response, or to
+/// start from the beginning. Clients must never construct or modify one.
+///
+/// Maximum changes to return. The server clamps this.
+/// Optional filter. Empty or null means all types.
+public sealed record SyncPullRequest(
+ string? Cursor,
+ int? Limit,
+ IReadOnlyList? EntityTypes);
+
+/// One change as returned by a pull.
+/// Kind of item.
+/// Identifier.
+/// Upsert or delete. A delete carries no payload.
+/// Version after the change.
+/// Position in the vault's change log.
+/// Ciphertext, absent for a delete.
+/// Plaintext columns, absent for a delete.
+/// When the change was recorded.
+public sealed record SyncChange(
+ SyncEntityType EntityType,
+ Guid EntityId,
+ SyncOperation Operation,
+ int Version,
+ long ChangeSequence,
+ EncryptedPayload? Payload,
+ SyncPlaintextFields? PlaintextFields,
+ DateTimeOffset UpdatedAt);
+
+/// A page of changes.
+/// Changes in ascending change-sequence order.
+/// Cursor to pass to the following pull.
+/// Whether more changes are immediately available.
+///
+/// The server's clock, so a client can detect its own skew rather than mis-ordering local edits.
+///
+///
+/// The vault's current key generation. A client seeing a generation ahead of its own knows a
+/// rekey happened and that it must fetch new grants.
+///
+public sealed record SyncPullResponse(
+ IReadOnlyList Changes,
+ string NextCursor,
+ bool HasMore,
+ DateTimeOffset ServerTime,
+ uint CurrentKeyGeneration);
diff --git a/src/DodoSSH.Contracts/SyncEntityType.cs b/src/DodoSSH.Contracts/SyncEntityType.cs
new file mode 100644
index 0000000..adb5bf8
--- /dev/null
+++ b/src/DodoSSH.Contracts/SyncEntityType.cs
@@ -0,0 +1,45 @@
+namespace DodoSSH.Contracts;
+
+///
+/// The kind of vault item a sync change refers to.
+///
+///
+/// Values are persisted in the change log and are part of the wire contract. Append only;
+/// never renumber. These mirror CryptoSpec.AadResourceType for the item types, but are
+/// a separate enum because only syncable items appear here.
+///
+public enum SyncEntityType
+{
+ /// Not a legal value.
+ Unspecified = 0,
+
+ /// An SSH host.
+ Host = 1,
+
+ /// A credential.
+ Credential = 2,
+
+ /// An SSH key pair.
+ SshKey = 3,
+
+ /// A host group.
+ HostGroup = 4,
+
+ /// A tag.
+ Tag = 5,
+
+ /// A host-to-tag association.
+ HostTag = 6,
+
+ /// A host-to-credential association.
+ HostCredential = 7,
+
+ /// A command snippet.
+ Snippet = 8,
+
+ /// A port forward.
+ PortForward = 9,
+
+ /// A known SSH host key.
+ KnownHostKey = 10,
+}
diff --git a/src/DodoSSH.Contracts/SyncOperation.cs b/src/DodoSSH.Contracts/SyncOperation.cs
new file mode 100644
index 0000000..0d14645
--- /dev/null
+++ b/src/DodoSSH.Contracts/SyncOperation.cs
@@ -0,0 +1,21 @@
+namespace DodoSSH.Contracts;
+
+///
+/// What a sync change did to an entity.
+///
+///
+/// There is no hard delete. A delete is a revisioned tombstone, because a client that has been
+/// offline must be able to learn that an item went away; a row that simply vanished would be
+/// indistinguishable from one the client had never seen.
+///
+public enum SyncOperation
+{
+ /// Not a legal value.
+ Unspecified = 0,
+
+ /// The entity was created or modified.
+ Upsert = 1,
+
+ /// The entity was soft-deleted, leaving a tombstone.
+ Delete = 2,
+}
diff --git a/src/DodoSSH.Contracts/SyncOperationStatus.cs b/src/DodoSSH.Contracts/SyncOperationStatus.cs
new file mode 100644
index 0000000..51d3676
--- /dev/null
+++ b/src/DodoSSH.Contracts/SyncOperationStatus.cs
@@ -0,0 +1,37 @@
+namespace DodoSSH.Contracts;
+
+///
+/// Outcome of a single operation within a sync push.
+///
+///
+/// A push returns HTTP 200 even when some operations fail, with one of these per operation.
+/// Conflicting operations are skipped rather than aborting the batch, so one stale item cannot
+/// block every other change a client has queued while offline.
+///
+public enum SyncOperationStatus
+{
+ /// Not a legal value.
+ Unspecified = 0,
+
+ /// Applied. The response carries the new version and change sequence.
+ Applied = 1,
+
+ ///
+ /// The expected version did not match. The response carries the server's current entity so
+ /// the client can merge and re-push; the server never merges, because it cannot read the
+ /// payload.
+ ///
+ Conflict = 2,
+
+ /// The caller lacks permission for this entity.
+ Forbidden = 3,
+
+ /// Structurally invalid: unknown entity type, malformed envelope, missing field.
+ Invalid = 4,
+
+ ///
+ /// This operation id was already applied. Retries are therefore exactly-once at operation
+ /// granularity rather than merely at batch granularity.
+ ///
+ Duplicate = 5,
+}
diff --git a/tests/DodoSSH.Contracts.Tests/SerializationTests.cs b/tests/DodoSSH.Contracts.Tests/SerializationTests.cs
new file mode 100644
index 0000000..acefac8
--- /dev/null
+++ b/tests/DodoSSH.Contracts.Tests/SerializationTests.cs
@@ -0,0 +1,215 @@
+using System.Text.Json;
+using DodoSSH.Contracts;
+
+namespace DodoSSH.Contracts.Tests;
+
+///
+/// Wire-format behaviour of the shared contracts.
+///
+///
+/// These assert the properties the client depends on and that a compile-time reference cannot
+/// guarantee: camelCase naming, enums as strings, base64 for binary, and strict rejection of
+/// unmapped members on inbound requests.
+///
+public sealed class SerializationTests
+{
+ // Deliberately the shared options, not a hand-rolled instance: constructing options
+ // separately is exactly the mistake these contracts must not permit.
+ private static JsonSerializerOptions Options => DodoSshJsonContext.ResponseOptions;
+
+ [Fact]
+ public void Properties_AreCamelCase()
+ {
+ var payload = new EncryptedPayload([1, 2, 3], KeyGeneration: 4, AadVersion: 1);
+
+ var json = JsonSerializer.Serialize(payload, Options);
+
+ json.ShouldContain("\"envelope\"");
+ json.ShouldContain("\"keyGeneration\"");
+ json.ShouldContain("\"aadVersion\"");
+ }
+
+ [Fact]
+ public void ByteArrays_AreBase64()
+ {
+ var payload = new EncryptedPayload([0xDE, 0xAD, 0xBE, 0xEF], 1, 1);
+
+ var json = JsonSerializer.Serialize(payload, Options);
+
+ json.ShouldContain(Convert.ToBase64String([0xDE, 0xAD, 0xBE, 0xEF]));
+ }
+
+ [Fact]
+ public void Enums_AreSerialisedAsStrings()
+ {
+ // Integers across the wire would make a reordered enum silently reinterpret data, and
+ // would make captured payloads unreadable without the matching build.
+ var change = new SyncChange(
+ SyncEntityType.Host,
+ Guid.CreateVersion7(),
+ SyncOperation.Upsert,
+ Version: 1,
+ ChangeSequence: 42,
+ Payload: null,
+ PlaintextFields: null,
+ UpdatedAt: DateTimeOffset.UnixEpoch);
+
+ var json = JsonSerializer.Serialize(change, Options);
+
+ json.ShouldContain("\"Host\"");
+ json.ShouldContain("\"Upsert\"");
+ json.ShouldNotContain("\"entityType\":1");
+ }
+
+ [Fact]
+ public void EncryptedPayload_RoundTrips()
+ {
+ var original = new EncryptedPayload([9, 8, 7, 6, 5], 12, 1);
+
+ var restored = JsonSerializer.Deserialize(
+ JsonSerializer.Serialize(original, Options), Options);
+
+ restored.ShouldNotBeNull();
+ restored.Envelope.ShouldBe(original.Envelope);
+ restored.KeyGeneration.ShouldBe(original.KeyGeneration);
+ restored.AadVersion.ShouldBe(original.AadVersion);
+ }
+
+ [Fact]
+ public void SyncPushRequest_RoundTripsWithAllFields()
+ {
+ var original = new SyncPushRequest(
+ [
+ new SyncPushOperation(
+ Guid.CreateVersion7(),
+ SyncEntityType.Host,
+ Guid.CreateVersion7(),
+ SyncOperation.Upsert,
+ ExpectedVersion: 3,
+ Payload: new EncryptedPayload([1, 2, 3], 2, 1),
+ PlaintextFields: new SyncPlaintextFields(
+ RelayEnabled: true,
+ Hostname: "bastion.internal",
+ Port: 22)),
+ new SyncPushOperation(
+ Guid.CreateVersion7(),
+ SyncEntityType.Credential,
+ Guid.CreateVersion7(),
+ SyncOperation.Delete,
+ ExpectedVersion: 7,
+ Payload: null,
+ PlaintextFields: null),
+ ]);
+
+ var restored = JsonSerializer.Deserialize(
+ JsonSerializer.Serialize(original, Options), Options);
+
+ restored.ShouldNotBeNull();
+ restored.Operations.Count.ShouldBe(2);
+ restored.Operations[0].PlaintextFields!.Hostname.ShouldBe("bastion.internal");
+ restored.Operations[0].PlaintextFields!.RelayEnabled.ShouldBeTrue();
+ restored.Operations[1].Payload.ShouldBeNull();
+ restored.Operations[1].Operation.ShouldBe(SyncOperation.Delete);
+ }
+
+ [Fact]
+ public void NullFields_AreOmitted()
+ {
+ var request = new SyncPullRequest(Cursor: null, Limit: null, EntityTypes: null);
+
+ var json = JsonSerializer.Serialize(request, Options);
+
+ json.ShouldNotContain("cursor");
+ json.ShouldNotContain("limit");
+ }
+
+ [Fact]
+ public void StrictRequestOptions_RejectUnmappedMembers()
+ {
+ // A renamed or misspelled client property must surface as a 400, not as a silently
+ // missing value that later looks like data loss.
+ const string Json = """
+ {"cursor":null,"limit":50,"entityTypes":null,"unexpectedProperty":"surprise"}
+ """;
+
+ Should.Throw(() =>
+ JsonSerializer.Deserialize(Json, DodoSshJsonContext.StrictRequestOptions));
+ }
+
+ [Fact]
+ public void StrictRequestOptions_AcceptAWellFormedRequest()
+ {
+ const string Json = """
+ {"cursor":"abc","limit":50}
+ """;
+
+ var request = JsonSerializer.Deserialize(
+ Json, DodoSshJsonContext.StrictRequestOptions);
+
+ request.ShouldNotBeNull();
+ request.Cursor.ShouldBe("abc");
+ request.Limit.ShouldBe(50);
+ }
+
+ [Fact]
+ public void ResponseOptions_TolerateUnknownMembers()
+ {
+ // Forward compatibility: an older client must still read a newer server's response
+ // rather than failing on a field it does not know about.
+ const string Json = """
+ {"envelope":"AQID","keyGeneration":1,"aadVersion":1,"futureField":true}
+ """;
+
+ var payload = JsonSerializer.Deserialize(Json, Options);
+
+ payload.ShouldNotBeNull();
+ payload.KeyGeneration.ShouldBe(1u);
+ }
+
+ [Fact]
+ public void NumberHandling_IsStrict()
+ {
+ // A quoted number would let a sloppy client send "1" where 1 is meant, which then
+ // diverges between implementations.
+ const string Json = """
+ {"envelope":"AQID","keyGeneration":"1","aadVersion":1}
+ """;
+
+ Should.Throw(() => JsonSerializer.Deserialize(Json, Options));
+ }
+
+ [Fact]
+ public void MetaResponse_RoundTrips()
+ {
+ var original = new MetaResponse(
+ ServerVersion: "0.1.0",
+ ApiVersions: [1],
+ SyncProtocolVersion: 1,
+ CryptoSpecVersion: 1,
+ Features: ["relay", "teams"],
+ MinClientVersion: "0.1.0",
+ MaxOperationsPerPush: 500,
+ MaxPayloadBytes: 8 * 1024 * 1024,
+ MaxItemPayloadBytes: 256 * 1024);
+
+ var restored = JsonSerializer.Deserialize(
+ JsonSerializer.Serialize(original, Options), Options);
+
+ restored.ShouldNotBeNull();
+ restored.Features.ShouldBe(["relay", "teams"]);
+ restored.MaxPayloadBytes.ShouldBe(8 * 1024 * 1024);
+ }
+
+ [Fact]
+ public void RelayTicketRequest_HasNoAddressField()
+ {
+ // Structural guard on ADR 0004: if a client could name its own target, the relay
+ // would become an authenticated open TCP proxy into the operator's network.
+ var properties = typeof(RelayTicketRequest)
+ .GetProperties()
+ .Select(p => p.Name)
+ .ToList();
+
+ properties.ShouldBe(["HostId", "PortForwardId"], ignoreOrder: true);
+ }
+}