Skopka.Chat.Protocol 0.19.0

dotnet add package Skopka.Chat.Protocol --version 0.19.0
                    
NuGet\Install-Package Skopka.Chat.Protocol -Version 0.19.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Skopka.Chat.Protocol" Version="0.19.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Skopka.Chat.Protocol" Version="0.19.0" />
                    
Directory.Packages.props
<PackageReference Include="Skopka.Chat.Protocol" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Skopka.Chat.Protocol --version 0.19.0
                    
#r "nuget: Skopka.Chat.Protocol, 0.19.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Skopka.Chat.Protocol@0.19.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Skopka.Chat.Protocol&version=0.19.0
                    
Install as a Cake Addin
#tool nuget:?package=Skopka.Chat.Protocol&version=0.19.0
                    
Install as a Cake Tool

Skopka.Chat

Skopka.Chat — переиспользуемый транспорт-независимый движок личного и малого группового чата для .NET 10. Сервер регистрирует публичные данные устройств, хранит и доставляет зашифрованные конверты, но не получает закрытые ключи и не имеет API расшифровки.

Security status: это ограниченный E2EE MVP, а не реализация Signal и не прошедший аудит production-протокол. В v1 нет Double Ratchet, forward secrecy относительно компрометации долгосрочного ключа получателя и post-compromise security. Перед использованием прочитайте threat model, ADR по криптографии и ограничения MVP.

Пакеты

Версия 0.19.0 ещё не опубликована. Текущая ветка разработки добавляет первый серверный event-срез: envelope и PostgreSQL Outbox фиксируются одной транзакцией, а отдельный optional Skopka.Chat.Server.Kafka публикует metadata-only событие с at-least-once семантикой. Синхронные HTTP-команды, polling/history и E2EE-форматы не меняются; Kafka не становится источником истины. См. руководство по server events и ADR 0022.

Версия 0.18.0 добавляет малые групповые диалоги до 64 участников, роли владельца/администратора/участника, атомарную revision-модель PostgreSQL и recipient-per-device E2EE fan-out. Новые участники не получают старую историю автоматически, а удалённые сохраняют уже доставленные данные, но перестают получать новые конверты. Структурированные @user и @all находятся внутри нового content-v4; обычный текст без упоминаний остаётся byte-identical content-v1. @all считается действительным только от владельца или администратора на стороне получателя. Это bounded MVP без MLS, ratchet и криптографически защищённого журнала состава; подробности — ADR 0021.

Версия 0.17.0 добавляет browser-bound открытие локального vault без ежедневной фразы. Новый браузер создаёт неэкспортируемый WebCrypto-ключ в IndexedDB, а существующий vault 0.16 можно один раз открыть прежней фразой и привязать без перешифрования записей или замены DeviceId. Ключ не отправляется на сервер. Для явно одноразовых установок DiscardLegacyAsync позволяет хосту после информированного подтверждения удалить только legacy-scope и создать новый vault. Защита рассчитана на кражу экспортированных ciphertext, но не на вредоносный код того же origin или доступ к разблокированному профилю браузера.

Версия 0.16.0 добавляет opt-in E2EE backup текстовой истории, восстановление без старого устройства, отдельный случайный recovery key, Browser/MAUI адаптеры и account-scoped PostgreSQL/HTTP. Device identity и outbox не переносятся; импорт явно помечен как recovery-key-authenticated, а не повторно проверенная подпись отправителя. Подключение и ограничения — резервные копии. Форматы envelope/content/binding не изменены. Для backup требуется явное подключение новых серверных маршрутов и отдельная PostgreSQL migration; обычный чат продолжает работать без них. Согласованный набор по-прежнему состоит из 23 пакетов.

Версия 0.15.0 добавляет Skopka.Chat.Client.Browser: standalone Blazor WebAssembly, общий с native клиентский движок, локальная libsodium.js, зашифрованные IndexedDB identity/history/outbox и cookie/BFF-интеграция без OAuth-токенов в браузере. Старые NSec-ключи и серверные протоколы сохранены. Проверяются реальные Chromium и Firefox, включая взаимное E2EE с .NET, перезагрузки и конкурентные вкладки. Описание, ограничения и запускаемый пример — в руководстве браузерного клиента.

