Skip to content

Releases: 07artem132/SignalCli.NET

SignalCli.NET v4.10.3

Choose a tag to compare

@github-actions github-actions released this 17 Jul 10:05
48c2912

Критичний фікс вхідних повідомлень + захист event-диспетчера

Якщо ти споживаєш вхідні повідомлення (receive) — онови обов'язково. Підняли бандл до signal-cli 0.14.6, який фіксить upstream #2059: з ~10 червня 2026 Signal-сервер перестав слати serverGuid у частині sealed-sender конвертів, і signal-cli ≤0.14.4.1 кидав NullPointerException → дропав УСІ вхідні sealed-sender повідомлення (надсилання не зачеплене).

Пакети

  • SignalCli.Runtime, .Native, .Jre.win-x64, .Jre.osx-arm64: 0.14.3.1 → 0.14.6.1 (signal-cli 0.14.6). JDK-вимога незмінна — 25.
  • SignalCli.NET / SignalCli.NET.HealthChecks: 4.10.3.

Інше

  • SignalEventService.OnNotificationReceived тепер стійкий до порожніх/малформед receive-нотіфікацій (лог-і-скіп на Debug замість NRE-спаму на Error).

Деталі — CHANGELOG.md [4.10.3].

SignalCli.NET 4.10.2 — per-call RPC timeout

Choose a tag to compare

@07artem132 07artem132 released this 17 Jul 02:55
ce928f3

