Changelog

May 27, 2026 · View on GitHub

All notable changes to this project will be documented in this file.

The format is inspired by Keep a Changelog, and this project adheres to semantic versioning once 1.0.0 is released.

[Unreleased]

[2.0.7] - 2026-05-27

Fixed

  • Closed a TOCTOU window in the 2.0.6 send() isOpen() guard (PR #526 review). The check was performed at the top of send(), before sendFrameGated() acquired the sessionGate read lock — so a concurrent close() could acquire the write lock, close the session, and release it between the check and the actual sendMessage(), still letting the frame reach a closed session. Moved the isOpen() check inside sendFrameGated(), immediately after the read lock is taken, so the open-check and the write are now atomic with respect to closeGated() (which holds the write lock). 2.0.6 already handled the dominant case (the session already closed when send() is invoked); this closes the narrow in-flight-close window.

[2.0.6] - 2026-05-27

Fixed

  • NostrRelayClient.send() now guards on clientSession.isOpen() before writing, mirroring subscribe(). A per-subscription Nostr CLOSE issued while a relay connection was being torn down previously reached Tomcat's sendText, where WsRemoteEndpointImplBase.sendMessageBlockInternal invokes doClose() mid-write and emits a WS CLOSE frame while the text write is still pending on the async channel — throwing IllegalStateException: Concurrent write operations are not permitted, which surfaced via handleTransportError as a transport-error reconnect storm during subscription teardown of a breaking connection (spec-026). The application-level locks (the decorator, the sessionGate, and the upstream adapter sendLock) cannot prevent this because it is Tomcat closing the session inside a single in-progress send, not two application threads racing. send() now fails fast with IOException("WebSocket session is closed") on a closed session — the doomed write never enters Tomcat's close-mid-write path. +regression test send_onClosedSession_failsFastWithoutDelegateWrite.

[2.0.5] - 2026-05-26

Fixed

  • NostrRelayClient.send() no longer leaves the client stuck in the "request in flight" state when the write fails for a reason other than overflow. A plain transport IOException from the gated send previously bypassed pendingRequest cleanup, so the next send() (including an @NostrRetryable retry) hit IllegalStateException: A request is already in flight instead of retrying. send() now clears pendingRequest on any send failure, not only SessionLimitExceededException. (PR #525 review follow-up.)
  • closeQuietly() now logs the swallowed throwable (with stack trace) and the relay URI instead of just the exception message, preserving diagnostics for best-effort closes.

[2.0.4] - 2026-05-26

Fixed

  • Close-vs-write race in NostrRelayClient (spec-026 US3). The client wrapped its session in Spring's ConcurrentWebSocketSessionDecorator but called the no-arg clientSession.close() (in close() and the send() timeout path). Spring 6.2.x's decorator overrides only close(CloseStatus) (guarded by closeLock); the no-arg close() falls through to WebSocketSessionDecorator.close()delegate.close() with no coordination, sending a CLOSE frame straight to the Tomcat delegate while a sendMessage flush was in flight (Tomcat permits only one write in flight → IllegalStateException: Concurrent write operations are not permitted). Routing through close(CloseStatus) alone is insufficient because its closeLock is a separate lock from the flushLock guarding delegate.sendMessage. Introduced a ReentrantReadWriteLock session gate: every write (send/subscribe) holds the read side (sends stay concurrent — the decorator still serialises the delegate writes among them), every close holds the write side and always uses close(CloseStatus), so a close waits for all in-flight sends to drain before sending the CLOSE frame. No lock nesting → no added deadlock risk.

Removed

  • Dead code cleanup — deleted unused classes: IContent, JsonContent, Reaction enum, Response, Nip05Content, Nip05ContentDecoder, BaseAuthMessage, GenericMessage, IKey, GenericEventConverter, GenericEventTypeClassifier, GenericEventDecoder, FiltersDecoder, BaseTagDecoder, GenericEventValidator, GenericEventSerializer, GenericEventUpdater, GenericTagQuery, HttpClientProvider, DefaultHttpClientProvider.
  • testAuthMessage test and GenericEventSupportTest removed (tested deleted classes).
  • createGenericTagQuery() removed from EntityFactory (only consumer of deleted GenericTagQuery).

Changed

  • RelayAuthenticationMessage and CanonicalAuthenticationMessage now extend BaseMessage directly (previously extended the now-deleted BaseAuthMessage).
  • BaseKey now directly implements Serializable (previously implemented the now-deleted IKey interface).
  • Nip05Validator now creates HttpClient instances directly via a Function<Duration, HttpClient> factory (previously used deleted HttpClientProvider/DefaultHttpClientProvider interface).

[2.0.3] - 2026-05-08

Fixed

  • Explicit close on OverflowStrategy.TERMINATE; restores upstream isOpen()==false reconnect contract. Spring's ConcurrentWebSocketSessionDecorator under OverflowStrategy.TERMINATE only sets a private limitExceeded flag and throws SessionLimitExceededException from limitExceeded() — it does not close the delegate session. As a result, after 2.0.2 the upstream caller's clientSession.isOpen()==false → reconnect contract did not hold (the session stayed open after overflow). NostrRelayClient.subscribe() and NostrRelayClient.send() now detect a SessionLimitExceededException cause in their respective catch blocks and call clientSession.close(CloseStatus.SESSION_NOT_RELIABLE) explicitly before rewrapping the exception. Non-overflow RuntimeExceptions continue to flow through the existing wrap-as-IOException path unchanged. The §6.7d concurrency test now strictly asserts clientSession.isOpen()==false after overflow, matching the spec's original contract.

[2.0.2] - 2026-05-08

Fixed

  • NostrRelayClient.subscribe() was calling clientSession.sendMessage(...) without holding the existing sendLock, while the same class's send() path correctly held it. Two threads racing inside subscribe() (or one in subscribe() racing one in send()) could trigger the underlying writer's IllegalStateException("The remote endpoint was in state [TEXT_FULL_WRITING]") (Tomcat / Spring StandardWebSocketSession) or Blocking message pending (Jetty), which the catch (RuntimeException) block at the bottom of subscribe() rewrapped as IOException("Failed to send subscription payload", e). Downstream consumer (imani-gateway-core account-app) observed ~70 RelaySubscribeException per 60 minutes once a circuit-breaker tuning fix unmasked the underlying race. Fixed by wrapping the underlying WebSocketSession returned by connectSession(...) in Spring's ConcurrentWebSocketSessionDecorator(session, sendTimeLimit, bufferSizeLimit, OverflowStrategy.TERMINATE) at construction time, so concurrent sendMessage() calls from any send-path are serialised at the session layer.

Added

  • New canonical four-arg public constructor NostrRelayClient(String relayUri, long awaitTimeoutMs, int sendBufferLimit, int sendTimeLimitMs) annotated @Autowired for Spring constructor-injection. Spring binds against this overload via the new @Value("${nostr.websocket.send-buffer-limit:262144}") and @Value("${nostr.websocket.send-time-limit-ms:10000}") keys (also reachable via the NOSTR_WEBSOCKET_SEND_BUFFER_LIMIT / NOSTR_WEBSOCKET_SEND_TIME_LIMIT_MS env-vars under Spring relaxed binding). The previously-existing one-arg (String) and two-arg (String, long) public constructors are retained as delegating overloads (binary-compatible) and now also benefit from the decorator wrap by way of the canonical ctor.
  • Test-only static factories forTestWithRawSession(...) and forTestWithDecoratedSession(...) (two overloads — defaults and explicit) plus a package-private four-arg test constructor, so the new NostrRelayClientConcurrencyTest can assert both the regression (raw-session reproduction of the IllegalStateException race) and the resolution (decorator-wrapped session serialises sends).

Changed

  • awaitTimeoutMs is now a final field assigned once via constructor injection (previously @Value-annotated field). Spring's constructor-injection ordering guarantees the value reaches the constructor body before the decorator is constructed, which is required for the explicit overflow strategy to be applied. The system property / env-var key (nostr.websocket.await-timeout-ms) and its default (60 000 ms) are unchanged.

[2.0.1] - 2026-05-06

Fixed

  • NostrRelayClient log statements were emitting Sending request to relay null: ... (and similar) once the WebSocket session had closed, because the relay URI was being read via clientSession.getUri() which returns null after close. Captured the URI in a private final String relayUri field set in each constructor and replaced 8 clientSession.getUri() log call-sites. Resolves 188 occurrences per 2-hour window observed in a downstream consumer's staging logs (#523).

[2.0.0] - 2026-02-24

This is a major release that implements the full design simplification described in docs/developer/SIMPLIFICATION_PROPOSAL.md, reducing the library from 9 modules with ~180 classes to 4 modules with ~40 classes.

Added

  • Kinds utility class with static int constants for common Nostr event kinds (TEXT_NOTE, SET_METADATA, CONTACT_LIST, etc.) and range-check methods (isReplaceable(), isEphemeral(), isAddressable(), isValid()).
  • GenericTag.of(String code, String... params) factory method for concise tag creation.
  • GenericTag.toArray() returning the NIP-01 wire format ["code", "param0", "param1", ...].
  • GenericTag now stores tag values as List<String> (replacing List<ElementAttribute>), providing direct access via getParams().
  • EventFilter builder API for composable relay filters: .kinds(), .authors(), .since(), .until(), .addTagFilter(), .limit(), .ids().
  • RelayTimeoutException — typed exception replacing silent empty-list returns on relay timeout.
  • ConnectionState enum (CONNECTING, CONNECTED, RECONNECTING, CLOSED) for WebSocket connection state tracking.
  • NostrRelayClient async Virtual Thread APIs: connectAsync(...), sendAsync(...), and subscribeAsync(...).
  • Nip05Validator.validateAsync() and Nip05Validator.validateBatch(...) for parallel NIP-05 validation workloads.
  • Spring Retry support (@NostrRetryable, @Recover) consolidated directly into NostrRelayClient.

Changed

  • Module consolidation — merged 9 modules into 4:
    • nostr-java-util + nostr-java-cryptonostr-java-core
    • nostr-java-base + nostr-java-eventnostr-java-event
    • nostr-java-id + nostr-java-encryptionnostr-java-identity
    • nostr-java-client (unchanged)
  • GenericEvent is now the sole event class. All 39 concrete event subclasses removed. Events are differentiated by int kind instead of Java type.
  • GenericTag is now the sole tag class. All 17 concrete tag subclasses removed. Tags are a simple code + List<String> params.
  • GenericEvent.kind changed from Kind enum to plain int. Builder simplified to .kind(int) only.
  • GenericEvent.tags changed from List<BaseTag> to List<GenericTag>.
  • GenericEvent implements ISignable directly (no longer extends BaseEvent).
  • EventMessage now references GenericEvent directly instead of IEvent.
  • IDecoder<T> type bound changed from IDecoder<T extends IElement> to unbounded IDecoder<T>.
  • TagDeserializer now always produces GenericTag with List<String> params — no registry dispatch.
  • GenericTagSerializer simplified to output [code, param0, param1, ...] directly from List<String>.
  • GenericEventDeserializer simplified — no subclass dispatch to concrete event types.
  • NostrUtil.bytesToHex() now uses java.util.HexFormat instead of hand-rolled hex encoding.
  • NostrUtil.hexToBytes() family of methods now uses java.util.HexFormat.parseHex() — fails fast on invalid hex instead of silently producing corrupt bytes.
  • WebSocket client termination detection now uses proper JSON parsing instead of brittle string-prefix matching.
  • StandardWebSocketClient renamed to NostrRelayClient.
  • SpringWebSocketClient absorbed into NostrRelayClient (single client class with retry support).
  • Relay subscription callbacks are now dispatched on Virtual Threads to avoid blocking inbound WebSocket processing.
  • DefaultHttpClientProvider now uses a shared Virtual Thread executor instead of creating a new executor per HttpClient.
  • All synchronized blocks in NostrRelayClient replaced with ReentrantLock to avoid Virtual Thread pinning.
  • Configurable max events per request limit (default 10,000) to prevent unbounded memory accumulation.

Fixed

  • GenericTag.getCode() NPE — structurally eliminated by removing the dual-path tag architecture. getCode() is now a trivial field accessor with zero NPE risk.
  • Removed the remaining synchronized cleanup block from NostrRelayClient.send(...), using ReentrantLock consistently to avoid VT pinning risk.
  • Relay timeout now throws RelayTimeoutException instead of silently returning an empty list, allowing callers to distinguish "no results" from "timed out".

Removed

  • nostr-java-api module — all 26 NIP classes (NIP01–NIP99), EventNostr, factory classes, client managers, service layer, and configuration classes.
  • nostr-java-examples module — all 6 example classes.
  • 39 concrete event subclassesTextNoteEvent, DirectMessageEvent, ContactListEvent, ReactionEvent, DeletionEvent, EphemeralEvent, ReplaceableEvent, AddressableEvent, all Calendar/Marketplace/Channel/NostrConnect events, and more. Use GenericEvent with the appropriate int kind.
  • 17 concrete tag subclassesEventTag, PubKeyTag, AddressTag, IdentifierTag, ReferenceTag, HashtagTag, ExpirationTag, UrlTag, SubjectTag, DelegationTag, RelaysTag, NonceTag, PriceTag, EmojiTag, GeohashTag, LabelTag, LabelNamespaceTag, VoteTag. Use GenericTag.of(code, params...).
  • 27 entity classesUserProfile, Profile, ChannelProfile, ZapRequest, ZapReceipt, Reaction, all Cashu entities, all marketplace entities, and more.
  • Kind enum — replaced by Kinds utility class with static int constants.
  • ElementAttribute — replaced by List<String> in GenericTag.
  • TagRegistry — no longer needed with a single tag class.
  • Interfaces and abstract classes: IElement, ITag, IEvent, IGenericElement, IBech32Encodable, Deleteable, BaseEvent, BaseTag.
  • Annotations: @Tag, @Event, @Key.
  • 14 filter classesAbstractFilterable, KindFilter, AuthorFilter, SinceFilter, UntilFilter, HashtagTagFilter, AddressTagFilter, GeohashTagFilter, IdentifierTagFilter, ReferencedEventFilter, ReferencedPublicKeyFilter, UrlTagFilter, VoteTagFilter, GenericTagQueryFilter. Use EventFilter.builder().
  • Concrete serializers/deserializersAddressTagSerializer, ReferenceTagSerializer, ExpirationTagSerializer, IdentifierTagSerializer, RelaysTagSerializer, BaseTagSerializer, AbstractTagSerializer, CalendarEventDeserializer, ClassifiedListingEventDeserializer, CashuTokenSerializer.
  • Client classesWebSocketClientIF, WebSocketClientFactory, SpringWebSocketClientFactory, SpringWebSocketClient. Use NostrRelayClient directly.
  • Dead codeMarker enum, RelayUri value object.
  • 5 old modulesnostr-java-util, nostr-java-crypto, nostr-java-base, nostr-java-id, nostr-java-encryption (merged into the 4 remaining modules).
  • Dead pollIntervalMs parameter from WebSocket client constructors.

[1.3.0] - 2026-01-25

Added

  • Configurable WebSocket buffer sizes for handling large Nostr events via nostr.websocket.max-text-message-buffer-size and nostr.websocket.max-binary-message-buffer-size properties.

Changed

  • No additional behavior changes in this release; Kind APIs and WebSocket concurrency improvements were introduced in 1.2.1.

Fixed

  • No new fixes beyond 1.2.1; this release focuses on configurable WebSocket buffer sizes.

[1.2.1] - 2026-01-21

Fixed

  • NIP-44 now correctly uses HKDF-Extract for conversation key derivation, ensuring proper cryptographic key generation.
  • WebSocket client now correctly accumulates all relay responses (EVENT messages) before completing, waiting for termination signals (EOSE, OK, NOTICE, CLOSED) instead of returning after the first message.
  • WebSocket client thread-safety improved by encapsulating pending request state, preventing potential race conditions when multiple threads call send() concurrently.
  • WebSocket client now rejects concurrent send() calls with IllegalStateException instead of silently orphaning the previous request's future.
  • KindFilter and ClassifiedListingEventDeserializer now use Kind.valueOfStrict() for fail-fast deserialization of unknown kind values.

Changed

  • Kind.valueOf(int) now returns null for unknown kind values instead of throwing, allowing graceful handling of custom or future NIP kinds during JSON deserialization.
  • Added Kind.valueOfStrict(int) for callers who need fail-fast behavior on unknown kinds.
  • Added Kind.findByValue(int) returning Optional for safe, explicit handling of unknown kinds.

[1.2.0] - 2025-12-26

Fixed

  • NIP-44 encryption now correctly uses HKDF instead of PBKDF2 for key derivation, as required by the specification. This fix enables DM interoperability between Java backend and JavaScript frontend implementations (e.g., nostr-tools).

Changed

  • Switched integration tests to use strfry relay for improved robustness.

Removed

  • Removed AGENTS.md and CLAUDE.md documentation files from the repository.

[1.1.1] - 2025-12-24

Fixed

  • StandardWebSocketClient now configures WebSocketContainer with a 1-hour idle timeout (configurable via nostr.websocket.max-idle-timeout-ms) to prevent premature connection closures when relays have periods of inactivity.

[1.1.0] - 2025-12-23

Added

  • Public constructor StandardWebSocketClient(String relayUri, long awaitTimeoutMs, long pollIntervalMs) for programmatic timeout configuration outside Spring DI context.

Changed

  • Enhanced diagnostic logging for timeout configuration in StandardWebSocketClient.
  • Simplified WebSocket client initialization and retry logic in tests.

Fixed

  • Updated JsonDeserialize builder reference in API module.

[1.0.1] - 2025-12-20

Changed

  • Updated project version and added artifact names in POM files.
  • Added Sonatype Central server credentials configuration.
  • Updated Maven command for central publishing.

[1.0.0] - 2025-10-13

Added

  • Release automation script scripts/release.sh with bump/tag/verify/publish/next-snapshot commands (supports --no-docker, --skip-tests, and --dry-run).
  • GitHub Actions:
    • CI workflow .github/workflows/ci.yml with Java 21 build and Java 17 POM validation; separate Docker-based integration job; uploads reports/artifacts.
    • Release workflow .github/workflows/release.yml publishing to Maven Central, validating tag vs POM version, and creating GitHub releases.
  • Documentation:
    • docs/explanation/dependency-alignment.md — BOM alignment and post-1.0 override removal plan.
    • docs/howto/version-uplift-workflow.md — step-by-step release process; wired to scripts/release.sh.

Changed

  • Roadmap project helper scripts/create-roadmap-project.sh now adds tasks for:
    • Release workflow secrets setup (Central + GPG)
    • Enforcing tag/version parity during releases
    • Updating docs version references to latest
    • CI + Docker IT stability and triage plan
  • Expanded decoder and mapping tests to cover all implemented relay commands (EVENT, CLOSE, EOSE, NOTICE, OK, AUTH).
  • Stabilized NIP-52 (calendar) and NIP-99 (classifieds) integration tests for deterministic relay behavior.
  • Docs updates to prefer BOM usage:
    • docs/GETTING_STARTED.md updated with Maven/Gradle BOM examples
    • docs/howto/use-nostr-java-api.md updated to import BOM and omit per-module versions
    • Cross-links added from the roadmap to migration and dependency alignment docs
  • README cleanup: removed maintainer-only roadmap automation and moved troubleshooting to docs/howto/diagnostics.md.

Removed

  • Deprecated APIs finalized for 1.0.0:
    • nostr.config.Constants.Kind facade — use nostr.base.Kind
    • nostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD — use nostr.event.json.EventJsonMapper#getMapper()
    • nostr.api.NIP01#createTextNoteEvent(Identity, String) and related Identity-based overloads — use instance-configured sender
    • nostr.api.NIP61#createNutzapEvent(Amount, List<CashuProof>, URL, List<EventTag>, PublicKey, String) — use slimmer overload and add amount/unit via NIP60
    • nostr.event.tag.GenericTag(String, Integer) compatibility ctor
    • nostr.id.EntityFactory.Events#createGenericTag(PublicKey, IEvent, Integer)

Notes

  • Integration tests require Docker (Testcontainers). CI runs a separate job for them on push; PRs use the no-Docker profile.
  • See MIGRATION.md for complete guidance on deprecated API replacements.