В этот же релиз входят Skopka.Chat.Bots, .Bots.Sqlite и .Bots.AspNetCore: текстовый bot-runtime, долговечная очередь и приватный HTTP-шлюз у владельца бота. Основной сервер не расшифровывает сообщения. Подтверждение оператора, экран согласия и серверные правила доступа подключает приложение-хост; без них включать ботов нельзя. См. интеграцию ботов. Согласованный набор — 23 пакета (20 core/bot + 1 browser + 2 MAUI).

  • Skopka.Chat.Protocol — идентификаторы, лимиты, публичные контракты и каноническое бинарное представление v1; без ASP.NET Core, EF Core и криптографии клиента.
  • Skopka.Chat.Attachments — transport-neutral контракт immutable ciphertext storage, авторизация upload/download/delete и общие лимиты; зависит только от Protocol.
  • Skopka.Chat.Attachments.PostgreSql — отдельный AttachmentDbContext, migration и ограниченное bytea-хранилище для небольших зашифрованных файлов.
  • Skopka.Chat.Attachments.S3 — потоковое S3-compatible хранилище с conditional create, проверкой длины/SHA-256 и без перезаписи объекта.
  • Skopka.Chat.Client — общий движок для native/browser: идентичность, IDeviceKeyStore, платформенная граница криптографии (по умолчанию NSec на native), typed content, fan-out, файлы, проекции, fingerprints и IChatTransport.
  • Skopka.Chat.Client.Browser — browser-only криптография и защищённое локальное хранение, постоянная identity, durable очередь и same-origin cookie/CSRF адаптеры. Поддерживает browser-bound неэкспортируемый ключ; прежняя отдельная фраза нужна только один раз для миграции vault 0.16.
  • Skopka.Chat.Client.Storage — durable journal contracts, восстановление проекций и ChatSyncCoordinator с порядком verify/decrypt → store → apply → acknowledge.
  • Skopka.Chat.Client.Storage.Sqlite — локальный SQLite-журнал проверенных typed events с атомарной дедупликацией MessageId; хранит plaintext и требует host-защиты файла БД.
  • Skopka.Chat.Bots — клиентский runtime текстовых ботов с проверкой host-owned согласия и раскрытия оператора.
  • Skopka.Chat.Bots.Sqlite — долговечный inbox бота, acknowledgements и идемпотентность исходящих запросов; защита локальной БД остаётся у владельца.
  • Skopka.Chat.Bots.AspNetCore — приватный HTTP-шлюз у владельца бота и защищённый Data Protection адаптер identity; не часть основного chat server.
  • Skopka.Chat.Client.Maui — адаптеры MAUI SecureStorage, lifecycle/session coordination и ограниченная работа с временными plaintext-файлами; зависит от Client/Client.Storage/Media, но не от Server.
  • Skopka.Chat.Media — client-side режимы Auto/Media/File, заменяемая подготовка фото/видео и orchestration prepare → encrypt → upload; без server/persistence/UI framework.
  • Skopka.Chat.Media.FFmpeg — необязательное локальное JPEG/H.264/AAC преобразование через host-supplied FFmpeg; binary не входит в NuGet-пакет.
  • Skopka.Chat.UI.Core — framework-independent ChatViewModel, composer/reply/reaction/forward/edit commands и host-owned IChatContentSender; без зависимости от Blazor, transport или server.
  • Skopka.Chat.UI.Blazor — доступные Blazor-компоненты с CSS variables, локализуемыми строками и заменяемыми message/attachment/composer templates.
  • Skopka.Chat.UI.Maui — виртуализированный нативный CollectionView с compiled bindings, стабильным diff, темами, templates и host callbacks для файлов/пересылки/paging.
  • Skopka.Chat.Transport.Http — общие HTTP routes, JSON DTO, protocol mappings, лимиты и строгий source-generated System.Text.Json профиль; зависит только от Protocol.
  • Skopka.Chat.Client.Http — typed HttpClient, IAccessTokenProvider, HTTPS-by-default, bounded responses, потоковая загрузка/расшифровка attachments и ограниченные retries идемпотентных операций; без ссылки на Server.
  • Skopka.Chat.Server — личные и малые групповые диалоги, роли/состав, жизненный цикл устройств, идемпотентный приём, очередь доставки, acknowledgements и repository-интерфейсы; без ссылки на Client.
  • Skopka.Chat.Server.Kafka — необязательный Kafka publisher и hosted PostgreSQL-Outbox dispatcher; зависит от transport-neutral контрактов Server, не заменяет историю и не входит в доменное ядро.
  • Skopka.Chat.Server.NSec — optional Ed25519 verifier для device-binding proof через существующий NSec; без private keys/decryption API и без зависимости Server → Client.
  • Skopka.Chat.Server.AspNetCore — необязательные Minimal API endpoints для envelopes и attachment ciphertext с обязательной авторизацией и строгой привязкой user/device claims; без выбора формата токена или identity provider.
  • Skopka.Chat.Persistence.PostgreSql — EF Core 10/Npgsql, PostgreSQL migration, ограничения bytea, внешние ключи, индексы доставки и TTL cleanup.

