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); + } +}