Merge branch 'main' into claude/host-management-ui-plan-7f20ab

Seven files needed a hand. Most were two branches adding something in the same
place, but three were one branch changing what the other had moved or renamed,
and those are the ones worth reading.

The shell keeps both new fields and both constructor lines: the connection
recorder this branch built and the teams view model main did. Where main put a
teams load inside OnScreenChanged, it now sits beside the logs refresh rather
than inside RaiseSurfaceState — this branch extracted that notification block
and it is called from two properties, so a screen-specific side effect in there
would fire on every terminal switch as well.

Main gave four row types a vault id and a vault name, and this branch had moved
one of them — KnownHostRowViewModel — into its own file when the pinned keys
became a screen. Git resolved that as "deleted here, modified there" and took
the delete, which compiles as long as nobody looks: the moved copy still had
the two-argument constructor and the call site had grown to four. Carried over
by hand, along with the ordering the pins list now does on them.

The status line's quiet rule was the subtle one. Main extracted it into
IsWorthReporting; this branch had changed the same condition to read item
counts rather than raw ones, because every user action queues a log entry a
moment later and this machine reads its own entries back on the next pull. Take
main's structure and the merge builds, passes, and silently restores a bug this
branch existed partly to fix — every save's message overwritten a second after
it appears. The method now reads PulledItems and PushedItems, with the reason
in its remarks.

Two conflicts were prose that had gone stale rather than code. The keychain
screen's comment said team vaults are refused by the server's access service,
which was true when it was written and is not now; main's replacement stands,
in this branch's vocabulary. The design-gaps row for groups was claimed by both
— real host groups here, per-vault headings there — and they are different
things, so both rows stay and the difference is stated: a group is a shelf the
user chose, a vault is who can read the item.

One defect the tests found and the compiler could not. Generating a key opens
the same editor as pasting one, but not through NewKey — so it never set the
target vault main added, and a generated key was filed into whatever vault was
edited last, or none. Both key-generation tests failed on it. Fixed where the
editor opens, with the reason recorded there.

One gap is left deliberately and is written down rather than half-built. Hosts,
keys, credentials and pins are read across every vault this session holds a key
for; groups are read from the active vault alone, so a host a teammate filed
shows under UNGROUPED. Nothing is lost or misfiled — it is what the sidebar
already shows for a group that has been deleted — but closing it needs a vault
id on every group row for rename and delete, and a way to tell two vaults'
identically-named groups apart under a layout with one heading per group. Both
are worth doing and neither is a merge's business. It is in the remarks on
ReloadGroupsAsync and in docs/design-import-gaps.md.