Постоянная device identity и opt-in enrollment/rebind из 0.14.0 сохраняются: logout/re-login не меняет DeviceId, ключи и пути history/outbox. В 0.18.0 outer protocol-v1 и binding-v1 не менялись; content-v4 используется только текстом со структурированными упоминаниями. Ни binding, ни локальное шифрование не заменяют Auth, не восстанавливают потерянные ключи и не добавляют ratchet/forward secrecy. См. инструкцию подключения и совместимость.

flowchart LR
    Maui[Client.Maui: protected metadata] --> Client[Client: persistent identity + proof]
    Http[Client.Http: authenticated bootstrap] --> Client
    Http --> DTO[Transport.Http]
    Api[Server.AspNetCore: account/device policies] --> DTO
    Api --> Server[Server: binding orchestration]
    Pg[Persistence.PostgreSql: atomic consume/enroll/bind] --> Server
    Verify[Server.NSec: public-key verification] --> Server
    Kafka[Server.Kafka: optional event publisher] --> Server
    Server --> Protocol[Protocol: canonical binding-v1]
    Client --> Protocol
    DTO --> Protocol

Документация

Быстрый старт клиента

Реальное приложение должно реализовать IDeviceKeyStore поверх защищённого хранилища платформы. InMemoryDeviceKeyStore предназначен только для тестов и sample.

Низкоуровневое создание ниже — только первый явный enrollment, не обработчик каждого login. Для повторных входов используйте PersistentDeviceIdentityService.LoadAsync и DeviceBindingCoordinator из руководства. Custom key stores должны реализовать atomic TryCreateAsync; создание больше не перезаписывает существующие ключи.

var keyStore = new MyPlatformDeviceKeyStore();
var identityService = new DeviceIdentityService(keyStore);
var alice = await identityService.CreateAsync(userId, DeviceId.New(), DateTimeOffset.UtcNow);

// PublicDevice получателя приходит из аутентифицированного каталога сервера.
var crypto = new ChatCryptoService(keyStore);
var content = new ChatTextContent(ChatContentId.New(), "hello");
EncryptedEnvelope envelope = await crypto.EncryptContentAsync(
    content,
    conversationId,
    MessageId.New(),
    alice.DeviceId,
    bobPublicDevice,
    DateTimeOffset.UtcNow);

await transport.SendAsync(envelope); // transport реализует IChatTransport

Получатель обязан получить текущий PublicDevice отправителя, проверить подпись при расшифровке и сравнить security code по доверенному внешнему каналу при первом контакте или смене ключа:

var receiver = new ChatReceiver(
    new ChatCryptoService(keyStore),
    new MyTransactionalReceivedMessageStore());

ChatContentReceiveResult result = await receiver.ReceiveContentAsync(
    delivery.Envelope,
    senderPublicDevice);
var projection = new ChatConversationProjection(delivery.Envelope.ConversationId);
if (result.Delivery is not null)
{
    projection.Apply(result.Delivery);
}

string code = SecurityCodes.Between(myPublicDevice, senderPublicDevice);

Для постоянной typed history используйте отдельный безопасный pipeline вместо ручного acknowledgement:

var events = new SqliteChatEventStore("Data Source=protected/chat-history.db;Pooling=False");
var projections = new ChatConversationProjectionRegistry();
using var sync = new ChatSyncCoordinator(
    transport,
    new ChatCryptoService(keyStore),
    events,
    projections,
    myPublicDevice.DeviceId);

await sync.InitializeAsync(cancellationToken); // восстановление до polling
await sync.SynchronizeAsync(100, cancellationToken);
await sync.CommitLocalEchoAsync(successfulSend.Delivery!, cancellationToken);

Координатор подтверждает серверную доставку только после проверки, атомарной записи и идемпотентного применения. SQLite хранит канонический расшифрованный content, включая attachment keys; host обязан защищать файл БД, backups и retention. Подробнее: docs/client-storage.md.

Ответы, пересылки, реакции и редактирование

ChatContentId идентифицирует одно логическое событие и переиспользуется при шифровании для нескольких устройств. MessageId остаётся уникальным ID recipient-specific конверта:

var original = new ChatTextContent(ChatContentId.New(), "first");
var reply = new ChatTextContent(ChatContentId.New(), "answer", original.ContentId);
var forwarded = original.Forward(ChatContentId.New());
var reaction = new ChatReactionContent(
    ChatContentId.New(),
    original.ContentId,
    "👍",
    ChatReactionOperation.Add);
var edit = new ChatEditContent(
    ChatContentId.New(),
    original.ContentId,
    ChatEditField.Text,
    "updated text");

Пересылка копирует только текст, очищает reply-ссылку и ставит IsForwarded. Она не переносит исходного автора, conversation ID или подпись и поэтому не выдаёт отображаемую атрибуцию за криптографическое доказательство. Реакция и правка — отдельные зашифрованные события; сервер не видит целевой ChatContentId, emoji или новый текст. ChatConversationProjection принимает уже аутентифицированный ReceivedChatContent, сохраняет события, пришедшие раньше цели, и применяет правку только от пользователя — автора исходного сообщения. Последняя правка выбирается по authenticated sender time и ContentId; исходный server ciphertext не переписывается. Подпись вложения редактируется через ChatEditField.AttachmentCaption, а null удаляет только подпись.

Сырые EncryptTextAsync, EncryptAsync, DecryptAsync и ChatReceiver.ReceiveAsync сохранены для приложений 0.1.x–0.7.x; они не пытаются угадать тип содержимого. Новые приложения должны явно использовать EncryptContentAsync, DecryptContentAsync или ReceiveContentAsync.

Передача файлов и медиа

Файл не помещается в 64-КиБ envelope. Клиент потоково шифрует его независимым случайным ключом по XChaCha20-Poly1305 chunks, загружает ciphertext в выбранное хранилище и только затем отправляет небольшой ChatAttachmentContent каждому устройству. Имя, MIME, caption, plaintext length, ключ и nonce prefix находятся внутри E2EE-манифеста; сервер/S3/PostgreSQL видят только opaque ID, conversation/uploader IDs, ciphertext length/hash и retention timestamps.

await using var input = File.OpenRead(localPath);
await using var encrypted = File.Create(ciphertextPath);
var manifest = await ChatAttachmentCryptoService.EncryptAsync(
    input,
    input.Length,
    encrypted,
    AttachmentId.New(),
    ChatContentId.New(),
    Path.GetFileName(localPath),
    "application/octet-stream");

encrypted.Position = 0;
var stored = await api.UploadAttachmentAsync(conversationId, manifest, encrypted);
if (stored is AttachmentStoreResult.Stored or AttachmentStoreResult.Duplicate)
{
    await chat.SendAttachmentAsync(manifest);
}

Сообщения можно хранить в Skopka.Chat.Persistence.PostgreSql, а files — в Skopka.Chat.Attachments.S3. Для небольших файлов есть независимый Skopka.Chat.Attachments.PostgreSql с default limit 16 МиБ; общий контракт ограничен 5 ГиБ. S3 рекомендуется для больших media. Multipart/resume, range playback, thumbnails и attachment forwarding пока не входят в API. Подробный client/server setup: docs/attachments.md.

