namespace DodoSSH.Domain;
///
/// An SSH host.
///
///
///
/// The only vault item type in M1. Everything sensitive — username, notes, jump chain, SSH
/// options — lives inside . There is deliberately no plaintext label:
/// access-control administration happens in the client, which can decrypt names, so the server
/// never needs a searchable title.
///
///
/// and are the one deliberate plaintext concession, and
/// only when is set. The relay must resolve its target server-side or
/// it becomes an authenticated open TCP proxy into the operator's own network. A database CHECK
/// constraint enforces the pairing so it cannot drift. See ADR 0004.
///
///
/// There is no group column, and there used to be one. A group_id was reserved here from the
/// first migration and never written by any shipped client; it was dropped when groups actually landed,
/// because membership belongs inside . The test ADR 0004 sets for a plaintext
/// concession is that the server cannot function without the value, and nothing on the server reads
/// a group — the client decrypts its own vault to draw the sidebar. What the column would have handed over
/// is a clustering of the estate: which machines this user files together, for free, in the clear. See
/// .
///
///
public sealed class SshHost : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so items can be created offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted item: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
///
/// Reserved for per-item content keys wrapped to individual users, which is what will make
/// per-item access control cryptographic rather than server-enforced. Present from the first
/// migration so that lands without a migration; see docs/crypto.md §3.
///
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Whether this host may be dialled through the server relay.
public bool RelayEnabled { get; set; }
/// Target hostname. Permitted only when is set.
public string? Hostname { get; set; }
/// Target port. Permitted only when is set.
public int? Port { get; set; }
///
/// Client-visible, monotonic item version. Used for optimistic concurrency on push, and
/// deliberately distinct from the internal xmin guard, which is never exposed because
/// it is not stable across VACUUM FREEZE.
///
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
///
/// Soft-delete marker. Deletes are tombstones: a client that has been offline must be able to
/// learn an item went away, and a vanished row is indistinguishable from one never seen.
///
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// An SSH key pair, held as ciphertext.
///
///
///
/// The private key, its passphrase, its label and its comment are all inside . This
/// row is the reason the vault is worth having — a key that syncs between a user's machines and can later be
/// shared with a teammate — and it is also the row that would hurt most if the server could read it, so
/// there is deliberately not one plaintext column of substance.
///
///
/// Notably absent: the relay trio. A key is not something the server dials, so the plaintext concession
/// ADR 0004 makes for has no analogue here and no reason to exist. That is the whole
/// argument for a separate table rather than one wide item table: the columns a host needs are columns a key
/// must never have, and a shared table could only make them nullable and trust the code.
///
///
/// is the single exception, and it stays null until something needs it.
/// The contract reserved the slot for showing which key a host is configured to use without decrypting
/// every key first; write it only when that feature lands, and never derive anything security-relevant from
/// it, because a fingerprint the server stores is a fingerprint the server chose.
///
///
public sealed class VaultSshKey : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so keys can be created offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted key material: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
///
/// OpenSSH-style SHA256:base64 fingerprint of the public half, when a client publishes it.
///
///
/// Plaintext, and therefore opt-in and currently unused. A public key fingerprint is not a secret, but
/// it is an identifier that links a vault to a machine's authorized_keys, so it is stored only
/// when a feature needs it rather than because it is harmless.
///
public string? PublicKeyFingerprint { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
/// Soft-delete marker; a tombstone, so an offline client learns the key went away.
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// A stored username and password, as ciphertext.
///
///
///
/// Its own table for the same reason is: the columns a host needs are columns a
/// credential must never have. There is no relay trio here, and unlike a key there is not even a fingerprint
/// — nothing about a password is safe to hold in the clear, not its length, not a hash, not a hint. So this
/// row is an opaque envelope and its bookkeeping, and that is the whole design.
///
///
/// The server cannot enforce anything about the contents, and should not pretend to. Whether a
/// credential has a username, whether its password is empty, whether it is still valid — all of that is
/// inside the payload and belongs to the client. The one thing this row asserts is that the ciphertext
/// belongs to a vault and carries a version, which is what the write path needs to order changes.
///
///
public sealed class VaultCredential : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so credentials can be created offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted credential: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
/// Soft-delete marker; a tombstone, so an offline client learns the credential went away.
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// One SSH host key a user has decided to trust, as ciphertext.
///
///
///
/// The host, the port, the key algorithm and the fingerprint are all inside , and this
/// is the item type where that is least obvious and most deliberate. None of those four values is
/// confidential on its own — an operator publishes the fingerprint so it can be checked, and a
/// relay-enabled host's address is already in a plaintext column next door. Together and across a user's
/// whole vault they are something else: which machines that person reaches, and when they first reached
/// them. The server has no use for any of it, so it gets none of it, and there is no column here to put it
/// in even by mistake.
///
///
/// Why the server holds these at all, given it cannot read them. Trust-on-first-use is only as good
/// as its memory. Kept locally, a pin dies with the machine and the user is asked to approve the same host
/// on every device — training them to approve without looking, which is the one habit that makes the
/// warning worthless. Kept here, trust follows the user, and because the payload is sealed the server
/// cannot drop a pin to force a fresh first-use decision without the client noticing the item is gone.
///
///
/// Otherwise this is 's shape exactly: an opaque envelope and the bookkeeping
/// the write path needs to order changes. Its own table for the same reason — the columns a host needs are
/// columns this must never have.
///
///
public sealed class VaultKnownHostKey : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so a pin can be recorded offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted pin: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
///
/// Soft-delete marker; a tombstone, so an offline client learns the pin was withdrawn.
///
///
/// A tombstone matters more here than for the other types. Withdrawing trust is the deliberate act a
/// user performs when a server has legitimately been rebuilt, and a pin that simply vanished from one
/// machine's view would come back on the next pull — leaving the rebuilt host permanently unreachable
/// from the machine that had the old key.
///
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// A folder hosts can be filed under, as ciphertext.
///
///
///
/// Everything a group is — its name, the group it sits under, and the port, username and binding hosts
/// inside it fall back to — is inside the envelope, so this row is one of the narrowest in the schema: an
/// envelope and its bookkeeping. There is no parent_id and no name column, and both absences
/// are deliberate.
///
///
/// No parent column, although groups do nest. The pointer exists; it lives in the payload, and the
/// push path refuses a plaintext ParentId outright. What a column here would hand the operator is the
/// shape of every user's estate — how many groupings, how deep, which under which — which is the same
/// disclosure a plaintext group_id on a host would have been, and ADR 0004 spends the one plaintext
/// concession this design allows on the relay address instead.
///
///
/// The consequence is that the server cannot see a cycle, and it was once the argument for having no parent
/// at all: two offline clients can each re-parent A under B and B under A, a scalar merge accepts both, and
/// nothing server-side can refuse the pair. That is still true and is now handled rather than avoided —
/// every walk of the chain carries a visited set and stops at a repeat, so a cycle degrades to a group that
/// reads as a root. See HostGroupSecret on the client, which is the only thing that can read any of
/// this.
///
///
/// No name, for the reason has no host column. A group name is not
/// confidential the way a password is, but the set of names one person files their machines under is a
/// description of the estate — "customer-a", "pci", "on-call" — and the server has no use for any of it. It
/// sorts nothing; the client decrypts its own vault to draw a list.
///
///
/// Membership lives on the host, not here. The alternative — a member id list in this payload — would
/// make adding one host to a group a write to the group, so two clients adding two different hosts at once
/// would collide on one item. ThreeWayMerge has no set merge, so that collision would resolve by one
/// side winning and the other host quietly leaving the group. One pointer per host makes each of those two
/// operations a write to a different item, which cannot collide at all.
///
///
public sealed class VaultHostGroup : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so a group can be created offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted group: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
///
/// Soft-delete marker; a tombstone, so an offline client learns the group went away.
///
///
/// Deleting a group leaves every host that named it holding an id that resolves to nothing, and that is
/// the intended outcome rather than an oversight: those hosts fall back to the ungrouped heading. The
/// alternative is rewriting N host payloads inside one delete, which turns a single user action into N
/// pushes, N outbox rows and N chances to merge against a change nobody made.
///
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// A label that can be put on many hosts, as ciphertext.
///
///
///
/// 's shape exactly — an envelope and its bookkeeping — and the reasoning for the
/// missing name column is the same argument at higher stakes. A group name describes how one user
/// files their machines; a tag name describes what the machines are, and one tag spans twenty hosts
/// by design. A column here would give the operator a labelled map of every estate on the server, to sort a
/// list nothing server-side draws.
///
///
/// No membership, and no join table either. Which hosts wear this tag is a set of ids inside each
/// host's payload. SyncEntityType.HostTag reserves a slot for the join and it stays reserved: the one
/// thing a join buys over a set on the host — two people tagging the same host without one of them losing —
/// is already bought by the client's per-key three-way merge, and a second item type is a second table, a
/// second migration and a second pass through every rule in this file for nothing.
///
///
public sealed class VaultTag : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so a tag can be created offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted tag: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
///
/// Soft-delete marker; a tombstone, so an offline client learns the tag went away.
///
///
/// Deleting a tag leaves every host wearing it holding an id that resolves to nothing, and that is the
/// intended outcome: those hosts simply stop drawing that chip. Rewriting N host payloads inside one
/// delete would turn a single user action into N pushes, and each of those is a read-modify-write of a
/// set that can lose a merge — which would leave the vault holding a tag that is deleted and still worn.
///
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// An S3-compatible bucket and the credentials that reach it, as ciphertext.
///
///
///
/// 's shape, and it holds the same class of thing: a secret access key is a
/// password by another name, and everything around it — the endpoint, the region, the bucket — describes
/// somewhere the user keeps data. There is no plaintext column and there is no argument for one; the relay
/// does not dial a bucket, so ADR 0004's single concession has no analogue here.
///
///
/// The endpoint is inside the payload even though it is often s3.amazonaws.com. For everybody
/// self-hosting MinIO or Ceph it is an address on their own network, which is precisely the thing the host
/// table only stores in the clear when the relay cannot work without it. Nothing on the server dials this.
///
///
public sealed class VaultObjectStore : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so a bucket can be added offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted bucket and its keys: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
/// Soft-delete marker; a tombstone, so an offline client learns the bucket went away.
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// One connection that was made, as ciphertext.
///
///
///
/// Written once, at close, and never updated. That single decision is what makes a synced log
/// tractable: an entry that is created and then left alone needs no three-way merge, produces exactly one
/// outbox row, and cannot collide with the unique index that allows one outbox row per item. A connection
/// that is still open is shown from the workspace's own in-memory state instead, because that is where the
/// truth about a running shell actually lives.
///
///
/// Synced, and that is a deliberate trade rather than the obvious choice. A log kept on the machine
/// that produced it cannot be read by an administrator, cannot survive a reinstall, and cannot be checked
/// against anything. Keeping it here buys team auditing; what it costs is that the operator learns a user's
/// connection rate and timing from row counts and updated_at, even though every field inside
/// is sealed. ADR 0001 already concedes it cannot hide that class of metadata; this widens it, and says so.
///
///
/// The shape is 's: an envelope and its bookkeeping, no plaintext column of any
/// kind. Which machines somebody reaches and when is the single most revealing thing this schema could hold
/// in the clear, which is exactly why it holds none of it.
///
///
public sealed class VaultConnectionLogEntry : IVaultItem
{
/// Primary key. UUIDv7, generated by the client, so an entry can be written offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted entry: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
///
/// Client-visible, monotonic item version.
///
///
/// Always 1 in practice, because nothing updates a log entry. The column stays because the shared write
/// path needs it, and a kind that opted out of the version check would be a second write path.
///
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
///
/// Soft-delete marker; a tombstone, so an offline client learns the entry was pruned.
///
///
/// Retention is the only thing that deletes one of these, and it is a real tombstone that pushes — which
/// is why it is rate-limited rather than run on a timer. A prune that did not sync would delete the same
/// thousand entries again on every machine, for ever.
///
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// One create, edit or delete of a vault item, as ciphertext.
///
///
///
/// 's shape and its write-once rule, for a different history: what
/// changed in the keychain rather than what was connected to. The payload records the names of the
/// fields that changed and never their values, which is the same rule ADR 0006 imposes on the server's own
/// detail column. A log that recorded an old password would be a plaintext credential store with no
/// vault around it.
///
///
/// Note the deliberate asymmetry with the outbox, because it reads as a discrepancy otherwise: the outbox
/// coalesces two edits of one item into a single pending row, and this log does not — two edits are
/// two lines. The outbox describes what still has to be sent; this describes what somebody did.
///
///
public sealed class VaultActivityLogEntry : IVaultItem
{
/// Primary key. UUIDv7, generated by the client, so an entry can be written offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted entry: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
///
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
///
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}
///
/// A saved command, as ciphertext.
///
///
///
/// 's shape exactly, and for a stronger reason than symmetry. A snippet is a
/// command somebody runs on their infrastructure — systemctl restart against a named unit, a
/// psql line naming a database — and read as a set it describes what the estate is made of at least
/// as precisely as a list of hostnames does. There is nothing here the server could sort by that is worth
/// what holding it would cost, so there is no plaintext column to put it in even by mistake.
///
///
/// Whether the snippet runs on its own or merely gets typed into the terminal is inside the payload too, and
/// that is worth stating: it is the field the whole feature's safety rests on, so it must be one that
/// travels sealed and merges with everything else it belongs to, rather than a flag the server could see or
/// change.
///
///
public sealed class VaultSnippet : IVaultItem
{
/// Primary key. UUIDv7, generated by the client so a snippet can be written offline.
public Guid Id { get; set; }
/// Owning vault.
public Guid VaultId { get; set; }
/// Owning vault.
public Vault? Vault { get; set; }
/// The encrypted snippet: a DSH1 envelope. Opaque to the server.
public byte[] Payload { get; set; } = [];
/// The item's data key, wrapped under the vault key. Opaque.
public byte[]? DataKeyWrap { get; set; }
/// Reserved for per-item content keys wrapped to individual users; see docs/crypto.md §3.
public Guid? ContentKeyId { get; set; }
/// Vault key generation this payload was encrypted under.
public int KeyGeneration { get; set; }
/// AAD rule version, enabling a lazy re-encrypt-on-write migration later.
public short PayloadAadVersion { get; set; }
/// Client-visible, monotonic item version, used for expectedVersion checks.
public int Version { get; set; }
/// Latest change-log sequence touching this row, so a delta pull can join directly.
public long ChangeSequence { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Last modification timestamp.
public DateTimeOffset UpdatedAtUtc { get; set; }
/// Soft-delete marker; a tombstone, so an offline client learns the snippet went away.
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Who last modified it.
public Guid UpdatedByUserId { get; set; }
}