dotnet build, dotnet test and dotnet format --verify-no-changes are all clean:
1282 tests, including the end-to-end suite against real containers.
This commit is contained in:
2026-07-31 20:44:39 +02:00
49 changed files with 6889 additions and 149 deletions
@@ -40,6 +40,17 @@ namespace DodoSSH.Contracts;
[JsonSerializable(typeof(DirectoryEntry))]
[JsonSerializable(typeof(IReadOnlyList<DirectoryEntry>))]
[JsonSerializable(typeof(VaultSummary))]
[JsonSerializable(typeof(TeamSummary))]
[JsonSerializable(typeof(IReadOnlyList<TeamSummary>))]
[JsonSerializable(typeof(CreateTeamRequest))]
[JsonSerializable(typeof(TeamMemberSummary))]
[JsonSerializable(typeof(IReadOnlyList<TeamMemberSummary>))]
[JsonSerializable(typeof(AddTeamMemberRequest))]
[JsonSerializable(typeof(ChangeTeamMemberRoleRequest))]
[JsonSerializable(typeof(CreateTeamVaultRequest))]
[JsonSerializable(typeof(IssueVaultGrantRequest))]
[JsonSerializable(typeof(VaultGrantsResponse))]
[JsonSerializable(typeof(KeyLogPage))]
[JsonSerializable(typeof(SyncPullRequest))]
[JsonSerializable(typeof(SyncPullResponse))]
[JsonSerializable(typeof(SyncPushRequest))]
+64
View File
@@ -0,0 +1,64 @@
namespace DodoSSH.Contracts;
/// <summary>
/// One entry in the append-only log of every identity key statement ever published.
/// </summary>
/// <remarks>
/// <para>
/// Served so a client can recompute the chain for itself rather than taking the directory's word for a
/// public key. Every field the hash covers is here, in the order docs/crypto.md §7.2 hashes them, so
/// verification is <c>ComputeEntryHash(previous, …) == hash</c> and nothing else.
/// </para>
/// <para>
/// <see cref="Sequence"/> is deliberately <em>not</em> an input to the hash. It is assigned by the
/// database on insert, so a renumbered or gapped column cannot silently reorder history — order
/// follows the hash links. It is here to page with, and to compare against what a directory entry
/// claims.
/// </para>
/// </remarks>
/// <param name="Sequence">Monotonic position in the log.</param>
/// <param name="UserId">Whose key this is.</param>
/// <param name="Generation">Which generation of that user's key.</param>
/// <param name="EncryptionPublicKey">X25519 public key, 32 bytes.</param>
/// <param name="SigningPublicKey">Ed25519 public key, 32 bytes.</param>
/// <param name="StatementSignature">The self-signature over the key statement.</param>
/// <param name="PreviousHash">Hash of the preceding entry; all-zero for the first.</param>
/// <param name="Hash">This entry's hash, over the previous hash and its own contents.</param>
/// <param name="CreatedAt">When it was appended, truncated to milliseconds as the hash requires.</param>
public sealed record KeyLogRecord(
long Sequence,
Guid UserId,
int Generation,
byte[] EncryptionPublicKey,
byte[] SigningPublicKey,
byte[] StatementSignature,
byte[] PreviousHash,
byte[] Hash,
DateTimeOffset CreatedAt);
/// <summary>A page of the key log, with the head as of this response.</summary>
/// <remarks>
/// <para>
/// The head is what a client records in every grant it signs, which is what makes a forked view
/// detectable: for a server to show two clients different key logs it must keep both forks consistent
/// across every later shared operation, and any two clients touching one vault then disagree. It
/// converts an otherwise invisible key substitution into a visible one. It does not prevent it — see
/// ADR 0001.
/// </para>
/// <para>
/// <see cref="Head"/> describes the whole log, not this page: a client that pages from an old cursor
/// still learns where the end is, and can tell whether it has caught up without a second call.
/// </para>
/// </remarks>
/// <param name="Entries">Entries after the requested sequence, in ascending order.</param>
/// <param name="HeadSequence">Sequence of the log's last entry; 0 when the log is empty.</param>
/// <param name="Head">
/// Hash of the log's last entry. All-zero when the log is empty, which is the same value the first
/// entry records as its predecessor.
/// </param>
/// <param name="HasMore">Whether entries beyond this page are immediately available.</param>
public sealed record KeyLogPage(
IReadOnlyList<KeyLogRecord> Entries,
long HeadSequence,
byte[] Head,
bool HasMore);
+36
View File
@@ -80,4 +80,40 @@ public static class ProblemCodes
/// <summary>A push batch exceeded the operation count or payload size cap.</summary>
public const string PushBatchTooLarge = "push-batch-too-large";
/// <summary>
/// A team create or membership change was structurally invalid: a blank name, a slug that is
/// not URL-safe, an unknown role, or an account that does not exist here.
/// </summary>
public const string InvalidTeam = "invalid-team";
/// <summary>
/// The requested slug is already in use.
/// </summary>
/// <remarks>
/// Its own code rather than folded into <see cref="InvalidTeam"/>, because it is the one create
/// failure the caller could not have predicted from their own input and the only one whose
/// remedy is "pick a different one" rather than "fix what you typed".
/// </remarks>
public const string TeamSlugTaken = "team-slug-taken";
/// <summary>
/// The change would leave a team with no owner.
/// </summary>
/// <remarks>
/// Refused rather than allowed, because a team with no owner has nobody who can appoint one —
/// and the only route back would be an operator editing the database by hand.
/// </remarks>
public const string LastTeamOwner = "last-team-owner";
/// <summary>
/// A vault key grant was rejected: a fingerprint or wrap of the wrong size, a generation that is
/// not the vault's current one, or a recipient who cannot reach the vault in the first place.
/// </summary>
/// <remarks>
/// Never a statement about the wrapped key's <em>contents</em>. The server cannot open it, so a
/// grant containing garbage is accepted here and surfaces at the recipient as a tag failure,
/// with the signature naming who issued it. See docs/crypto.md §6.
/// </remarks>
public const string InvalidVaultGrant = "invalid-vault-grant";
}
@@ -8,13 +8,61 @@ const DodoSSH.Contracts.ProblemCodes.IdentityBindingInvalid = "identity-binding-
const DodoSSH.Contracts.ProblemCodes.InvalidCursor = "invalid-cursor" -> string!
const DodoSSH.Contracts.ProblemCodes.InvalidDeviceRegistration = "invalid-device-registration" -> string!
const DodoSSH.Contracts.ProblemCodes.InvalidEnrollment = "invalid-enrollment" -> string!
const DodoSSH.Contracts.ProblemCodes.InvalidTeam = "invalid-team" -> string!
const DodoSSH.Contracts.ProblemCodes.InvalidVaultGrant = "invalid-vault-grant" -> string!
const DodoSSH.Contracts.ProblemCodes.LastTeamOwner = "last-team-owner" -> string!
const DodoSSH.Contracts.ProblemCodes.MalformedRequest = "malformed-request" -> string!
const DodoSSH.Contracts.ProblemCodes.PushBatchTooLarge = "push-batch-too-large" -> string!
const DodoSSH.Contracts.ProblemCodes.RelayLimitReached = "relay-limit-reached" -> string!
const DodoSSH.Contracts.ProblemCodes.RelayTargetRejected = "relay-target-rejected" -> string!
const DodoSSH.Contracts.ProblemCodes.RelayTicketInvalid = "relay-ticket-invalid" -> string!
const DodoSSH.Contracts.ProblemCodes.TeamSlugTaken = "team-slug-taken" -> string!
const DodoSSH.Contracts.ProblemCodes.TypeBaseUri = "https://dodossh.dev/problems/" -> string!
const DodoSSH.Contracts.ProblemCodes.VaultConflict = "vault-conflict" -> string!
DodoSSH.Contracts.AddTeamMemberRequest
DodoSSH.Contracts.AddTeamMemberRequest.<Clone>$() -> DodoSSH.Contracts.AddTeamMemberRequest!
DodoSSH.Contracts.AddTeamMemberRequest.AddTeamMemberRequest(System.Guid UserId, DodoSSH.Contracts.TeamMemberRole Role) -> void
DodoSSH.Contracts.AddTeamMemberRequest.Deconstruct(out System.Guid UserId, out DodoSSH.Contracts.TeamMemberRole Role) -> void
DodoSSH.Contracts.AddTeamMemberRequest.Equals(DodoSSH.Contracts.AddTeamMemberRequest? other) -> bool
DodoSSH.Contracts.AddTeamMemberRequest.Role.get -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.AddTeamMemberRequest.Role.init -> void
DodoSSH.Contracts.AddTeamMemberRequest.UserId.get -> System.Guid
DodoSSH.Contracts.AddTeamMemberRequest.UserId.init -> void
DodoSSH.Contracts.ChangeTeamMemberRoleRequest
DodoSSH.Contracts.ChangeTeamMemberRoleRequest.<Clone>$() -> DodoSSH.Contracts.ChangeTeamMemberRoleRequest!
DodoSSH.Contracts.ChangeTeamMemberRoleRequest.ChangeTeamMemberRoleRequest(DodoSSH.Contracts.TeamMemberRole Role) -> void
DodoSSH.Contracts.ChangeTeamMemberRoleRequest.Deconstruct(out DodoSSH.Contracts.TeamMemberRole Role) -> void
DodoSSH.Contracts.ChangeTeamMemberRoleRequest.Equals(DodoSSH.Contracts.ChangeTeamMemberRoleRequest? other) -> bool
DodoSSH.Contracts.ChangeTeamMemberRoleRequest.Role.get -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.ChangeTeamMemberRoleRequest.Role.init -> void
DodoSSH.Contracts.CreateTeamRequest
DodoSSH.Contracts.CreateTeamRequest.<Clone>$() -> DodoSSH.Contracts.CreateTeamRequest!
DodoSSH.Contracts.CreateTeamRequest.CreateTeamRequest(System.Guid TeamId, string! Name, string! Slug, string? Description) -> void
DodoSSH.Contracts.CreateTeamRequest.Deconstruct(out System.Guid TeamId, out string! Name, out string! Slug, out string? Description) -> void
DodoSSH.Contracts.CreateTeamRequest.Description.get -> string?
DodoSSH.Contracts.CreateTeamRequest.Description.init -> void
DodoSSH.Contracts.CreateTeamRequest.Equals(DodoSSH.Contracts.CreateTeamRequest? other) -> bool
DodoSSH.Contracts.CreateTeamRequest.Name.get -> string!
DodoSSH.Contracts.CreateTeamRequest.Name.init -> void
DodoSSH.Contracts.CreateTeamRequest.Slug.get -> string!
DodoSSH.Contracts.CreateTeamRequest.Slug.init -> void
DodoSSH.Contracts.CreateTeamRequest.TeamId.get -> System.Guid
DodoSSH.Contracts.CreateTeamRequest.TeamId.init -> void
DodoSSH.Contracts.CreateTeamVaultRequest
DodoSSH.Contracts.CreateTeamVaultRequest.<Clone>$() -> DodoSSH.Contracts.CreateTeamVaultRequest!
DodoSSH.Contracts.CreateTeamVaultRequest.CreateTeamVaultRequest(System.Guid VaultId, string! Name, byte[]! WrappedVaultKey, byte[]! GrantSignature, System.DateTimeOffset GrantedAt) -> void
DodoSSH.Contracts.CreateTeamVaultRequest.Deconstruct(out System.Guid VaultId, out string! Name, out byte[]! WrappedVaultKey, out byte[]! GrantSignature, out System.DateTimeOffset GrantedAt) -> void
DodoSSH.Contracts.CreateTeamVaultRequest.Equals(DodoSSH.Contracts.CreateTeamVaultRequest? other) -> bool
DodoSSH.Contracts.CreateTeamVaultRequest.GrantedAt.get -> System.DateTimeOffset
DodoSSH.Contracts.CreateTeamVaultRequest.GrantedAt.init -> void
DodoSSH.Contracts.CreateTeamVaultRequest.GrantSignature.get -> byte[]!
DodoSSH.Contracts.CreateTeamVaultRequest.GrantSignature.init -> void
DodoSSH.Contracts.CreateTeamVaultRequest.Name.get -> string!
DodoSSH.Contracts.CreateTeamVaultRequest.Name.init -> void
DodoSSH.Contracts.CreateTeamVaultRequest.VaultId.get -> System.Guid
DodoSSH.Contracts.CreateTeamVaultRequest.VaultId.init -> void
DodoSSH.Contracts.CreateTeamVaultRequest.WrappedVaultKey.get -> byte[]!
DodoSSH.Contracts.CreateTeamVaultRequest.WrappedVaultKey.init -> void
DodoSSH.Contracts.DirectoryEntry
DodoSSH.Contracts.DirectoryEntry.<Clone>$() -> 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
@@ -105,6 +153,25 @@ 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.IssueVaultGrantRequest
DodoSSH.Contracts.IssueVaultGrantRequest.<Clone>$() -> DodoSSH.Contracts.IssueVaultGrantRequest!
DodoSSH.Contracts.IssueVaultGrantRequest.Deconstruct(out System.Guid RecipientUserId, out byte[]! RecipientKeyFingerprint, out uint KeyGeneration, out byte[]! WrappedVaultKey, out byte[]! KeyLogHead, out byte[]! GrantSignature, out System.DateTimeOffset GrantedAt) -> void
DodoSSH.Contracts.IssueVaultGrantRequest.Equals(DodoSSH.Contracts.IssueVaultGrantRequest? other) -> bool
DodoSSH.Contracts.IssueVaultGrantRequest.GrantedAt.get -> System.DateTimeOffset
DodoSSH.Contracts.IssueVaultGrantRequest.GrantedAt.init -> void
DodoSSH.Contracts.IssueVaultGrantRequest.GrantSignature.get -> byte[]!
DodoSSH.Contracts.IssueVaultGrantRequest.GrantSignature.init -> void
DodoSSH.Contracts.IssueVaultGrantRequest.IssueVaultGrantRequest(System.Guid RecipientUserId, byte[]! RecipientKeyFingerprint, uint KeyGeneration, byte[]! WrappedVaultKey, byte[]! KeyLogHead, byte[]! GrantSignature, System.DateTimeOffset GrantedAt) -> void
DodoSSH.Contracts.IssueVaultGrantRequest.KeyGeneration.get -> uint
DodoSSH.Contracts.IssueVaultGrantRequest.KeyGeneration.init -> void
DodoSSH.Contracts.IssueVaultGrantRequest.KeyLogHead.get -> byte[]!
DodoSSH.Contracts.IssueVaultGrantRequest.KeyLogHead.init -> void
DodoSSH.Contracts.IssueVaultGrantRequest.RecipientKeyFingerprint.get -> byte[]!
DodoSSH.Contracts.IssueVaultGrantRequest.RecipientKeyFingerprint.init -> void
DodoSSH.Contracts.IssueVaultGrantRequest.RecipientUserId.get -> System.Guid
DodoSSH.Contracts.IssueVaultGrantRequest.RecipientUserId.init -> void
DodoSSH.Contracts.IssueVaultGrantRequest.WrappedVaultKey.get -> byte[]!
DodoSSH.Contracts.IssueVaultGrantRequest.WrappedVaultKey.init -> void
DodoSSH.Contracts.KdfParameters
DodoSSH.Contracts.KdfParameters.<Clone>$() -> DodoSSH.Contracts.KdfParameters!
DodoSSH.Contracts.KdfParameters.Algorithm.get -> string!
@@ -120,6 +187,42 @@ 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.KeyLogPage
DodoSSH.Contracts.KeyLogPage.<Clone>$() -> DodoSSH.Contracts.KeyLogPage!
DodoSSH.Contracts.KeyLogPage.Deconstruct(out System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.KeyLogRecord!>! Entries, out long HeadSequence, out byte[]! Head, out bool HasMore) -> void
DodoSSH.Contracts.KeyLogPage.Entries.get -> System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.KeyLogRecord!>!
DodoSSH.Contracts.KeyLogPage.Entries.init -> void
DodoSSH.Contracts.KeyLogPage.Equals(DodoSSH.Contracts.KeyLogPage? other) -> bool
DodoSSH.Contracts.KeyLogPage.HasMore.get -> bool
DodoSSH.Contracts.KeyLogPage.HasMore.init -> void
DodoSSH.Contracts.KeyLogPage.Head.get -> byte[]!
DodoSSH.Contracts.KeyLogPage.Head.init -> void
DodoSSH.Contracts.KeyLogPage.HeadSequence.get -> long
DodoSSH.Contracts.KeyLogPage.HeadSequence.init -> void
DodoSSH.Contracts.KeyLogPage.KeyLogPage(System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.KeyLogRecord!>! Entries, long HeadSequence, byte[]! Head, bool HasMore) -> void
DodoSSH.Contracts.KeyLogRecord
DodoSSH.Contracts.KeyLogRecord.<Clone>$() -> DodoSSH.Contracts.KeyLogRecord!
DodoSSH.Contracts.KeyLogRecord.CreatedAt.get -> System.DateTimeOffset
DodoSSH.Contracts.KeyLogRecord.CreatedAt.init -> void
DodoSSH.Contracts.KeyLogRecord.Deconstruct(out long Sequence, out System.Guid UserId, out int Generation, out byte[]! EncryptionPublicKey, out byte[]! SigningPublicKey, out byte[]! StatementSignature, out byte[]! PreviousHash, out byte[]! Hash, out System.DateTimeOffset CreatedAt) -> void
DodoSSH.Contracts.KeyLogRecord.EncryptionPublicKey.get -> byte[]!
DodoSSH.Contracts.KeyLogRecord.EncryptionPublicKey.init -> void
DodoSSH.Contracts.KeyLogRecord.Equals(DodoSSH.Contracts.KeyLogRecord? other) -> bool
DodoSSH.Contracts.KeyLogRecord.Generation.get -> int
DodoSSH.Contracts.KeyLogRecord.Generation.init -> void
DodoSSH.Contracts.KeyLogRecord.Hash.get -> byte[]!
DodoSSH.Contracts.KeyLogRecord.Hash.init -> void
DodoSSH.Contracts.KeyLogRecord.KeyLogRecord(long Sequence, System.Guid UserId, int Generation, byte[]! EncryptionPublicKey, byte[]! SigningPublicKey, byte[]! StatementSignature, byte[]! PreviousHash, byte[]! Hash, System.DateTimeOffset CreatedAt) -> void
DodoSSH.Contracts.KeyLogRecord.PreviousHash.get -> byte[]!
DodoSSH.Contracts.KeyLogRecord.PreviousHash.init -> void
DodoSSH.Contracts.KeyLogRecord.Sequence.get -> long
DodoSSH.Contracts.KeyLogRecord.Sequence.init -> void
DodoSSH.Contracts.KeyLogRecord.SigningPublicKey.get -> byte[]!
DodoSSH.Contracts.KeyLogRecord.SigningPublicKey.init -> void
DodoSSH.Contracts.KeyLogRecord.StatementSignature.get -> byte[]!
DodoSSH.Contracts.KeyLogRecord.StatementSignature.init -> void
DodoSSH.Contracts.KeyLogRecord.UserId.get -> System.Guid
DodoSSH.Contracts.KeyLogRecord.UserId.init -> void
DodoSSH.Contracts.KeyStatement
DodoSSH.Contracts.KeyStatement.<Clone>$() -> DodoSSH.Contracts.KeyStatement!
DodoSSH.Contracts.KeyStatement.CreatedAt.get -> System.DateTimeOffset
@@ -451,6 +554,96 @@ 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.TeamMemberRole
DodoSSH.Contracts.TeamMemberRole.Admin = 30 -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamMemberRole.Member = 20 -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamMemberRole.Owner = 40 -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamMemberRole.Unspecified = 0 -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamMemberRole.Viewer = 10 -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamMemberStatus
DodoSSH.Contracts.TeamMemberStatus.Active = 2 -> DodoSSH.Contracts.TeamMemberStatus
DodoSSH.Contracts.TeamMemberStatus.Invited = 1 -> DodoSSH.Contracts.TeamMemberStatus
DodoSSH.Contracts.TeamMemberStatus.Revoked = 3 -> DodoSSH.Contracts.TeamMemberStatus
DodoSSH.Contracts.TeamMemberStatus.Unspecified = 0 -> DodoSSH.Contracts.TeamMemberStatus
DodoSSH.Contracts.TeamMemberSummary
DodoSSH.Contracts.TeamMemberSummary.<Clone>$() -> DodoSSH.Contracts.TeamMemberSummary!
DodoSSH.Contracts.TeamMemberSummary.Deconstruct(out System.Guid UserId, out string? Email, out string? DisplayName, out DodoSSH.Contracts.TeamMemberRole Role, out DodoSSH.Contracts.TeamMemberStatus Status, out bool IsEnrolled, out System.DateTimeOffset? JoinedAt) -> void
DodoSSH.Contracts.TeamMemberSummary.DisplayName.get -> string?
DodoSSH.Contracts.TeamMemberSummary.DisplayName.init -> void
DodoSSH.Contracts.TeamMemberSummary.Email.get -> string?
DodoSSH.Contracts.TeamMemberSummary.Email.init -> void
DodoSSH.Contracts.TeamMemberSummary.Equals(DodoSSH.Contracts.TeamMemberSummary? other) -> bool
DodoSSH.Contracts.TeamMemberSummary.IsEnrolled.get -> bool
DodoSSH.Contracts.TeamMemberSummary.IsEnrolled.init -> void
DodoSSH.Contracts.TeamMemberSummary.JoinedAt.get -> System.DateTimeOffset?
DodoSSH.Contracts.TeamMemberSummary.JoinedAt.init -> void
DodoSSH.Contracts.TeamMemberSummary.Role.get -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamMemberSummary.Role.init -> void
DodoSSH.Contracts.TeamMemberSummary.Status.get -> DodoSSH.Contracts.TeamMemberStatus
DodoSSH.Contracts.TeamMemberSummary.Status.init -> void
DodoSSH.Contracts.TeamMemberSummary.TeamMemberSummary(System.Guid UserId, string? Email, string? DisplayName, DodoSSH.Contracts.TeamMemberRole Role, DodoSSH.Contracts.TeamMemberStatus Status, bool IsEnrolled, System.DateTimeOffset? JoinedAt) -> void
DodoSSH.Contracts.TeamMemberSummary.UserId.get -> System.Guid
DodoSSH.Contracts.TeamMemberSummary.UserId.init -> void
DodoSSH.Contracts.TeamSummary
DodoSSH.Contracts.TeamSummary.<Clone>$() -> DodoSSH.Contracts.TeamSummary!
DodoSSH.Contracts.TeamSummary.CreatedAt.get -> System.DateTimeOffset
DodoSSH.Contracts.TeamSummary.CreatedAt.init -> void
DodoSSH.Contracts.TeamSummary.Deconstruct(out System.Guid TeamId, out string! Name, out string! Slug, out string? Description, out DodoSSH.Contracts.TeamMemberRole Role, out int MemberCount, out int VaultCount, out System.DateTimeOffset CreatedAt) -> void
DodoSSH.Contracts.TeamSummary.Description.get -> string?
DodoSSH.Contracts.TeamSummary.Description.init -> void
DodoSSH.Contracts.TeamSummary.Equals(DodoSSH.Contracts.TeamSummary? other) -> bool
DodoSSH.Contracts.TeamSummary.MemberCount.get -> int
DodoSSH.Contracts.TeamSummary.MemberCount.init -> void
DodoSSH.Contracts.TeamSummary.Name.get -> string!
DodoSSH.Contracts.TeamSummary.Name.init -> void
DodoSSH.Contracts.TeamSummary.Role.get -> DodoSSH.Contracts.TeamMemberRole
DodoSSH.Contracts.TeamSummary.Role.init -> void
DodoSSH.Contracts.TeamSummary.Slug.get -> string!
DodoSSH.Contracts.TeamSummary.Slug.init -> void
DodoSSH.Contracts.TeamSummary.TeamId.get -> System.Guid
DodoSSH.Contracts.TeamSummary.TeamId.init -> void
DodoSSH.Contracts.TeamSummary.TeamSummary(System.Guid TeamId, string! Name, string! Slug, string? Description, DodoSSH.Contracts.TeamMemberRole Role, int MemberCount, int VaultCount, System.DateTimeOffset CreatedAt) -> void
DodoSSH.Contracts.TeamSummary.VaultCount.get -> int
DodoSSH.Contracts.TeamSummary.VaultCount.init -> void
DodoSSH.Contracts.VaultGrantsResponse
DodoSSH.Contracts.VaultGrantsResponse.<Clone>$() -> DodoSSH.Contracts.VaultGrantsResponse!
DodoSSH.Contracts.VaultGrantsResponse.Deconstruct(out System.Guid VaultId, out uint KeyGeneration, out bool RekeyRequired, out System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.VaultGrantSummary!>! Grants) -> void
DodoSSH.Contracts.VaultGrantsResponse.Equals(DodoSSH.Contracts.VaultGrantsResponse? other) -> bool
DodoSSH.Contracts.VaultGrantsResponse.Grants.get -> System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.VaultGrantSummary!>!
DodoSSH.Contracts.VaultGrantsResponse.Grants.init -> void
DodoSSH.Contracts.VaultGrantsResponse.KeyGeneration.get -> uint
DodoSSH.Contracts.VaultGrantsResponse.KeyGeneration.init -> void
DodoSSH.Contracts.VaultGrantsResponse.RekeyRequired.get -> bool
DodoSSH.Contracts.VaultGrantsResponse.RekeyRequired.init -> void
DodoSSH.Contracts.VaultGrantsResponse.VaultGrantsResponse(System.Guid VaultId, uint KeyGeneration, bool RekeyRequired, System.Collections.Generic.IReadOnlyList<DodoSSH.Contracts.VaultGrantSummary!>! Grants) -> void
DodoSSH.Contracts.VaultGrantsResponse.VaultId.get -> System.Guid
DodoSSH.Contracts.VaultGrantsResponse.VaultId.init -> void
DodoSSH.Contracts.VaultGrantState
DodoSSH.Contracts.VaultGrantState.Active = 1 -> DodoSSH.Contracts.VaultGrantState
DodoSSH.Contracts.VaultGrantState.AwaitingRewrap = 2 -> DodoSSH.Contracts.VaultGrantState
DodoSSH.Contracts.VaultGrantState.Revoked = 3 -> DodoSSH.Contracts.VaultGrantState
DodoSSH.Contracts.VaultGrantState.Unspecified = 0 -> DodoSSH.Contracts.VaultGrantState
DodoSSH.Contracts.VaultGrantSummary
DodoSSH.Contracts.VaultGrantSummary.<Clone>$() -> DodoSSH.Contracts.VaultGrantSummary!
DodoSSH.Contracts.VaultGrantSummary.CreatedAt.get -> System.DateTimeOffset
DodoSSH.Contracts.VaultGrantSummary.CreatedAt.init -> void
DodoSSH.Contracts.VaultGrantSummary.Deconstruct(out System.Guid RecipientUserId, out string? Email, out string? DisplayName, out uint KeyGeneration, out DodoSSH.Contracts.VaultGrantState State, out System.Guid GranterUserId, out System.DateTimeOffset CreatedAt, out System.DateTimeOffset? RevokedAt) -> void
DodoSSH.Contracts.VaultGrantSummary.DisplayName.get -> string?
DodoSSH.Contracts.VaultGrantSummary.DisplayName.init -> void
DodoSSH.Contracts.VaultGrantSummary.Email.get -> string?
DodoSSH.Contracts.VaultGrantSummary.Email.init -> void
DodoSSH.Contracts.VaultGrantSummary.Equals(DodoSSH.Contracts.VaultGrantSummary? other) -> bool
DodoSSH.Contracts.VaultGrantSummary.GranterUserId.get -> System.Guid
DodoSSH.Contracts.VaultGrantSummary.GranterUserId.init -> void
DodoSSH.Contracts.VaultGrantSummary.KeyGeneration.get -> uint
DodoSSH.Contracts.VaultGrantSummary.KeyGeneration.init -> void
DodoSSH.Contracts.VaultGrantSummary.RecipientUserId.get -> System.Guid
DodoSSH.Contracts.VaultGrantSummary.RecipientUserId.init -> void
DodoSSH.Contracts.VaultGrantSummary.RevokedAt.get -> System.DateTimeOffset?
DodoSSH.Contracts.VaultGrantSummary.RevokedAt.init -> void
DodoSSH.Contracts.VaultGrantSummary.State.get -> DodoSSH.Contracts.VaultGrantState
DodoSSH.Contracts.VaultGrantSummary.State.init -> void
DodoSSH.Contracts.VaultGrantSummary.VaultGrantSummary(System.Guid RecipientUserId, string? Email, string? DisplayName, uint KeyGeneration, DodoSSH.Contracts.VaultGrantState State, System.Guid GranterUserId, System.DateTimeOffset CreatedAt, System.DateTimeOffset? RevokedAt) -> void
DodoSSH.Contracts.VaultSummary
DodoSSH.Contracts.VaultSummary.<Clone>$() -> 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
@@ -472,6 +665,18 @@ 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
override DodoSSH.Contracts.AddTeamMemberRequest.Equals(object? obj) -> bool
override DodoSSH.Contracts.AddTeamMemberRequest.GetHashCode() -> int
override DodoSSH.Contracts.AddTeamMemberRequest.ToString() -> string!
override DodoSSH.Contracts.ChangeTeamMemberRoleRequest.Equals(object? obj) -> bool
override DodoSSH.Contracts.ChangeTeamMemberRoleRequest.GetHashCode() -> int
override DodoSSH.Contracts.ChangeTeamMemberRoleRequest.ToString() -> string!
override DodoSSH.Contracts.CreateTeamRequest.Equals(object? obj) -> bool
override DodoSSH.Contracts.CreateTeamRequest.GetHashCode() -> int
override DodoSSH.Contracts.CreateTeamRequest.ToString() -> string!
override DodoSSH.Contracts.CreateTeamVaultRequest.Equals(object? obj) -> bool
override DodoSSH.Contracts.CreateTeamVaultRequest.GetHashCode() -> int
override DodoSSH.Contracts.CreateTeamVaultRequest.ToString() -> string!
override DodoSSH.Contracts.DirectoryEntry.Equals(object? obj) -> bool
override DodoSSH.Contracts.DirectoryEntry.GetHashCode() -> int
override DodoSSH.Contracts.DirectoryEntry.ToString() -> string!
@@ -487,9 +692,18 @@ 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.IssueVaultGrantRequest.Equals(object? obj) -> bool
override DodoSSH.Contracts.IssueVaultGrantRequest.GetHashCode() -> int
override DodoSSH.Contracts.IssueVaultGrantRequest.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.KeyLogPage.Equals(object? obj) -> bool
override DodoSSH.Contracts.KeyLogPage.GetHashCode() -> int
override DodoSSH.Contracts.KeyLogPage.ToString() -> string!
override DodoSSH.Contracts.KeyLogRecord.Equals(object? obj) -> bool
override DodoSSH.Contracts.KeyLogRecord.GetHashCode() -> int
override DodoSSH.Contracts.KeyLogRecord.ToString() -> string!
override DodoSSH.Contracts.KeyStatement.Equals(object? obj) -> bool
override DodoSSH.Contracts.KeyStatement.GetHashCode() -> int
override DodoSSH.Contracts.KeyStatement.ToString() -> string!
@@ -547,9 +761,29 @@ 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.TeamMemberSummary.Equals(object? obj) -> bool
override DodoSSH.Contracts.TeamMemberSummary.GetHashCode() -> int
override DodoSSH.Contracts.TeamMemberSummary.ToString() -> string!
override DodoSSH.Contracts.TeamSummary.Equals(object? obj) -> bool
override DodoSSH.Contracts.TeamSummary.GetHashCode() -> int
override DodoSSH.Contracts.TeamSummary.ToString() -> string!
override DodoSSH.Contracts.VaultGrantsResponse.Equals(object? obj) -> bool
override DodoSSH.Contracts.VaultGrantsResponse.GetHashCode() -> int
override DodoSSH.Contracts.VaultGrantsResponse.ToString() -> string!
override DodoSSH.Contracts.VaultGrantSummary.Equals(object? obj) -> bool
override DodoSSH.Contracts.VaultGrantSummary.GetHashCode() -> int
override DodoSSH.Contracts.VaultGrantSummary.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.AddTeamMemberRequest.operator !=(DodoSSH.Contracts.AddTeamMemberRequest? left, DodoSSH.Contracts.AddTeamMemberRequest? right) -> bool
static DodoSSH.Contracts.AddTeamMemberRequest.operator ==(DodoSSH.Contracts.AddTeamMemberRequest? left, DodoSSH.Contracts.AddTeamMemberRequest? right) -> bool
static DodoSSH.Contracts.ChangeTeamMemberRoleRequest.operator !=(DodoSSH.Contracts.ChangeTeamMemberRoleRequest? left, DodoSSH.Contracts.ChangeTeamMemberRoleRequest? right) -> bool
static DodoSSH.Contracts.ChangeTeamMemberRoleRequest.operator ==(DodoSSH.Contracts.ChangeTeamMemberRoleRequest? left, DodoSSH.Contracts.ChangeTeamMemberRoleRequest? right) -> bool
static DodoSSH.Contracts.CreateTeamRequest.operator !=(DodoSSH.Contracts.CreateTeamRequest? left, DodoSSH.Contracts.CreateTeamRequest? right) -> bool
static DodoSSH.Contracts.CreateTeamRequest.operator ==(DodoSSH.Contracts.CreateTeamRequest? left, DodoSSH.Contracts.CreateTeamRequest? right) -> bool
static DodoSSH.Contracts.CreateTeamVaultRequest.operator !=(DodoSSH.Contracts.CreateTeamVaultRequest? left, DodoSSH.Contracts.CreateTeamVaultRequest? right) -> bool
static DodoSSH.Contracts.CreateTeamVaultRequest.operator ==(DodoSSH.Contracts.CreateTeamVaultRequest? left, DodoSSH.Contracts.CreateTeamVaultRequest? right) -> bool
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
@@ -563,8 +797,14 @@ static DodoSSH.Contracts.EnrollmentRequest.operator !=(DodoSSH.Contracts.Enrollm
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.IssueVaultGrantRequest.operator !=(DodoSSH.Contracts.IssueVaultGrantRequest? left, DodoSSH.Contracts.IssueVaultGrantRequest? right) -> bool
static DodoSSH.Contracts.IssueVaultGrantRequest.operator ==(DodoSSH.Contracts.IssueVaultGrantRequest? left, DodoSSH.Contracts.IssueVaultGrantRequest? 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.KeyLogPage.operator !=(DodoSSH.Contracts.KeyLogPage? left, DodoSSH.Contracts.KeyLogPage? right) -> bool
static DodoSSH.Contracts.KeyLogPage.operator ==(DodoSSH.Contracts.KeyLogPage? left, DodoSSH.Contracts.KeyLogPage? right) -> bool
static DodoSSH.Contracts.KeyLogRecord.operator !=(DodoSSH.Contracts.KeyLogRecord? left, DodoSSH.Contracts.KeyLogRecord? right) -> bool
static DodoSSH.Contracts.KeyLogRecord.operator ==(DodoSSH.Contracts.KeyLogRecord? left, DodoSSH.Contracts.KeyLogRecord? 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
@@ -603,5 +843,13 @@ static DodoSSH.Contracts.SyncPushResponse.operator !=(DodoSSH.Contracts.SyncPush
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.TeamMemberSummary.operator !=(DodoSSH.Contracts.TeamMemberSummary? left, DodoSSH.Contracts.TeamMemberSummary? right) -> bool
static DodoSSH.Contracts.TeamMemberSummary.operator ==(DodoSSH.Contracts.TeamMemberSummary? left, DodoSSH.Contracts.TeamMemberSummary? right) -> bool
static DodoSSH.Contracts.TeamSummary.operator !=(DodoSSH.Contracts.TeamSummary? left, DodoSSH.Contracts.TeamSummary? right) -> bool
static DodoSSH.Contracts.TeamSummary.operator ==(DodoSSH.Contracts.TeamSummary? left, DodoSSH.Contracts.TeamSummary? right) -> bool
static DodoSSH.Contracts.VaultGrantsResponse.operator !=(DodoSSH.Contracts.VaultGrantsResponse? left, DodoSSH.Contracts.VaultGrantsResponse? right) -> bool
static DodoSSH.Contracts.VaultGrantsResponse.operator ==(DodoSSH.Contracts.VaultGrantsResponse? left, DodoSSH.Contracts.VaultGrantsResponse? right) -> bool
static DodoSSH.Contracts.VaultGrantSummary.operator !=(DodoSSH.Contracts.VaultGrantSummary? left, DodoSSH.Contracts.VaultGrantSummary? right) -> bool
static DodoSSH.Contracts.VaultGrantSummary.operator ==(DodoSSH.Contracts.VaultGrantSummary? left, DodoSSH.Contracts.VaultGrantSummary? 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
+266
View File
@@ -0,0 +1,266 @@
namespace DodoSSH.Contracts;
/// <summary>
/// A member's role within a team, as it travels on the wire.
/// </summary>
/// <remarks>
/// <para>
/// A separate type from <c>DodoSSH.Domain.TeamRole</c> only because both are visible inside the
/// server, exactly as <c>GrantPurpose</c> is separate from <c>GrantKind</c>. The <b>numeric values
/// must match</b> that enum, and a test pins them: the two are converted by cast, so a renumbering
/// here silently promotes or demotes every member on the next deployment.
/// </para>
/// <para>
/// There is no <c>ConnectOnly</c> role, and there will not be one built this way. Connect is a
/// user-interface hint rather than a boundary — SSH terminates on the client, so opening a session
/// needs the credential's plaintext on that machine, and "may connect but may not read the key" is
/// unenforceable in this architecture. See <c>docs/adr/0001-e2ee-trust-model.md</c>.
/// </para>
/// </remarks>
public enum TeamMemberRole
{
/// <summary>Not a legal value.</summary>
Unspecified = 0,
/// <summary>May read the team's vaults and nothing else.</summary>
Viewer = 10,
/// <summary>May read and change the team's vaults.</summary>
Member = 20,
/// <summary>May also manage members, create vaults, and share vault keys.</summary>
Admin = 30,
/// <summary>Sole owner. Everything an admin may do, and cannot be removed while sole.</summary>
Owner = 40,
}
/// <summary>State of a team membership, as it travels on the wire.</summary>
/// <remarks>
/// Values match <c>DodoSSH.Domain.MembershipStatus</c>, for the reason
/// <see cref="TeamMemberRole"/> gives.
/// </remarks>
public enum TeamMemberStatus
{
/// <summary>Not a legal value.</summary>
Unspecified = 0,
/// <summary>
/// Invited but not yet accepted.
/// </summary>
/// <remarks>
/// Nothing writes this today. An invitation needs a token with a lifetime and an outbound mail
/// path, and this server has neither — so a member is added by looking their account up in the
/// directory, which requires that they have signed in here at least once. Retained because the
/// column exists and a client must not fail on a value a later server may send.
/// </remarks>
Invited = 1,
/// <summary>Active member.</summary>
Active = 2,
/// <summary>Removed. Retained so audit history stays resolvable to a person.</summary>
Revoked = 3,
}
/// <summary>State of a vault key grant, as it travels on the wire.</summary>
/// <remarks>Values match <c>DodoSSH.Domain.GrantState</c>.</remarks>
public enum VaultGrantState
{
/// <summary>Not a legal value.</summary>
Unspecified = 0,
/// <summary>Usable.</summary>
Active = 1,
/// <summary>
/// The recipient's identity key changed or the vault was rekeyed, so a member holding Share
/// must wrap the key afresh before the recipient can read anything again.
/// </summary>
AwaitingRewrap = 2,
/// <summary>
/// Revoked. Blocks future reads only — anything already downloaded is already gone, and the
/// remediation for a departed member is rotating the SSH credential itself. See ADR 0001.
/// </summary>
Revoked = 3,
}
/// <summary>A team the caller belongs to.</summary>
/// <param name="TeamId">The team.</param>
/// <param name="Name">Display name.</param>
/// <param name="Slug">URL-safe unique identifier.</param>
/// <param name="Description">Optional description.</param>
/// <param name="Role">The caller's own role.</param>
/// <param name="MemberCount">Active members, including the caller.</param>
/// <param name="VaultCount">Vaults the team owns.</param>
/// <param name="CreatedAt">When the team was created.</param>
public sealed record TeamSummary(
Guid TeamId,
string Name,
string Slug,
string? Description,
TeamMemberRole Role,
int MemberCount,
int VaultCount,
DateTimeOffset CreatedAt);
/// <summary>A request to create a team.</summary>
/// <remarks>
/// <see cref="TeamId"/> is chosen by the client for the same reason a vault id is: a request whose
/// response was lost can be re-sent verbatim and returns the identical team rather than creating a
/// second one under a name the user only meant to type once.
/// </remarks>
/// <param name="TeamId">Client-generated UUIDv7.</param>
/// <param name="Name">Display name.</param>
/// <param name="Slug">
/// URL-safe unique identifier, lowercase. Unique across the deployment, so this is the one field a
/// create can fail on for a reason the caller cannot see coming.
/// </param>
/// <param name="Description">Optional description.</param>
public sealed record CreateTeamRequest(
Guid TeamId,
string Name,
string Slug,
string? Description);
/// <summary>One member of a team.</summary>
/// <remarks>
/// Carries no last-active time and no avatar. <c>UserAccount.LastSeenAtUtc</c> is written at
/// provisioning and at enrollment and at no other point, so a column labelled "last active" would
/// be reporting something else entirely; and no picture is stored anywhere.
/// </remarks>
/// <param name="UserId">The member.</param>
/// <param name="Email">Email, for display.</param>
/// <param name="DisplayName">Display name.</param>
/// <param name="Role">Role within the team.</param>
/// <param name="Status">Membership state.</param>
/// <param name="IsEnrolled">
/// Whether this member has published an identity key. A member who has not cannot be granted a
/// vault key at all — there is nothing to wrap one to — so the interface has to be able to say so
/// rather than offering a share that would fail.
/// </param>
/// <param name="JoinedAt">When the membership became active.</param>
public sealed record TeamMemberSummary(
Guid UserId,
string? Email,
string? DisplayName,
TeamMemberRole Role,
TeamMemberStatus Status,
bool IsEnrolled,
DateTimeOffset? JoinedAt);
/// <summary>Adds a member to a team.</summary>
/// <remarks>
/// By user id rather than by email, and the id comes from a directory lookup the caller has already
/// made. That ordering is not incidental: whoever adds a member is usually about to wrap a vault key
/// to their public key, and the key they must verify is the one the directory returned. Adding by
/// email here would put an account resolution the client never saw between those two steps.
/// </remarks>
/// <param name="UserId">The account to add, as returned by the directory.</param>
/// <param name="Role">Role to grant.</param>
public sealed record AddTeamMemberRequest(Guid UserId, TeamMemberRole Role);
/// <summary>Changes a member's role.</summary>
/// <param name="Role">The new role.</param>
public sealed record ChangeTeamMemberRoleRequest(TeamMemberRole Role);
/// <summary>
/// Creates a vault owned by a team, with its key already wrapped to the creator.
/// </summary>
/// <remarks>
/// Shaped like <see cref="PersonalVaultRequest"/> and for the same reasons: the vault key is
/// generated on the client and sealed to the creator's own X25519 key, so the server cannot produce
/// this and cannot check that <see cref="WrappedVaultKey"/> contains anything in particular. A vault
/// created with no grant would be a container nobody could ever open, so the two arrive together.
/// <para>
/// The creator's grant carries no key log head, exactly as a personal vault's does not: there is no
/// third party whose key could have been substituted. Every <em>other</em> member's grant does carry
/// one — see <see cref="IssueVaultGrantRequest"/>.
/// </para>
/// </remarks>
/// <param name="VaultId">Client-generated UUIDv7.</param>
/// <param name="Name">Display name. Plaintext, as all vault names are.</param>
/// <param name="WrappedVaultKey">The vault key sealed to the creator's encryption key.</param>
/// <param name="GrantSignature">Ed25519 signature over the canonical grant tuple.</param>
/// <param name="GrantedAt">Signing timestamp, part of the signed tuple.</param>
public sealed record CreateTeamVaultRequest(
Guid VaultId,
string Name,
byte[] WrappedVaultKey,
byte[] GrantSignature,
DateTimeOffset GrantedAt);
/// <summary>Issues a vault key grant to another member.</summary>
/// <remarks>
/// <para>
/// The wrap is made by a client that holds the vault key, to a public key it has verified. The
/// server stores both the ciphertext and the signature and can check neither — which is the property
/// that makes it a zero-knowledge server rather than a key-holding one.
/// </para>
/// <para>
/// <see cref="KeyLogHead"/> is required here and absent for a self-grant. A third party's key could
/// have been substituted by the server; recording the log head the granter observed while wrapping
/// is what converts that from an undetectable attack into a detectable one. See docs/crypto.md §7.2.
/// </para>
/// </remarks>
/// <param name="RecipientUserId">Who the key was wrapped to.</param>
/// <param name="RecipientKeyFingerprint">
/// The exact identity key it was wrapped to. Stored so a later rotation invalidates this grant
/// explicitly rather than leaving a row that no longer opens.
/// </param>
/// <param name="KeyGeneration">
/// The generation wrapped. Rejected when it is not the vault's current one, because a grant for a
/// superseded generation opens nothing and would read as corruption at the far end.
/// </param>
/// <param name="WrappedVaultKey">The vault key sealed to the recipient. Opaque to the server.</param>
/// <param name="KeyLogHead">The key log head the granter observed while wrapping.</param>
/// <param name="GrantSignature">Ed25519 signature over the canonical grant tuple.</param>
/// <param name="GrantedAt">Signing timestamp, part of the signed tuple.</param>
public sealed record IssueVaultGrantRequest(
Guid RecipientUserId,
byte[] RecipientKeyFingerprint,
uint KeyGeneration,
byte[] WrappedVaultKey,
byte[] KeyLogHead,
byte[] GrantSignature,
DateTimeOffset GrantedAt);
/// <summary>One vault key grant, as the sharing interface sees it.</summary>
/// <remarks>
/// The wrapped key itself is deliberately not here. A member reads their own through
/// <see cref="VaultSummary.WrappedVaultKey"/>; this listing exists so somebody holding Share can see
/// <em>who has one</em>, and serving every member's sealed key to every member would be a pointless
/// widening of what a stolen access token yields.
/// </remarks>
/// <param name="RecipientUserId">Who holds it.</param>
/// <param name="Email">Their email, for display.</param>
/// <param name="DisplayName">Their display name.</param>
/// <param name="KeyGeneration">Generation this grant is for.</param>
/// <param name="State">Grant state.</param>
/// <param name="GranterUserId">Who issued it.</param>
/// <param name="CreatedAt">When it was issued.</param>
/// <param name="RevokedAt">When it was revoked, if it was.</param>
public sealed record VaultGrantSummary(
Guid RecipientUserId,
string? Email,
string? DisplayName,
uint KeyGeneration,
VaultGrantState State,
Guid GranterUserId,
DateTimeOffset CreatedAt,
DateTimeOffset? RevokedAt);
/// <summary>Who can open a vault, and at which generation.</summary>
/// <param name="VaultId">The vault.</param>
/// <param name="KeyGeneration">
/// The vault's current generation. A grant listed at anything lower is stale, which is what a client
/// compares against rather than inferring from <see cref="VaultGrantSummary.State"/> alone.
/// </param>
/// <param name="RekeyRequired">Whether a membership change has left this vault needing a rekey.</param>
/// <param name="Grants">Every grant, including revoked ones.</param>
public sealed record VaultGrantsResponse(
Guid VaultId,
uint KeyGeneration,
bool RekeyRequired,
IReadOnlyList<VaultGrantSummary> Grants);