Перед шифрованием Skopka.Chat.Media может подготовить локальное фото или видео. Auto сжимает поддерживаемое media, но сохраняет оригинал, если результат не меньше; Media требует преобразование; File полностью обходит процессор и сохраняет исходные байты/name/MIME. Skopka.Chat.Media.FFmpeg выдаёт bounded JPEG или H.264/AAC MP4, удаляет mapped metadata/chapters и использует только защищённую host-owned временную директорию. FFmpeg binary не загружается библиотекой. Полный setup: docs/media.md.

Адаптируемый UI

Skopka.Chat.UI.Core хранит только presentation state одного диалога. Для стандартного multi-device пути приложение компонует ChatMultiDeviceSender и MultiDeviceChatContentSender: движок получает авторизованный список peer/sibling devices, сохраняет точный fan-out plan до сети, создаёт отдельный MessageId для каждого устройства и возвращает локальный echo. IChatContentSender остаётся заменяемой host-границей. Входящие данные применяются только после ChatReceiver.ReceiveContentAsync либо через durable ChatSyncCoordinator:

var chat = new ChatViewModel(conversationId, currentUserId, myContentSender);

if (receiveResult.Delivery is not null)
{
    chat.Apply(receiveResult.Delivery);
}

chat.SetDraftText("hello");
await chat.TrySendDraftAsync();

chat.BeginEdit(ownMessageContentId);
chat.SetDraftText("corrected text");
await chat.TrySendDraftAsync();

Готовый Blazor-компонент можно использовать целиком либо заменить message/composer templates:

@using Skopka.Chat.UI.Blazor

<SkopkaChat ViewModel="Chat"
            CssClass="brand-chat"
            ForwardRequested="ChooseForwardTarget">
    <MessageTemplate Context="item">
        <MyMessageBubble Message="item.Message" />
    </MessageTemplate>
</SkopkaChat>

Цвета, размеры и типографика задаются CSS custom properties с префиксом --skopka-chat-*; строки заменяются через SkopkaChatStrings, quick reactions — через ReactionChoices. Полное руководство и границы безопасности находятся в docs/ui.md.

Для MAUI используйте SkopkaChatView из Skopka.Chat.UI.Maui. Он предоставляет virtualized timeline, стабильное добавление/обновление элементов, paging callback, собственные/чужие bubbles и полностью заменяемые message/attachment/composer/empty templates. Skopka.Chat.Client.Maui добавляет endpoint-адаптеры, но authentication, навигация, push и политика хранения остаются у приложения. Полный composition root находится в samples/Skopka.Chat.Maui.Sample, а требования платформ — в docs/maui.md.

HTTP-клиент

Пример ниже использует прежний claims-based режим. Для opt-in device binding вместо RegisterDeviceAsync сначала выполните Enrollment/Rebind через DeviceBindingCoordinator, как описано в руководстве identity.

Host реализует получение access token и регистрирует отдельный typed client для текущей пары user/device:

builder.Services.AddScoped<IAccessTokenProvider, MyAccessTokenProvider>();
builder.Services.AddSkopkaChatHttpClient(
    new Uri("https://chat.example.com/"),
    options =>
    {
        options.AuthenticatedUserId = myPublicDevice.UserId.Value;
        options.AuthenticatedDeviceId = myPublicDevice.DeviceId.Value;
    });

var api = serviceProvider.GetRequiredService<SkopkaChatHttpClient>();
await api.RegisterDeviceAsync(myPublicDevice);
await api.CreateConversationAsync(peerUserId, conversationId);

PublicDevice peer = await api.GetDeviceAsync(peerDeviceId)
    ?? throw new InvalidOperationException("Peer device was not found.");
var content = new ChatTextContent(ChatContentId.New(), "hello");
EncryptedEnvelope envelope = await crypto.EncryptContentAsync(
    content,
    conversationId,
    MessageId.New(),
    myPublicDevice.DeviceId,
    peer,
    DateTimeOffset.UtcNow);
await api.SendAsync(envelope);

SkopkaChatHttpClient также регистрируется как IChatTransport. Он предназначен для transient/scoped использования, требует HTTPS, отключает redirects в предоставленном DI handler, получает новый токен перед каждой попыткой и не копирует error body или детали JSON parser в исключения. Успешный ответ сначала проходит byte limit, проверку JSON Content-Type, строгий разбор и protocol validation. RequireHttps = false допустим только для доверенного локального TestServer. Подробнее: ADR 0003 и ADR 0006.

