Public Access
Settings → Security gains a SPELUNK section: an API address and a key, saved as a new keychain item, a Spelunk key (SyncEntityType 14, AadResourceType 17, server table spelunk_key via the AddSpelunkKeyItem migration). It is ciphertext only on the server, lives in the personal vault, syncs like every secret, and holds the key together with the one address it may be sent to. The key box empties once saved; the list shows name, address and how many hosts use each key, never the key. Saving a key for an address that already has one replaces it, and deleting goes through the shared confirmation card with the hosts that would stop connecting. A host's route names a saved key where it named a credential, in the same apiKeyCredentialId field, so no host schema changes and an older build reads such a host as one whose key credential has gone. At connect a saved key is sent only to its own address; a host naming it with another Spelunk address is refused in words. Keys kept as a credential's password still work. The editor offers saved keys first and picks the one saved for an address as it is typed. A link from a Spelunk with a saved key now connects straight away, including the first one: saving the key is the user's say-so that the first-link editor used to ask for. The address and key merge as one pair, so two concurrent edits can never pair one device's key with another's address, and the key never reaches the conflict log.
133 lines
4.1 KiB
C#
133 lines
4.1 KiB
C#
namespace DodoSSH.Domain;
|
|
|
|
/// <summary>What a change did to an entity.</summary>
|
|
public enum ChangeOperation
|
|
{
|
|
/// <summary>Not a legal value.</summary>
|
|
Unspecified = 0,
|
|
|
|
/// <summary>Created or modified.</summary>
|
|
Upsert = 1,
|
|
|
|
/// <summary>Soft-deleted, leaving a tombstone.</summary>
|
|
Delete = 2,
|
|
}
|
|
|
|
/// <summary>The kind of item a change refers to. Mirrors the contract enum; append only.</summary>
|
|
public enum ChangeEntityType
|
|
{
|
|
/// <summary>Not a legal value.</summary>
|
|
Unspecified = 0,
|
|
|
|
/// <summary>An SSH host.</summary>
|
|
SshHost = 1,
|
|
|
|
/// <summary>A credential. M2.</summary>
|
|
Credential = 2,
|
|
|
|
/// <summary>An SSH key pair. M2.</summary>
|
|
SshKey = 3,
|
|
|
|
/// <summary>A host group. M2.</summary>
|
|
HostGroup = 4,
|
|
|
|
/// <summary>A tag. M2.</summary>
|
|
Tag = 5,
|
|
|
|
/// <summary>A host-to-tag association. M2.</summary>
|
|
HostTag = 6,
|
|
|
|
/// <summary>A host-to-credential association. M2.</summary>
|
|
HostCredential = 7,
|
|
|
|
/// <summary>A snippet. M2.</summary>
|
|
Snippet = 8,
|
|
|
|
/// <summary>A port forward. M2.</summary>
|
|
PortForward = 9,
|
|
|
|
/// <summary>A known SSH host key. M2.</summary>
|
|
KnownHostKey = 10,
|
|
|
|
/// <summary>One connection that was made, and how long it lasted. M2.</summary>
|
|
ConnectionLogEntry = 11,
|
|
|
|
/// <summary>One create, edit or delete of a vault item. M2.</summary>
|
|
ActivityLogEntry = 12,
|
|
|
|
/// <summary>An S3-compatible bucket, and the credentials that reach it. M2.</summary>
|
|
ObjectStore = 13,
|
|
|
|
/// <summary>A Spelunk API key, and the one Spelunk address it may be sent to.</summary>
|
|
SpelunkKey = 14,
|
|
}
|
|
|
|
/// <summary>
|
|
/// One entry in a vault's change log, which is what delta sync reads.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <see cref="Sequence"/> comes from a <c>bigserial</c>, and that carries a trap worth stating
|
|
/// where the code lives: sequence values are handed out <em>before</em> commit. If transaction A
|
|
/// takes 5 and B takes 6 but B commits first, a reader that advances its cursor to 6 permanently
|
|
/// misses 5 — silent sync corruption that only appears under concurrent writes to one vault.
|
|
/// </para>
|
|
/// <para>
|
|
/// Every push therefore takes a per-vault transaction-scoped advisory lock as its first
|
|
/// statement, so sequence order equals commit order. See ADR 0003.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class VaultChange
|
|
{
|
|
/// <summary>Monotonic sequence. Database-generated.</summary>
|
|
public long Sequence { get; set; }
|
|
|
|
/// <summary>Owning vault. Cursors are scoped per vault.</summary>
|
|
public Guid VaultId { get; set; }
|
|
|
|
/// <summary>Kind of item.</summary>
|
|
public ChangeEntityType EntityType { get; set; }
|
|
|
|
/// <summary>The item.</summary>
|
|
public Guid EntityId { get; set; }
|
|
|
|
/// <summary>What happened.</summary>
|
|
public ChangeOperation Operation { get; set; }
|
|
|
|
/// <summary>The item's version after the change.</summary>
|
|
public int Revision { get; set; }
|
|
|
|
/// <summary>Who made the change.</summary>
|
|
public Guid ActorUserId { get; set; }
|
|
|
|
/// <summary>When it was recorded.</summary>
|
|
public DateTimeOffset OccurredAtUtc { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Records that a client operation was applied, so a retry is a no-op.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Keyed on the client-generated operation id, which makes retries exactly-once at
|
|
/// <em>operation</em> granularity. A batch-level idempotency key alone would not: a client that
|
|
/// times out mid-push and retries with a partially overlapping batch would otherwise double-apply
|
|
/// the operations that did land.
|
|
/// </remarks>
|
|
public sealed class SyncOperationReceipt
|
|
{
|
|
/// <summary>The client-generated operation id. Primary key.</summary>
|
|
public Guid OperationId { get; set; }
|
|
|
|
/// <summary>Vault the operation targeted.</summary>
|
|
public Guid VaultId { get; set; }
|
|
|
|
/// <summary>Change-log sequence produced, when the operation was applied.</summary>
|
|
public long? AppliedChangeSequence { get; set; }
|
|
|
|
/// <summary>The item's version after the operation.</summary>
|
|
public int? ResultVersion { get; set; }
|
|
|
|
/// <summary>Creation timestamp.</summary>
|
|
public DateTimeOffset CreatedAtUtc { get; set; }
|
|
}
|