diff --git a/Directory.Packages.props b/Directory.Packages.props
index a752abe..f7c0bb1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,6 +26,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/DodoSSH.Infrastructure.Tests/PostgresFixture.cs b/tests/DodoSSH.Infrastructure.Tests/PostgresFixture.cs
new file mode 100644
index 0000000..0d862ec
--- /dev/null
+++ b/tests/DodoSSH.Infrastructure.Tests/PostgresFixture.cs
@@ -0,0 +1,58 @@
+using DodoSSH.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Testcontainers.PostgreSql;
+using Xunit;
+
+namespace DodoSSH.Infrastructure.Tests;
+
+///
+/// One PostgreSQL container per test assembly, migrated once.
+///
+///
+/// A container per test would dominate the runtime. Tests that write must therefore use distinct
+/// identifiers rather than assuming an empty database.
+///
+public sealed class PostgresFixture : IAsyncLifetime
+{
+ private readonly PostgreSqlContainer container = new PostgreSqlBuilder()
+ .WithImage("postgres:18-alpine")
+ .WithDatabase("dodossh")
+ .WithUsername("postgres")
+ .WithPassword("test")
+ .Build();
+
+ /// Connection string for the running container.
+ public string ConnectionString => container.GetConnectionString();
+
+ ///
+ public async ValueTask InitializeAsync()
+ {
+ await container.StartAsync();
+
+ await using var context = CreateContext();
+ await context.Database.MigrateAsync();
+ }
+
+ ///
+ public async ValueTask DisposeAsync() => await container.DisposeAsync();
+
+ /// Creates a context against the container.
+ public DodoDbContext CreateContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseNpgsql(ConnectionString, npgsql =>
+ npgsql.MigrationsHistoryTable("__EFMigrationsHistory", DodoDbContext.SchemaName))
+ .UseSnakeCaseNamingConvention()
+ .Options;
+
+ return new DodoDbContext(options);
+ }
+}
+
+/// Shares one container across every test class in the assembly.
+[CollectionDefinition(Name)]
+public sealed class PostgresCollection : ICollectionFixture
+{
+ /// Collection name.
+ public const string Name = "postgres";
+}
diff --git a/tests/DodoSSH.Infrastructure.Tests/SchemaConstraintTests.cs b/tests/DodoSSH.Infrastructure.Tests/SchemaConstraintTests.cs
new file mode 100644
index 0000000..352576b
--- /dev/null
+++ b/tests/DodoSSH.Infrastructure.Tests/SchemaConstraintTests.cs
@@ -0,0 +1,571 @@
+using DodoSSH.Domain;
+using Microsoft.EntityFrameworkCore;
+using Npgsql;
+
+namespace DodoSSH.Infrastructure.Tests;
+
+///
+/// Verifies the database enforces the invariants the design depends on.
+///
+///
+/// These are not tests of EF. They assert that the constraints hold even if application code has
+/// a bug, which is the whole reason for putting them in the schema. The relay-target constraint in
+/// particular is a security boundary: see ADR 0004.
+///
+[Collection(PostgresCollection.Name)]
+public sealed class SchemaConstraintTests(PostgresFixture fixture)
+{
+ private static readonly DateTimeOffset Now = new(2026, 7, 28, 12, 0, 0, TimeSpan.Zero);
+
+ [Fact]
+ public async Task Migration_AppliedCleanly_WithNoPendingModelChanges()
+ {
+ await using var context = fixture.CreateContext();
+
+ var applied = await context.Database.GetAppliedMigrationsAsync();
+ applied.ShouldNotBeEmpty();
+
+ (await context.Database.GetPendingMigrationsAsync()).ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task Xmin_IsASystemColumnAndNotDeclaredInTheTable()
+ {
+ // Npgsql maps xmin as a concurrency token without emitting a column. A negative attnum
+ // means PostgreSQL's own system column; a positive one would mean we had created a real
+ // column, which PostgreSQL would in fact have rejected.
+ await using var context = fixture.CreateContext();
+
+ var attnum = await context.Database
+ .SqlQuery($"""
+ SELECT a.attnum AS "Value"
+ FROM pg_attribute a
+ JOIN pg_class c ON c.oid = a.attrelid
+ JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE n.nspname = 'dodo' AND c.relname = 'vault' AND a.attname = 'xmin'
+ """)
+ .SingleAsync();
+
+ attnum.ShouldBeLessThan((short)0);
+ }
+
+ // ---- ADR 0004: the relay must never learn an address it was not granted ----
+
+ [Fact]
+ public async Task Host_WithRelayEnabled_RequiresHostnameAndPort()
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.Hosts.Add(NewHost(vault, relayEnabled: true, hostname: null, port: null));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_host_relay_target");
+ }
+
+ [Fact]
+ public async Task Host_WithRelayDisabled_MustNotCarryAnAddress()
+ {
+ // The important direction. If application code could store a plaintext address without
+ // the user opting into relay, the server would silently learn infrastructure it was never
+ // granted visibility of.
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.Hosts.Add(NewHost(vault, relayEnabled: false, hostname: "secret.internal", port: 22));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_host_relay_target");
+ }
+
+ [Fact]
+ public async Task Host_WithRelayEnabledAndAnAddress_IsAccepted()
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.Hosts.Add(NewHost(vault, relayEnabled: true, hostname: "bastion.internal", port: 22));
+
+ await context.SaveChangesAsync();
+ }
+
+ [Fact]
+ public async Task Host_WithRelayDisabledAndNoAddress_IsAccepted()
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.Hosts.Add(NewHost(vault, relayEnabled: false, hostname: null, port: null));
+
+ await context.SaveChangesAsync();
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(65536)]
+ [InlineData(-1)]
+ public async Task Host_RejectsAnOutOfRangePort(int port)
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.Hosts.Add(NewHost(vault, relayEnabled: true, hostname: "h.internal", port: port));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_host_port_range");
+ }
+
+ // ---- Vault ownership ----
+
+ [Fact]
+ public async Task Vault_MustHaveExactlyOneOwner()
+ {
+ await using var context = fixture.CreateContext();
+ var user = await SeedUserAsync(context);
+ var team = SeedTeam(context, user.Id);
+ await context.SaveChangesAsync();
+
+ // Both owners set: permission resolution would have no defined answer.
+ context.Vaults.Add(new Vault
+ {
+ Id = Guid.CreateVersion7(),
+ Name = "ambiguous",
+ OwnerKind = VaultOwnerKind.Personal,
+ OwnerUserId = user.Id,
+ TeamId = team.Id,
+ CreatedAtUtc = Now,
+ UpdatedAtUtc = Now,
+ });
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_vault_owner");
+ }
+
+ [Fact]
+ public async Task Vault_PersonalWithoutAnOwnerUser_IsRejected()
+ {
+ await using var context = fixture.CreateContext();
+
+ context.Vaults.Add(new Vault
+ {
+ Id = Guid.CreateVersion7(),
+ Name = "orphan",
+ OwnerKind = VaultOwnerKind.Personal,
+ CreatedAtUtc = Now,
+ UpdatedAtUtc = Now,
+ });
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_vault_owner");
+ }
+
+ // ---- Grants ----
+
+ [Fact]
+ public async Task MemberGrant_RequiresARecipient()
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.VaultKeyGrants.Add(NewGrant(vault, GrantKind.Member, recipientUserId: null));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_vault_key_grant_recipient");
+ }
+
+ [Fact]
+ public async Task RecoveryGrant_MustNotNameARecipient()
+ {
+ // Recovery and escrow grants are wrapped to a key, not to a user. Allowing a recipient
+ // would make it ambiguous whether revoking that user revokes recovery.
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+ var user = await context.Users.FirstAsync(u => u.Id == vault.OwnerUserId);
+
+ context.VaultKeyGrants.Add(NewGrant(vault, GrantKind.Recovery, recipientUserId: user.Id));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_vault_key_grant_recipient");
+ }
+
+ [Fact]
+ public async Task RecoveryGrant_IsAcceptedWithoutARecipient()
+ {
+ // Recovery must be expressible in the very first schema, or every vault created before it
+ // existed would be permanently unrecoverable.
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ context.VaultKeyGrants.Add(NewGrant(vault, GrantKind.Recovery, recipientUserId: null));
+
+ await context.SaveChangesAsync();
+ }
+
+ [Fact]
+ public async Task MemberGrant_IsUniquePerRecipientPerGeneration()
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+ var userId = vault.OwnerUserId!.Value;
+
+ context.VaultKeyGrants.Add(NewGrant(vault, GrantKind.Member, userId));
+ await context.SaveChangesAsync();
+
+ context.VaultKeyGrants.Add(NewGrant(vault, GrantKind.Member, userId));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ exception.InnerException.ShouldBeOfType()
+ .SqlState.ShouldBe(PostgresErrorCodes.UniqueViolation);
+ }
+
+ [Fact]
+ public async Task MemberGrant_MayBeReissuedAfterRevocation()
+ {
+ // The uniqueness filter is on revocation, not deletion: revoked grants are retained so
+ // audit history stays intact, and a rewrap must still be possible.
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+ var userId = vault.OwnerUserId!.Value;
+
+ var first = NewGrant(vault, GrantKind.Member, userId);
+ context.VaultKeyGrants.Add(first);
+ await context.SaveChangesAsync();
+
+ first.RevokedAtUtc = Now;
+ first.State = GrantState.Revoked;
+ await context.SaveChangesAsync();
+
+ context.VaultKeyGrants.Add(NewGrant(vault, GrantKind.Member, userId));
+ await context.SaveChangesAsync();
+ }
+
+ // ---- Key wraps ----
+
+ [Fact]
+ public async Task PassphraseWrap_RequiresKdfParameters()
+ {
+ // A password-derived wrap without its parameters is permanently unopenable.
+ await using var context = fixture.CreateContext();
+ var user = await SeedUserAsync(context);
+
+ context.UserKeyWraps.Add(new UserKeyWrap
+ {
+ Id = Guid.CreateVersion7(),
+ UserId = user.Id,
+ Kind = UserKeyWrapKind.Passphrase,
+ Wrap = [1, 2, 3],
+ WrapVersion = 1,
+ CreatedAtUtc = Now,
+ });
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_user_key_wrap_kdf");
+ }
+
+ [Fact]
+ public async Task DeviceWrap_RequiresADevice()
+ {
+ await using var context = fixture.CreateContext();
+ var user = await SeedUserAsync(context);
+
+ context.UserKeyWraps.Add(new UserKeyWrap
+ {
+ Id = Guid.CreateVersion7(),
+ UserId = user.Id,
+ Kind = UserKeyWrapKind.Device,
+ Wrap = [1, 2, 3],
+ WrapVersion = 1,
+ CreatedAtUtc = Now,
+ });
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ ConstraintName(exception).ShouldBe("ck_user_key_wrap_device");
+ }
+
+ [Fact]
+ public async Task PassphraseWrap_WithParameters_IsAccepted()
+ {
+ await using var context = fixture.CreateContext();
+ var user = await SeedUserAsync(context);
+
+ context.UserKeyWraps.Add(new UserKeyWrap
+ {
+ Id = Guid.CreateVersion7(),
+ UserId = user.Id,
+ Kind = UserKeyWrapKind.Passphrase,
+ Wrap = [1, 2, 3],
+ WrapVersion = 1,
+ KdfAlgorithm = "argon2id",
+ KdfSalt = new byte[16],
+ KdfMemoryKibibytes = 262144,
+ KdfPasses = 4,
+ KdfParallelism = 1,
+ CreatedAtUtc = Now,
+ });
+
+ await context.SaveChangesAsync();
+ }
+
+ // ---- Identity uniqueness ----
+
+ [Fact]
+ public async Task User_IsUniquePerIssuerAndSubject()
+ {
+ await using var context = fixture.CreateContext();
+ var subject = Guid.NewGuid().ToString();
+
+ context.Users.Add(NewUser("https://idp.example", subject));
+ await context.SaveChangesAsync();
+
+ context.Users.Add(NewUser("https://idp.example", subject));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ exception.InnerException.ShouldBeOfType()
+ .SqlState.ShouldBe(PostgresErrorCodes.UniqueViolation);
+ }
+
+ [Fact]
+ public async Task User_MayShareASubjectAcrossDifferentIssuers()
+ {
+ // Multi-issuer from the start: the issuer is part of the identity, not a detail.
+ await using var context = fixture.CreateContext();
+ var subject = Guid.NewGuid().ToString();
+
+ context.Users.Add(NewUser("https://idp-a.example", subject));
+ context.Users.Add(NewUser("https://idp-b.example", subject));
+
+ await context.SaveChangesAsync();
+ }
+
+ [Fact]
+ public async Task Email_IsCaseInsensitivelyUnique()
+ {
+ // citext: an attacker must not be able to register Alice@x with alice@x already present.
+ await using var context = fixture.CreateContext();
+ var local = $"user{Guid.NewGuid():N}";
+
+ var first = NewUser("https://idp.example", Guid.NewGuid().ToString());
+ first.Email = $"{local}@example.com";
+ context.Users.Add(first);
+ await context.SaveChangesAsync();
+
+ var second = NewUser("https://idp.example", Guid.NewGuid().ToString());
+ second.Email = $"{local.ToUpperInvariant()}@EXAMPLE.COM";
+ context.Users.Add(second);
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ exception.InnerException.ShouldBeOfType()
+ .SqlState.ShouldBe(PostgresErrorCodes.UniqueViolation);
+ }
+
+ [Fact]
+ public async Task UserKey_AllowsOnlyOneCurrentGenerationPerUser()
+ {
+ // Two current keys would make it ambiguous which one to wrap a vault key to.
+ await using var context = fixture.CreateContext();
+ var user = await SeedUserAsync(context);
+
+ context.UserKeys.Add(NewUserKey(user.Id, generation: 1, isCurrent: true));
+ await context.SaveChangesAsync();
+
+ context.UserKeys.Add(NewUserKey(user.Id, generation: 2, isCurrent: true));
+
+ var exception = await Should.ThrowAsync(() => context.SaveChangesAsync());
+ exception.InnerException.ShouldBeOfType()
+ .SqlState.ShouldBe(PostgresErrorCodes.UniqueViolation);
+ }
+
+ [Fact]
+ public async Task UserKey_AllowsManySupersededGenerations()
+ {
+ await using var context = fixture.CreateContext();
+ var user = await SeedUserAsync(context);
+
+ context.UserKeys.Add(NewUserKey(user.Id, generation: 1, isCurrent: false));
+ context.UserKeys.Add(NewUserKey(user.Id, generation: 2, isCurrent: false));
+ context.UserKeys.Add(NewUserKey(user.Id, generation: 3, isCurrent: true));
+
+ await context.SaveChangesAsync();
+ }
+
+ // ---- Sync log ----
+
+ [Fact]
+ public async Task SyncChange_SequenceIsDatabaseAssignedAndMonotonic()
+ {
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ var first = NewChange(vault.Id);
+ var second = NewChange(vault.Id);
+ context.SyncChanges.AddRange(first, second);
+ await context.SaveChangesAsync();
+
+ first.Sequence.ShouldBeGreaterThan(0);
+ second.Sequence.ShouldBeGreaterThan(first.Sequence);
+ }
+
+ [Fact]
+ public async Task SyncChange_RejectsACallerSuppliedSequence()
+ {
+ // Identity ALWAYS. If anything could choose its own sequence, cursor ordering would stop
+ // meaning anything and delta sync would silently skip changes.
+ await using var context = fixture.CreateContext();
+ var vault = await SeedVaultAsync(context);
+
+ await using var connection = new NpgsqlConnection(fixture.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = """
+ INSERT INTO dodo.sync_change
+ (sequence, vault_id, entity_type, entity_id, operation, revision, actor_user_id, occurred_at_utc)
+ VALUES (999999, @vault, 1, @entity, 1, 1, @actor, now())
+ """;
+ command.Parameters.AddWithValue("vault", vault.Id);
+ command.Parameters.AddWithValue("entity", Guid.CreateVersion7());
+ command.Parameters.AddWithValue("actor", vault.OwnerUserId!.Value);
+
+ var exception = await Should.ThrowAsync(() => command.ExecuteNonQueryAsync());
+
+ // 428C9 is generated_always: PostgreSQL refuses a value for a GENERATED ALWAYS column.
+ exception.SqlState.ShouldBe("428C9");
+ }
+
+ // ---- Concurrency ----
+
+ [Fact]
+ public async Task Xmin_DetectsAConcurrentUpdate()
+ {
+ // Two clients racing a rekey must not silently overwrite one another.
+ await using var writer = fixture.CreateContext();
+ var vault = await SeedVaultAsync(writer);
+
+ await using var contextA = fixture.CreateContext();
+ await using var contextB = fixture.CreateContext();
+
+ var asA = await contextA.Vaults.SingleAsync(v => v.Id == vault.Id);
+ var asB = await contextB.Vaults.SingleAsync(v => v.Id == vault.Id);
+
+ asA.KeyGeneration = 2;
+ await contextA.SaveChangesAsync();
+
+ asB.KeyGeneration = 3;
+ await Should.ThrowAsync(() => contextB.SaveChangesAsync());
+ }
+
+ // ---- Helpers ----
+
+ private static string? ConstraintName(DbUpdateException exception) =>
+ (exception.InnerException as PostgresException)?.ConstraintName;
+
+ private static UserAccount NewUser(string issuer, string subject) => new()
+ {
+ Id = Guid.CreateVersion7(),
+ Issuer = issuer,
+ Subject = subject,
+ Status = UserStatus.Active,
+ CreatedAtUtc = Now,
+ UpdatedAtUtc = Now,
+ };
+
+ private static async Task SeedUserAsync(DodoDbContext context)
+ {
+ var user = NewUser("https://idp.example", Guid.NewGuid().ToString());
+ context.Users.Add(user);
+ await context.SaveChangesAsync();
+ return user;
+ }
+
+ private static Team SeedTeam(DodoDbContext context, Guid createdBy)
+ {
+ var team = new Team
+ {
+ Id = Guid.CreateVersion7(),
+ Name = "Team",
+ Slug = $"team-{Guid.NewGuid():N}",
+ CreatedByUserId = createdBy,
+ CreatedAtUtc = Now,
+ };
+
+ context.Teams.Add(team);
+ return team;
+ }
+
+ private static async Task SeedVaultAsync(DodoDbContext context)
+ {
+ var user = await SeedUserAsync(context);
+
+ var vault = new Vault
+ {
+ Id = Guid.CreateVersion7(),
+ Name = "Personal",
+ OwnerKind = VaultOwnerKind.Personal,
+ OwnerUserId = user.Id,
+ KeyGeneration = 1,
+ CreatedAtUtc = Now,
+ UpdatedAtUtc = Now,
+ };
+
+ context.Vaults.Add(vault);
+ await context.SaveChangesAsync();
+ return vault;
+ }
+
+ private static Host NewHost(Vault vault, bool relayEnabled, string? hostname, int? port) => new()
+ {
+ Id = Guid.CreateVersion7(),
+ VaultId = vault.Id,
+ Payload = [1, 2, 3, 4],
+ KeyGeneration = vault.KeyGeneration,
+ PayloadAadVersion = 1,
+ RelayEnabled = relayEnabled,
+ Hostname = hostname,
+ Port = port,
+ Version = 1,
+ CreatedAtUtc = Now,
+ UpdatedAtUtc = Now,
+ CreatedByUserId = vault.OwnerUserId!.Value,
+ UpdatedByUserId = vault.OwnerUserId!.Value,
+ };
+
+ private static VaultKeyGrant NewGrant(Vault vault, GrantKind kind, Guid? recipientUserId) => new()
+ {
+ Id = Guid.CreateVersion7(),
+ VaultId = vault.Id,
+ KeyGeneration = vault.KeyGeneration,
+ Kind = kind,
+ RecipientUserId = recipientUserId,
+ RecipientKeyFingerprint = new byte[32],
+ WrappedKey = [1, 2, 3],
+ GranterUserId = vault.OwnerUserId!.Value,
+ GranterKeyFingerprint = new byte[32],
+ Signature = new byte[64],
+ State = GrantState.Active,
+ CreatedAtUtc = Now,
+ };
+
+ private static UserKey NewUserKey(Guid userId, int generation, bool isCurrent) => new()
+ {
+ Id = Guid.CreateVersion7(),
+ UserId = userId,
+ Generation = generation,
+ EncryptionPublicKey = new byte[32],
+ SigningPublicKey = new byte[32],
+ FingerprintSha256 = Guid.NewGuid().ToByteArray().Concat(Guid.NewGuid().ToByteArray()).ToArray(),
+ Statement = "{}",
+ StatementSignature = new byte[64],
+ IsCurrent = isCurrent,
+ CreatedAtUtc = Now,
+ };
+
+ private static SyncChange NewChange(Guid vaultId) => new()
+ {
+ VaultId = vaultId,
+ EntityType = ChangeEntityType.Host,
+ EntityId = Guid.CreateVersion7(),
+ Operation = ChangeOperation.Upsert,
+ Revision = 1,
+ ActorUserId = Guid.CreateVersion7(),
+ OccurredAtUtc = Now,
+ };
+}
diff --git a/tests/DodoSSH.Infrastructure.Tests/packages.lock.json b/tests/DodoSSH.Infrastructure.Tests/packages.lock.json
new file mode 100644
index 0000000..8382bd2
--- /dev/null
+++ b/tests/DodoSSH.Infrastructure.Tests/packages.lock.json
@@ -0,0 +1,449 @@
+{
+ "version": 2,
+ "dependencies": {
+ "net10.0": {
+ "Meziantou.Analyzer": {
+ "type": "Direct",
+ "requested": "[3.0.134, )",
+ "resolved": "3.0.134",
+ "contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
+ },
+ "Microsoft.CodeAnalysis.BannedApiAnalyzers": {
+ "type": "Direct",
+ "requested": "[5.6.0, )",
+ "resolved": "5.6.0",
+ "contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
+ },
+ "Npgsql.EntityFrameworkCore.PostgreSQL": {
+ "type": "Direct",
+ "requested": "[10.0.3, )",
+ "resolved": "10.0.3",
+ "contentHash": "IPGrrZnRkuW7OlHDhUESZz4G5DLkW7Nej/O3Cx+0iTsgyU5XJxBgpsvTHLloo3WWuAKKbDHXBvWPVkX1deRh1Q==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)",
+ "Npgsql": "10.0.3"
+ }
+ },
+ "NSubstitute": {
+ "type": "Direct",
+ "requested": "[6.0.0, )",
+ "resolved": "6.0.0",
+ "contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==",
+ "dependencies": {
+ "Castle.Core": "5.1.1"
+ }
+ },
+ "Shouldly": {
+ "type": "Direct",
+ "requested": "[4.3.0, )",
+ "resolved": "4.3.0",
+ "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
+ "dependencies": {
+ "DiffEngine": "11.3.0",
+ "EmptyFiles": "4.4.0"
+ }
+ },
+ "Testcontainers.PostgreSql": {
+ "type": "Direct",
+ "requested": "[4.13.0, )",
+ "resolved": "4.13.0",
+ "contentHash": "2ow4AE8drI9iA9Fr4ycAPusXPB1lJfQyyNONSMLE/XqLpm8VuNAh3pK38fOjkOWtTrnD03s4hAIdl4036Ik69A==",
+ "dependencies": {
+ "Testcontainers": "4.13.0"
+ }
+ },
+ "xunit.v3": {
+ "type": "Direct",
+ "requested": "[3.2.2, )",
+ "resolved": "3.2.2",
+ "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==",
+ "dependencies": {
+ "xunit.v3.mtp-v1": "[3.2.2]"
+ }
+ },
+ "Castle.Core": {
+ "type": "Transitive",
+ "resolved": "5.1.1",
+ "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
+ "dependencies": {
+ "System.Diagnostics.EventLog": "6.0.0"
+ }
+ },
+ "DiffEngine": {
+ "type": "Transitive",
+ "resolved": "11.3.0",
+ "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
+ "dependencies": {
+ "EmptyFiles": "4.4.0",
+ "System.Management": "6.0.1"
+ }
+ },
+ "Docker.DotNet.Enhanced": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "nGicLwvd42FhRk+khY5uS6cx49ErNdwYKnYBg0F4m4BDKLp/R77AVmmN9xAiqI3W/wN5ZCHkdUhgxf5ORkZuFQ==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3",
+ "Docker.DotNet.Enhanced.LegacyHttp": "4.3.3",
+ "Docker.DotNet.Enhanced.NPipe": "4.3.3",
+ "Docker.DotNet.Enhanced.NativeHttp": "4.3.3",
+ "Docker.DotNet.Enhanced.Unix": "4.3.3",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ },
+ "Docker.DotNet.Enhanced.Handler.Abstractions": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "9Cp8hOgtynixcDoAs9lnEaQosluojSYmiW3fsLsLIVfZjlq/fznSIZNUhnmyT4Xo1Iyuok/y49WL/25O47u0Pw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ },
+ "Docker.DotNet.Enhanced.LegacyHttp": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "7j3M16emv9PAQN7VwFn23xLYNj8GJmwPOcogveHkaWnOCqiC+anRaNKQwqIBNApM1AuwZKivehTKTPmmrjUUnw==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
+ }
+ },
+ "Docker.DotNet.Enhanced.NativeHttp": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "iNzK+jRFeEMobSA7l/h4ARwCKOOefOWtVN5/RB0ft6/6H6IQXvVUuOgGyZAjYLBT7TsyClRYno2B904f3dtBuQ==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
+ }
+ },
+ "Docker.DotNet.Enhanced.NPipe": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "ZTLYufuEfY0e6qLOgeH9QgXx2KYuoABRVaY5A8rsggyLgYqbDj9rCRfVAhHPCUv83S7pVxDHy+Tvm/BnxjWVpg==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
+ }
+ },
+ "Docker.DotNet.Enhanced.Unix": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "ypo8qNbmvHw1t9VfpRTMogCw2vht6VjkXzlGYUUeP2H2bf83USURdla1maW1njn2oq2rfLUFOGMfmt+A37QU2w==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
+ }
+ },
+ "Docker.DotNet.Enhanced.X509": {
+ "type": "Transitive",
+ "resolved": "4.3.3",
+ "contentHash": "oBDibWezEv4hgj3RIQxI3DVcxkNV1MdrD0d/jhjUu+h3DL+qc0wlkQva15kkwMatXmC/hWp1VP0DMoFXe+BmEw==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
+ }
+ },
+ "EmptyFiles": {
+ "type": "Transitive",
+ "resolved": "4.4.0",
+ "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
+ },
+ "Microsoft.ApplicationInsights": {
+ "type": "Transitive",
+ "resolved": "2.23.0",
+ "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
+ },
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q=="
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.10",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Options": "10.0.10",
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "10.0.10",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Options": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Primitives": "10.0.10"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ=="
+ },
+ "Microsoft.Testing.Extensions.Telemetry": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==",
+ "dependencies": {
+ "Microsoft.ApplicationInsights": "2.23.0",
+ "Microsoft.Testing.Platform": "1.9.1"
+ }
+ },
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==",
+ "dependencies": {
+ "Microsoft.Testing.Platform": "1.9.1"
+ }
+ },
+ "Microsoft.Testing.Platform": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA=="
+ },
+ "Microsoft.Testing.Platform.MSBuild": {
+ "type": "Transitive",
+ "resolved": "1.9.1",
+ "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==",
+ "dependencies": {
+ "Microsoft.Testing.Platform": "1.9.1"
+ }
+ },
+ "Microsoft.Win32.Registry": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg=="
+ },
+ "Npgsql": {
+ "type": "Transitive",
+ "resolved": "10.0.3",
+ "contentHash": "7nb5YzXuvWWJxB0J8DiyL3we+X4FOctZrt0fIBnucOIaIevFEEwGQVZKtiu9olXdlNAK1eNgqSral6r/jlhI4w==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0"
+ }
+ },
+ "SharpZipLib": {
+ "type": "Transitive",
+ "resolved": "1.4.2",
+ "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A=="
+ },
+ "SSH.NET": {
+ "type": "Transitive",
+ "resolved": "2025.1.0",
+ "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
+ "dependencies": {
+ "BouncyCastle.Cryptography": "2.6.2",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3"
+ }
+ },
+ "System.CodeDom": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "6.0.0",
+ "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
+ },
+ "System.Management": {
+ "type": "Transitive",
+ "resolved": "6.0.1",
+ "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
+ "dependencies": {
+ "System.CodeDom": "6.0.0"
+ }
+ },
+ "Testcontainers": {
+ "type": "Transitive",
+ "resolved": "4.13.0",
+ "contentHash": "j8vi9jPBNSwaraGGx8w+2gtZyWrlbKxdhiGMS3nektg+KiwjFWx9ghCjs57EoQfvI+IAbzti0oQJupQChwgMog==",
+ "dependencies": {
+ "Docker.DotNet.Enhanced": "4.3.3",
+ "Docker.DotNet.Enhanced.X509": "4.3.3",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
+ "SSH.NET": "2025.1.0",
+ "SharpZipLib": "1.4.2"
+ }
+ },
+ "xunit.analyzers": {
+ "type": "Transitive",
+ "resolved": "1.27.0",
+ "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g=="
+ },
+ "xunit.v3.assert": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA=="
+ },
+ "xunit.v3.common": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "6.0.0"
+ }
+ },
+ "xunit.v3.core.mtp-v1": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==",
+ "dependencies": {
+ "Microsoft.Testing.Extensions.Telemetry": "1.9.1",
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1",
+ "Microsoft.Testing.Platform": "1.9.1",
+ "Microsoft.Testing.Platform.MSBuild": "1.9.1",
+ "xunit.v3.extensibility.core": "[3.2.2]",
+ "xunit.v3.runner.inproc.console": "[3.2.2]"
+ }
+ },
+ "xunit.v3.extensibility.core": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==",
+ "dependencies": {
+ "xunit.v3.common": "[3.2.2]"
+ }
+ },
+ "xunit.v3.mtp-v1": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==",
+ "dependencies": {
+ "xunit.analyzers": "1.27.0",
+ "xunit.v3.assert": "[3.2.2]",
+ "xunit.v3.core.mtp-v1": "[3.2.2]"
+ }
+ },
+ "xunit.v3.runner.common": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==",
+ "dependencies": {
+ "Microsoft.Win32.Registry": "[5.0.0]",
+ "xunit.v3.common": "[3.2.2]"
+ }
+ },
+ "xunit.v3.runner.inproc.console": {
+ "type": "Transitive",
+ "resolved": "3.2.2",
+ "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==",
+ "dependencies": {
+ "xunit.v3.extensibility.core": "[3.2.2]",
+ "xunit.v3.runner.common": "[3.2.2]"
+ }
+ },
+ "dodossh.domain": {
+ "type": "Project"
+ },
+ "dodossh.infrastructure": {
+ "type": "Project",
+ "dependencies": {
+ "DodoSSH.Domain": "[1.0.0, )",
+ "EFCore.NamingConventions": "[10.0.1, )",
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.3, )"
+ }
+ },
+ "BouncyCastle.Cryptography": {
+ "type": "CentralTransitive",
+ "requested": "[2.6.2, )",
+ "resolved": "2.6.2",
+ "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
+ },
+ "EFCore.NamingConventions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1"
+ }
+ },
+ "Microsoft.EntityFrameworkCore": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10"
+ }
+ },
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "10.0.10",
+ "Microsoft.Extensions.Caching.Memory": "10.0.10",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
+ "Microsoft.Extensions.Logging": "10.0.10"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file