Подключение transport-neutral сервера

Ядро не навязывает HTTP, WebSocket или SignalR. Host проверяет пользователя/access token, затем вызывает движок:

var options = new DbContextOptionsBuilder<ChatDbContext>()
    .UseNpgsql(connectionString)
    .Options;

await using var db = new ChatDbContext(options);
await db.Database.MigrateAsync();
var store = new PostgreSqlChatStore(db);
var engine = new ChatServerEngine(store, store, store);

await engine.RegisterDeviceAsync(publicDevice);
await engine.CreateConversationAsync(aliceUserId, bobUserId, conversationId, now);
await engine.SubmitAsync(encryptedEnvelope, now);
IReadOnlyList<StoredEnvelope> pending = await engine.ReceiveAsync(recipientDeviceId, 50, now);

ChatServerEngine проверяет структуру, лимиты, участников, актуальность key ID, revocation и идемпотентность. Он намеренно не проверяет криптографическую подпись: получатель делает это в Client; транспортный host обязан аутентифицировать право устройства отправлять от своего имени.

Подключение ASP.NET Core API

Необязательный пакет Skopka.Chat.Server.AspNetCore не выпускает и не валидирует токены. Сначала host настраивает доверенный authentication handler, затем регистрирует движок/repositories и transport:

builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", jwt =>
    {
        jwt.Authority = identityAuthority;
        jwt.Audience = chatAudience;
    });
builder.Services.AddAuthorization();

builder.Services.AddDbContext<ChatDbContext>(options => options.UseNpgsql(connectionString));
builder.Services.AddScoped<PostgreSqlChatStore>();
builder.Services.AddScoped<IDeviceRepository>(sp => sp.GetRequiredService<PostgreSqlChatStore>());
builder.Services.AddScoped<IConversationRepository>(sp => sp.GetRequiredService<PostgreSqlChatStore>());
builder.Services.AddScoped<IEnvelopeRepository>(sp => sp.GetRequiredService<PostgreSqlChatStore>());
builder.Services.AddScoped<ChatServerEngine>();

// Optional reliable metadata events; PostgreSQL remains the source of truth.
builder.Services.AddScoped<IChatServerEventOutbox, PostgreSqlChatEventOutbox>();
builder.Services.AddSkopkaChatKafkaServerEvents(options =>
{
    options.BootstrapServers = "kafka:9092";
});
builder.Services.AddSkopkaChatAspNetCore(options =>
{
    options.UserIdClaimType = ClaimTypes.NameIdentifier;
    options.DeviceIdClaimType = "skopka_chat_device_id";
});

// Optional attachment layer: choose this PostgreSQL adapter or S3, not both.
builder.Services.AddDbContext<AttachmentDbContext>(options => options.UseNpgsql(connectionString));
builder.Services.AddScoped<IAttachmentStore, PostgreSqlAttachmentStore>();
builder.Services.AddScoped<IAttachmentAccessAuthorizer, MyConversationAttachmentAuthorizer>();
builder.Services.AddSkopkaChatAttachmentStorage();

app.UseAuthentication();
app.UseAuthorization();
app.MapSkopkaChatApi();

По умолчанию обе claims должны встречаться ровно один раз и содержать GUID в формате D. Регистрация получает user ID из principal, отправка сверяет claim устройства с SenderDeviceId, а polling/acknowledgement вообще не принимают recipient device ID от клиента. Общие DTO находятся в Skopka.Chat.Transport.Http, поэтому Client.Http не ссылается на серверную сборку. AddSkopkaChatAspNetCore применяет строгий профиль к общим ASP.NET Core HttpJsonOptions; host с другими Minimal API должен проверить их совместимость с case-sensitive DTO без неизвестных или дублированных полей. Для cookie-authentication host дополнительно обязан настроить CSRF-защиту; для любой схемы обязательны TLS, rate limits и внешний request-size limit. Полное решение зафиксировано в ADR 0002 и ADR 0006.

Сборка и проверка

dotnet restore
dotnet build --no-restore
dotnet test --solution Skopka.Chat.sln --no-build --no-restore
dotnet pack Skopka.Chat.sln --no-restore
dotnet run --project samples/Skopka.Chat.Sample