API-additive per-call timeout override для InvokeMethodAsync/FinishLinkAsync (trailing TimeSpan? timeout = null; null/≤0 → клієнтський default, поведінка незмінна; від'ємний → ArgumentOutOfRange). Закриває залежність консюмера WsRpcServer: finishLink потребує ≥130с на ручний QR-скан, тісніший за глобальний 30с.

514 тестів. Реліз тригерить publish-nuget → GitHub Packages.

SignalCli.NET 4.10.1

Choose a tag to compare

@07artem132 07artem132 released this 16 Jul 16:27
e41d3eb

Patch: виправлення silent-null wire-bug'у у device-linking флоу.

  • StartLinkResponse.DeviceLinkUri тепер несе явний [JsonPropertyName("deviceLinkUri")] — signal-cli віддає camelCase, а case-sensitive source-gen контекст мовчки давав null (RPC-відповідь "result": {}), QR-лінкування було зламане.
  • Новий regression guard RG10 (WireShapeAnnotationTests): кожна DTO-властивість контексту мусить мати явний [JsonPropertyName]/[JsonIgnore].
  • DeviceLinkingSerializationTests: wire-піни startLink/finishLink через продакшн-шлях.

Повний опис: CHANGELOG.md [4.10.1]. Тести: 509/509.

v4.0.2 — HealthChecks lockstep + JSON hardening + test guards

Choose a tag to compare

@07artem132 07artem132 released this 24 May 17:45

Patch-реліз без breaking changes. Якщо ти використовуєш SignalCli.NET.HealthChecks — онови його разом з main package: до 4.0.2 версії розійшлися й змішування пакетів давало runtime crash. Решта — 8 нових захисних тестів і одна реальна знахідка про JSON-валідацію, яка мовчки не fire'ила у продакшені.

🐛 Виправлено

  • Якщо консьюм'иш обидва пакети (SignalCli.NET + SignalCli.NET.HealthChecks) — онови їх разом. До 4.0.2 main lib був 4.0.1, а health-checks-adapter — 3.0.0. Змішування пакетів кидало MissingMethodException на першому health-check probe (бо adapter читає internal'и main lib через [InternalsVisibleTo]). Тепер обидва завжди ходять в lockstep через єдиний MSBuild property у Directory.Build.props. Bug-class виключено: reflection-тест VersionLockstepTests ловить розбіжність до merge'у. (NF-003, RG07)

  • JSON duplicate-key захист (rule #18) тепер реально діє на production-шляху. З 4.0.0 ми оголошували що malformed signal-cli response з повтореним ключем fail'ить deserialize. У реальності — fail'ило тільки на reflection-шляху, а production деsеріалізує через source-gen fast-path який власний runtime-flag не консумує. Тепер захист увімкнено на обох рівнях (runtime flag + source-gen attribute). Без впливу на нормальні signal-cli responses — Jackson на upstream-стороні фізично не може емітити дублікати; fix чисто defensive проти MITM/corruption. Знайшли під час audit'у коли наш же RG-тест спочатку failed. (json-hardening-source-gen-attribute)

  • CI pipeline нарешті пушить SignalCli.NET.HealthChecks на NuGet feed разом з main package. До 4.0.2 publish-nuget.yml workflow мав pack-steps для 5 пакетів (SignalCli.NET, SignalCli.Runtime, SignalCli.Runtime.Native, SignalCli.Runtime.Jre.{win-x64,osx-arm64}) — але SignalCli.NET.HealthChecks був відсутній. Net effect: csproj-сторона мала версії в lockstep (NF-003 main fix), але consumer-сторона все одно отримувала stale 3.0.0 HealthChecks-пакета з registry бо нічого свіжіше не пушилось. Цей gap робив весь NF-003 lockstep косметичним на consumer-стороні. Тепер крок присутній — після першого gh release create v4.0.2 HealthChecks теж потрапить на feed. (nf003-completion follow-up)

🛡️ Захист від регресій

8 нових unit + 1 E2E-тести закривають declared-але-untested invariants з CLAUDE.md "Future development guardrails":

  • Паралельні JSON-RPC виклики тепер pinned проти реального signal-cli — не лише проти mock'а на unit-рівні. Новий E2E запускає bundled JRE, кидає 10 паралельних version-викликів через справжній signal-cli virtual-thread dispatcher, асертить що всі 10 повертають свою власну відповідь. Якщо хтось у майбутньому замінить ConcurrentDictionary<id, TCS> на Queue<TCS> "бо порядок збережений" — unit-тести з in-order Subject<T>-моком цього б не побачили; цей E2E одразу впаде. (e2e-coverage-expansion)

  • Observability counters тепер pinned на всіх 3 тригерах перезапуску. Раніше privacy-guards перевіряли лише відсутність PII у Meter-tags, а фактичне інкрементування signalcli.process.restarts{trigger=…} було pinned тільки на trigger=force. Тепер trigger=crash (через event-raise на mock IProcess) і trigger=health (через FakeTimeProvider.Advance повз HealthCheckIntervalSeconds) теж покриті. (G4 subcases — T04, T05)

  • Event-API симетрія enforced reflection-тестом. CLAUDE.md правило "кожен IObservable<T> event-kind має парний IAsyncEnumerable<T> метод" раніше enforcъ'вся code-review'ом. Тепер EventApiSymmetryTests reflectively сканує ISignalEventService і fail'ить build якщо хтось додав 11-й event без парного методу. (NF-002, RG06)

  • JSON duplicate-key захист — 3-шаровий guard. Окрім самого fix'у з "Виправлено" вище, додано 3 facts у JsonSerializationTests: SignalJsonOptions_…IsFalse, JsonDocumentOptions_…ThrowsOnDuplicateKey, SignalJsonContext_…ThrowsOnDuplicateKey. Видалення будь-якого з 3 шарів (runtime flag, .NET 10 API contract, source-gen attribute) surface'иться як failed test. (G12, RG05)

  • JSON-RPC error wins over result. Захисний test пінує що при malformed-response (одночасно result І error) JsonRpcClient кидає exception, а не повертає result. CHANGELOG [4.0.1] стверджував покриття; grep показав що тесту не існувало. Тепер дійсно існує. (NF-001, G9)

🛠 Інше

  • Test-проект тепер ловить warnings як errors (як main lib давно). Раніше тихо акумулювалися 3 xUnit1031 violations у SyncDisposeDuringCleanupTests.cs (deadlock-prone .GetAwaiter().GetResult()). Виправлено: метод-сигнатури async Task + await; service.Dispose() навмисно залишається SYNC бо саме його тести й вимірюють. (NF-004)

  • Microsoft.Extensions.*.Testing bumped 9.0.0 → 10.0.0 — закриває last mismatched-major у dependency graph. Public-surface unchanged per MS Learn. (NF-005)

  • Версія обох NuGet-пакетів тепер централізована в Directory.Build.props → <SignalCliPackageVersion>. Bump = single-file edit. Hardcoded <Version> у будь-якому csproj заборонено.

  • CLAUDE.md розширено двома секціями для майбутніх контрибуторів + новим правилом:

    • "Audit baseline" — мінімальна планка яку PR не може опустити (test counts, regression guards, архітектурні invariants).
    • "How we discovered these issues" — 5 failure-mode-сценаріїв з PR-time checklists, щоб не повторювати ті самі паттерни.
    • "Version-CHANGELOG lockstep" + "CHANGELOG voice template" — bump версії МУСИТЬ йти з CHANGELOG-секцією в одному коміті; новий template для consumer-first voice (bold leading claim, технічна обгортка, internal-IDs у дужках).
    • Виправлено константа MaxInlineEncodedAttachmentBytes: 15 000 000 → 12 000 000 (drift з 4.0.0 — реальне значення в коді змінилось у signal-cli-protocol-alignment §5, doc відстав). (NF-006)
  • CHANGELOG voice tightened — починаючи з [4.0.2] (цей entry), пишемо у consumer-first voice. Старіші версії [4.0.1]/[4.0.0] перероблено в тому ж коміті задля консистенції.

📊 Тестова статистика

  • Unit: 279 → 287 (+8 захисних тестів).
  • Integration: 7 → 8 (+1 E2E на паралельний RPC).
  • Build на src/ + Tests/: 0 warnings, 0 errors.

Pending follow-up

Нічого — усі audit v2.1 знахідки закриті.

v3.0.0 — post-modernize-tuning

Choose a tag to compare

@07artem132 07artem132 released this 24 May 03:35

[3.0.0] — 2026-05-24

Друга велика хвиля модернізації — фокус на correctness/observability/agent-friendly-API. Містить breaking changes, перерахованих нижче.
Реалізація триває; цей розділ оновлюється у міру викочування кластерів. Див. openspec/changes/post-modernize-tuning/.

⚠️ Breaking

  • FinishLinkResponse.number → FinishLinkResponse.Number (PascalCase property; JSON wire-name збережено через [JsonPropertyName("number")]).
  • SubscribeReceiveResponse.id → SubscribeReceiveResponse.Id (так само PascalCase).
  • BaseSignalEventArgs.Account тепер string (non-nullable). Те ж саме поширено на всі 10 *EventArgs-records. Раніше було string?, що змушувало кожного підписника null-чекати гарантовано-присутнє значення.
  • Config.EnvironmentVariables і SignalCliOptions.EnvironmentVariables тепер IReadOnlyDictionary<string,string> на читання. Для мутації — Config.WithEnvironment(IDictionary<string,string>) (defensive copy + fluent return). Раніше можна було .Add(key, value) на shared посилання після DI-capture.
  • JsonRpcException(string, Exception?) ctor з нестандартним кодом -32000 видалено. Замість нього — три CA1032-стандартні ctors: (), (string), (string, Exception) — усі з канонічним JSON-RPC 2.0 кодом -32603 ("Internal error"). Консумери, що каталі legacy-конструктор, мають мігрувати на CA1032-ctors або передавати власний JsonRpcError.
  • SignalEventService.SubscribeAsync(account) тепер ідемпотентний — повторні виклики для того самого облікового запису повертають той самий subscriptionId без RPC. Раніше другий виклик кидав InvalidOperationException. catch (InvalidOperationException) when (msg.Contains("вже підписаний")) більше не зловить — операція тепер успіх.
  • JsonRequired на always-present полях Envelope.cs: JsonRemoteDelete.RemoteDeleteId, Offer.Type/Offer.Opaque, Answer.Opaque, IceUpdate.Opaque, Hangup.Type. Якщо signal-cli колись поверне ці поля з null — десеріалізація фолтиться з JsonException замість тихо пропустити null у non-nullable property.
  • UserRecipient/GroupRecipient ctor: null → ArgumentNullException, empty → ArgumentException. Раніше empty теж кидав ArgumentNullException (порушення контракту обох типів).
  • SignalCliHostedService тепер sealed — інхеріт не підтримується.
  • Стандартний шлях dotnet publish /p:PublishAot=true ще не enable'нений (deferred — потребує redesign на JsonTypeInfo<T> overloads), але всі предумови (drop Nito.AsyncEx, drop .ValidateDataAnnotations(), source-gen JSON fast-path) на місці.
  • (round 9 §4.7) CancellationToken-property + WithCancellationToken-Builder-method видалено з TextMessageOptions / AttachmentMessageOptions / StickerMessageOptions. Єдиний шлях скасування — параметр Send*Async(options, cancellationToken). [Obsolete]-shim після одного major-релізу (як обіцяно в CLAUDE.md "Backward compatibility convention"). Migration: .WithCancellationToken(ct).Build(); → .Build(); + передати ct другим аргументом.
  • (round 9 §4.23/4.24) ISignalMessage.{SendText,SendAttachment,SendSticker}MessageAsync повертають Task<SendMessageResponse> (single response), а не Task<List<SendMessageResponse>> — все одно завжди було [response]-wrap. Migration: (await SendTextMessageAsync(opts))[0] → await SendTextMessageAsync(opts).
  • (round 9 §4.27) Generic-параметри InvokeMethodAsync поміняли порядок: <TResponse, TRequest> → <TRequest, TResponse> на ISignalCliClient, IJsonRpcSender, обох impls + ~22 callsites. Узгоджено з JsonSerializer.Deserialize<TValue>-конвенцією. Shim неможливий — C# не розрізняє overload'и за порядком typeparam'ів (same runtime signature). Migration: розверни <X, Y> → <Y, X> на кожному виклику.

✨ Додано

Observability (capability observability)

  • Єдиний internal static readonly ActivitySource SignalCliDiagnostics.ActivitySource = new("SignalCli.NET", AssemblyVersion) — спани rpc.<method>, signalcli.process.start, signalcli.healthcheck.ping, signalcli.subscribe. Теги: method name, status enum, integer id, exception type name — без PII.
  • Єдиний internal static readonly Meter SignalCliDiagnostics.Meter = new("SignalCli.NET", AssemblyVersion):
    • Counter<long> signalcli.rpc.requests (теги method, status ∈ {ok,timeout,error})
    • Histogram<double> signalcli.rpc.duration (мс, тег method)
    • Counter<long> signalcli.process.restarts (тег trigger ∈ {force,crash,health})
    • Counter<long> signalcli.events.dropped (тег event_type ∈ 10 значень) — замінює приватний _droppedCount.
    • ObservableGauge<int> signalcli.subscriptions.active.
  • Документація: docs/cloud-development.md має нову секцію Observability з drop-in OTel-snippet.

RPC robustness

  • SignalCliOptions.NotificationChannelCapacity (default 1024). Між stdout-парсером і fan-out-споживачем — bounded Channel; повільний підписник створює back-pressure аж до signal-cli.
  • JsonRpcClient приймає TimeProvider — CancellationTokenSource(_requestTimeout, _timeProvider) робить timeout-шлях віртуалізованим у тестах.
  • SignalCliHostedService.StopProcessInternalAsyncNoLock теж використовує CancellationTokenSource(_, _timeProvider).
  • BeginScope(RpcMethod, RpcRequestId) у JsonRpcClient.InvokeMethodAsync — кожний нижчий JsonRpcClientLog.* несе structured-properties.

Subscription race safety

  • Reservation placeholder pattern у SignalEventService.SubscribeAsync через Dictionary<string, TaskCompletionSource<int>> _pendingSubscribes. Конкурентні виклики для того самого облікового запису роблять РІВНО 1 RPC; усі N викликачів отримують той самий ID.
  • ObjectDisposedException.ThrowIf(_disposed, this) на SubscribeAsync/UnsubscribeAsync (audit C6).

Async-suffix shims (one-major-grace)

  • ISignalAccounts.ListAccountsAsync/SyncAccountAsync, ISignalDevices.StartLinkAsync/FinishLinkAsync, ISignalGroups.ListGroupsAsync — нові методи + [Obsolete] DIM-shims на старі імена ("will be removed in 4.0").

🛠 Внутрішнє

  • ProcessStateManager: snapshot-then-emit (OnNext поза локом — System.Threading.Lock не реентрантний). _disposed всюди → int з Interlocked.Exchange (lock-free disposal short-circuit). Catch ObjectDisposedException з OnNext (documented disposal race window).
  • _disposed стандартизовано як int + Interlocked.Exchange у SignalCliHostedService, JsonRpcClient, JsonRpcClientHostedService, SignalEventService.
  • Nito.AsyncEx видалено. JsonRpcClient._sendLock і SignalCliHostedService._operationLock → SemaphoreSlim(1,1) з WaitAsync/Release.
  • .ValidateDataAnnotations() видалено з options-pipeline — [OptionsValidator] source-gen самостійно перевіряє [Required]/[Range] без reflection. Знято останній AOT-blocker у options-шляху.
  • SignalJsonContext.GenerationMode = Default (fast-path emission + metadata) замість Metadata-only.
  • SignalEventService, ProcessWrapper, ProcessFactory, JsonRpcClientFactory, SignalAccounts, SignalDevices, SignalGroups — sealed (CA1052).
  • Config.BuildClasspath кешує classpath; Directory.GetFiles викликається рівно 1 раз на Config-інстанс.
  • ValidateRecipients: single-pass materialization + один foreach на user/group split (раніше — 3 пройдення).
  • ArgumentException.ThrowIfNullOrEmpty boundary checks у SignalDevices.FinishLinkAsync/SignalGroups.ListGroupsAsync.
  • JetBrains.Annotations PackageReference PrivateAssets="all" — більше не leak у consumer dependency graph.
  • Example/Program.cs повністю переписаний на async Task Main/await host.StopAsync()/awaited SendTextMessageAsync — LLM-агенти, що копіюють приклад, успадковують правильні async-патерни.
  • Forward-slash MSBuild paths у SignalCli.runtime.csproj і SignalCli.Native.targets — Linux-збірки runtime-пакетів більше не ламаються тихо.
  • (round 8) Logging-perf analyzer rules: CA1848 → warning (блокує regression на direct _logger.Log* — кожна нова log-callsite має йти через [LoggerMessage]); CA1873 → suggestion (analyzer не розпізнає manual IsEnabled-guards, тож warning-рівень дає false-positives на legitimate Trace-only eager-eval сайтах). Trade-off задокументовано в .editorconfig.
  • (round 8) Manual if (_logger.IsEnabled(LogLevel.Trace)) guards над string.Join(", ", response) у SignalAccounts.ListAccountsAsync + SignalGroups.ListGroupsAsync — [LoggerMessage]-внутрішній IsEnabled живе всередині generated-methоду, тож callsite-level allocation платилася на КОЖНОМУ виклику (навіть Info-level). Економить N × string-allocation на listAccounts/listGroups.
  • (round 8) ObservabilityPrivacyTests flake-fix: lock-snapshot pattern для _capturedActivities/_capturedMeasurements. ActivityListener/MeterListener реєструються глобально на ActivitySource/Meter, тож callback'и можуть прилітати з потоків паралельних тестів — List<T> не thread-safe → Collection was modified intermittent. Всі writes тепер під Lock, читачі enumerate'ять snapshot.
  • (round 9 §4.9) .editorconfig піднято CA2007 (ConfigureAwait) до warning після audit-перевірки: 0 missing-sites у src/SignalCli/**. Тепер регресія неможлива — будь-який майбутній bare await ловиться build-warning'ом.
  • (round 9 §4.25) [StringSyntax(StringSyntaxAttribute.Uri)] на TextMessageOptions.PreviewUrl, PreviewImage, та параметрах Builder.WithPreview(previewUrl, …, previewImage). Zero runtime cost; IDEs тепер валідують URL-syntax.
  • (round 9 §4.26) XMLDoc'и на 3 Send*Async-методах в ISignalMessage отримали <exception cref="TimeoutException"> із посиланням на SignalCliOptions.RequestTimeoutSeconds. Closes audit-doc-gap.
  • (round 10 §6.12) Новий test-file JsonContextRegistrationTests — рефлексивно стверджує, що кожен *Parameters/*Response DTO у SignalCli.Models.Signal.* зареєстрований у SignalJsonContext через [JsonSerializable(typeof(...))]. Захист від "silent {}"-регресії, ...
Read more