NuGet-пакеты создаются в artifacts/packages.

PostgreSQL integration tests в четырёх проектах могут автоматически поднять изолированную PostgreSQL 18 через Testcontainers. Нужен запущенный Docker:

$env:SKOPKA_CHAT_POSTGRES_TESTCONTAINERS = 'true'
$env:SKOPKA_CHAT_POSTGRES_REQUIRED = 'true'
dotnet test --project tests/Skopka.Chat.Persistence.PostgreSql.Tests
dotnet test --project tests/Skopka.Chat.Attachments.Tests
dotnet test --project tests/Skopka.Chat.Http.IntegrationTests
dotnet test --project tests/Skopka.Chat.Binding.Tests

Каждая тестовая сборка получает собственный контейнер и удаляет его после выполнения. Для внешней одноразовой БД задайте SKOPKA_CHAT_POSTGRES; эта переменная имеет приоритет. Без connection string и флага Testcontainers DB-тесты корректно пропускаются; SKOPKA_CHAT_POSTGRES_REQUIRED=true превращает такой пропуск или недоступный Docker в ошибку release-gate.

Workflow .github/workflows/ci.yml запускает core/DB/fuzz gates на Linux, MAUI Android/Windows и package-consumer gate на Windows, iOS/Mac Catalyst + trimming smoke на macOS и только после этого объединяет точный набор из двадцати четырёх пакетов. Используемые GitHub Actions закреплены полными commit SHA; workflow имеет только contents: read.

Каждый CI build также воспроизводит сохранённый JSON/content/binding fuzz corpus, запускает короткую coverage-guided AFL++/SharpFuzz сессию, проверяет real-Kestrel request limits/cancellation и загружает двадцать четыре .nupkg вместе с двадцатью четырьмя .snupkg. Tag v<SemVer> запускает отдельный coordinated release: tag обязан принадлежать main, версия должна совпасть с VersionPrefix, вся версия должна быть свободна на NuGet.org, а после публикации создаётся GitHub Release. Настройка environment и ключа описана в releasing.md.

PostgreSQL delivery остаётся at-least-once: конкурентные poller'ы до acknowledgement могут получить один и тот же конверт. Хранилище держит одну строку на messageId, первый ack атомарно побеждает, а typed client может использовать IChatEventStore/ChatSyncCoordinator для durable store-before-ack; IReceivedMessageStore остаётся низкоуровневой границей ChatReceiver. При одинаковом acceptedAt порядок стабилен по messageId.

Что не входит в v1

Готовый product shell, contact discovery, Avalonia adapter, production-инфраструктура, интеграция со SkopiClub, удаление сообщений, история версий правок, группы, resumable/range media, thumbnails, attachment forwarding, push/background-delivery guarantee и backup/recovery ключей не входят в этот репозиторий. Multi-device sender создаёт отдельный immutable конверт с уникальным MessageId для каждого активного peer/sibling device, переиспользуя один ChatContentId; это fan-out без Double Ratchet и без автоматического доверия новым ключам.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (6)

Showing the top 5 NuGet packages that depend on Skopka.Chat.Protocol:

Package Downloads
Skopka.Chat.Attachments

Transport-neutral encrypted attachment storage contracts for Skopka.Chat.

Skopka.Chat.Client

Client-side identity, E2EE, typed chat content, reactions and local projection for Skopka.Chat.

Skopka.Chat.Server

Transport-neutral device, conversation, encrypted-envelope delivery and server-event contracts for Skopka.Chat.

Skopka.Chat.Transport.Http

Shared HTTP routes, JSON contracts and protocol mappings for Skopka.Chat clients and servers.

Skopka.Chat.Persistence.PostgreSql

EF Core and PostgreSQL storage for Skopka.Chat encrypted envelopes and atomic server-event outbox.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.19.0 309 9/4/2026
0.18.0 348 9/4/2026
0.17.0 290 9/3/2026
0.16.0 302 9/3/2026
0.15.0 289 9/2/2026
0.14.0 267 9/2/2026
0.13.0 256 9/2/2026
0.12.0 253 9/1/2026
0.11.0 233 9/1/2026
0.7.0 156 9/1/2026