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 ofsend(), beforesendFrameGated()acquired thesessionGateread lock — so a concurrentclose()could acquire the write lock, close the session, and release it between the check and the actualsendMessage(), still letting the frame reach a closed session. Moved theisOpen()check insidesendFrameGated(), immediately after the read lock is taken, so the open-check and the write are now atomic with respect tocloseGated()(which holds the write lock). 2.0.6 already handled the dominant case (the session already closed whensend()is invoked); this closes the narrow in-flight-close window.
[2.0.6] - 2026-05-27
Fixed
NostrRelayClient.send()now guards onclientSession.isOpen()before writing, mirroringsubscribe(). A per-subscription NostrCLOSEissued while a relay connection was being torn down previously reached Tomcat'ssendText, whereWsRemoteEndpointImplBase.sendMessageBlockInternalinvokesdoClose()mid-write and emits a WS CLOSE frame while the text write is still pending on the async channel — throwingIllegalStateException: Concurrent write operations are not permitted, which surfaced viahandleTransportErroras a transport-error reconnect storm during subscription teardown of a breaking connection (spec-026). The application-level locks (the decorator, thesessionGate, and the upstream adaptersendLock) 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 withIOException("WebSocket session is closed")on a closed session — the doomed write never enters Tomcat's close-mid-write path. +regression testsend_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 transportIOExceptionfrom the gated send previously bypassedpendingRequestcleanup, so the nextsend()(including an@NostrRetryableretry) hitIllegalStateException: A request is already in flightinstead of retrying.send()now clearspendingRequeston any send failure, not onlySessionLimitExceededException. (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'sConcurrentWebSocketSessionDecoratorbut called the no-argclientSession.close()(inclose()and thesend()timeout path). Spring 6.2.x's decorator overrides onlyclose(CloseStatus)(guarded bycloseLock); the no-argclose()falls through toWebSocketSessionDecorator.close()→delegate.close()with no coordination, sending a CLOSE frame straight to the Tomcat delegate while asendMessageflush was in flight (Tomcat permits only one write in flight →IllegalStateException: Concurrent write operations are not permitted). Routing throughclose(CloseStatus)alone is insufficient because itscloseLockis a separate lock from theflushLockguardingdelegate.sendMessage. Introduced aReentrantReadWriteLocksession 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 usesclose(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,Reactionenum,Response,Nip05Content,Nip05ContentDecoder,BaseAuthMessage,GenericMessage,IKey,GenericEventConverter,GenericEventTypeClassifier,GenericEventDecoder,FiltersDecoder,BaseTagDecoder,GenericEventValidator,GenericEventSerializer,GenericEventUpdater,GenericTagQuery,HttpClientProvider,DefaultHttpClientProvider. testAuthMessagetest andGenericEventSupportTestremoved (tested deleted classes).createGenericTagQuery()removed fromEntityFactory(only consumer of deletedGenericTagQuery).
Changed
RelayAuthenticationMessageandCanonicalAuthenticationMessagenow extendBaseMessagedirectly (previously extended the now-deletedBaseAuthMessage).BaseKeynow directly implementsSerializable(previously implemented the now-deletedIKeyinterface).Nip05Validatornow createsHttpClientinstances directly via aFunction<Duration, HttpClient>factory (previously used deletedHttpClientProvider/DefaultHttpClientProviderinterface).
[2.0.3] - 2026-05-08
Fixed
- Explicit close on
OverflowStrategy.TERMINATE; restores upstreamisOpen()==falsereconnect contract. Spring'sConcurrentWebSocketSessionDecoratorunderOverflowStrategy.TERMINATEonly sets a privatelimitExceededflag and throwsSessionLimitExceededExceptionfromlimitExceeded()— it does not close the delegate session. As a result, after 2.0.2 the upstream caller'sclientSession.isOpen()==false→ reconnect contract did not hold (the session stayed open after overflow).NostrRelayClient.subscribe()andNostrRelayClient.send()now detect aSessionLimitExceededExceptioncause in their respective catch blocks and callclientSession.close(CloseStatus.SESSION_NOT_RELIABLE)explicitly before rewrapping the exception. Non-overflowRuntimeExceptions continue to flow through the existing wrap-as-IOExceptionpath unchanged. The §6.7d concurrency test now strictly assertsclientSession.isOpen()==falseafter overflow, matching the spec's original contract.
[2.0.2] - 2026-05-08
Fixed
NostrRelayClient.subscribe()was callingclientSession.sendMessage(...)without holding the existingsendLock, while the same class'ssend()path correctly held it. Two threads racing insidesubscribe()(or one insubscribe()racing one insend()) could trigger the underlying writer'sIllegalStateException("The remote endpoint was in state [TEXT_FULL_WRITING]")(Tomcat / SpringStandardWebSocketSession) orBlocking message pending(Jetty), which thecatch (RuntimeException)block at the bottom ofsubscribe()rewrapped asIOException("Failed to send subscription payload", e). Downstream consumer (imani-gateway-coreaccount-app) observed ~70RelaySubscribeExceptionper 60 minutes once a circuit-breaker tuning fix unmasked the underlying race. Fixed by wrapping the underlyingWebSocketSessionreturned byconnectSession(...)in Spring'sConcurrentWebSocketSessionDecorator(session, sendTimeLimit, bufferSizeLimit, OverflowStrategy.TERMINATE)at construction time, so concurrentsendMessage()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@Autowiredfor 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 theNOSTR_WEBSOCKET_SEND_BUFFER_LIMIT/NOSTR_WEBSOCKET_SEND_TIME_LIMIT_MSenv-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(...)andforTestWithDecoratedSession(...)(two overloads — defaults and explicit) plus a package-private four-arg test constructor, so the newNostrRelayClientConcurrencyTestcan assert both the regression (raw-session reproduction of theIllegalStateExceptionrace) and the resolution (decorator-wrapped session serialises sends).
Changed
awaitTimeoutMsis now afinalfield 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
NostrRelayClientlog statements were emittingSending request to relay null: ...(and similar) once the WebSocket session had closed, because the relay URI was being read viaclientSession.getUri()which returnsnullafter close. Captured the URI in aprivate final String relayUrifield set in each constructor and replaced 8clientSession.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
Kindsutility class with staticintconstants 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", ...].GenericTagnow stores tag values asList<String>(replacingList<ElementAttribute>), providing direct access viagetParams().EventFilterbuilder API for composable relay filters:.kinds(),.authors(),.since(),.until(),.addTagFilter(),.limit(),.ids().RelayTimeoutException— typed exception replacing silent empty-list returns on relay timeout.ConnectionStateenum (CONNECTING,CONNECTED,RECONNECTING,CLOSED) for WebSocket connection state tracking.NostrRelayClientasync Virtual Thread APIs:connectAsync(...),sendAsync(...), andsubscribeAsync(...).Nip05Validator.validateAsync()andNip05Validator.validateBatch(...)for parallel NIP-05 validation workloads.- Spring Retry support (
@NostrRetryable,@Recover) consolidated directly intoNostrRelayClient.
Changed
- Module consolidation — merged 9 modules into 4:
nostr-java-util+nostr-java-crypto→nostr-java-corenostr-java-base+nostr-java-event→nostr-java-eventnostr-java-id+nostr-java-encryption→nostr-java-identitynostr-java-client(unchanged)
GenericEventis now the sole event class. All 39 concrete event subclasses removed. Events are differentiated byint kindinstead of Java type.GenericTagis now the sole tag class. All 17 concrete tag subclasses removed. Tags are a simplecode+List<String> params.GenericEvent.kindchanged fromKindenum to plainint. Builder simplified to.kind(int)only.GenericEvent.tagschanged fromList<BaseTag>toList<GenericTag>.GenericEventimplementsISignabledirectly (no longer extendsBaseEvent).EventMessagenow referencesGenericEventdirectly instead ofIEvent.IDecoder<T>type bound changed fromIDecoder<T extends IElement>to unboundedIDecoder<T>.TagDeserializernow always producesGenericTagwithList<String>params — no registry dispatch.GenericTagSerializersimplified to output[code, param0, param1, ...]directly fromList<String>.GenericEventDeserializersimplified — no subclass dispatch to concrete event types.NostrUtil.bytesToHex()now usesjava.util.HexFormatinstead of hand-rolled hex encoding.NostrUtil.hexToBytes()family of methods now usesjava.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.
StandardWebSocketClientrenamed toNostrRelayClient.SpringWebSocketClientabsorbed intoNostrRelayClient(single client class with retry support).- Relay subscription callbacks are now dispatched on Virtual Threads to avoid blocking inbound WebSocket processing.
DefaultHttpClientProvidernow uses a shared Virtual Thread executor instead of creating a new executor perHttpClient.- All
synchronizedblocks inNostrRelayClientreplaced withReentrantLockto 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
synchronizedcleanup block fromNostrRelayClient.send(...), usingReentrantLockconsistently to avoid VT pinning risk. - Relay timeout now throws
RelayTimeoutExceptioninstead of silently returning an empty list, allowing callers to distinguish "no results" from "timed out".
Removed
nostr-java-apimodule — all 26 NIP classes (NIP01–NIP99),EventNostr, factory classes, client managers, service layer, and configuration classes.nostr-java-examplesmodule — all 6 example classes.- 39 concrete event subclasses —
TextNoteEvent,DirectMessageEvent,ContactListEvent,ReactionEvent,DeletionEvent,EphemeralEvent,ReplaceableEvent,AddressableEvent, all Calendar/Marketplace/Channel/NostrConnect events, and more. UseGenericEventwith the appropriateint kind. - 17 concrete tag subclasses —
EventTag,PubKeyTag,AddressTag,IdentifierTag,ReferenceTag,HashtagTag,ExpirationTag,UrlTag,SubjectTag,DelegationTag,RelaysTag,NonceTag,PriceTag,EmojiTag,GeohashTag,LabelTag,LabelNamespaceTag,VoteTag. UseGenericTag.of(code, params...). - 27 entity classes —
UserProfile,Profile,ChannelProfile,ZapRequest,ZapReceipt,Reaction, all Cashu entities, all marketplace entities, and more. Kindenum — replaced byKindsutility class with staticintconstants.ElementAttribute— replaced byList<String>inGenericTag.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 classes —
AbstractFilterable,KindFilter,AuthorFilter,SinceFilter,UntilFilter,HashtagTagFilter,AddressTagFilter,GeohashTagFilter,IdentifierTagFilter,ReferencedEventFilter,ReferencedPublicKeyFilter,UrlTagFilter,VoteTagFilter,GenericTagQueryFilter. UseEventFilter.builder(). - Concrete serializers/deserializers —
AddressTagSerializer,ReferenceTagSerializer,ExpirationTagSerializer,IdentifierTagSerializer,RelaysTagSerializer,BaseTagSerializer,AbstractTagSerializer,CalendarEventDeserializer,ClassifiedListingEventDeserializer,CashuTokenSerializer. - Client classes —
WebSocketClientIF,WebSocketClientFactory,SpringWebSocketClientFactory,SpringWebSocketClient. UseNostrRelayClientdirectly. - Dead code —
Markerenum,RelayUrivalue object. - 5 old modules —
nostr-java-util,nostr-java-crypto,nostr-java-base,nostr-java-id,nostr-java-encryption(merged into the 4 remaining modules). - Dead
pollIntervalMsparameter 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-sizeandnostr.websocket.max-binary-message-buffer-sizeproperties.
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
JsonDeserializebuilder 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.shwith bump/tag/verify/publish/next-snapshot commands (supports--no-docker,--skip-tests, and--dry-run). - GitHub Actions:
- CI workflow
.github/workflows/ci.ymlwith Java 21 build and Java 17 POM validation; separate Docker-based integration job; uploads reports/artifacts. - Release workflow
.github/workflows/release.ymlpublishing to Maven Central, validating tag vs POM version, and creating GitHub releases.
- CI workflow
- 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 toscripts/release.sh.
Changed
- Roadmap project helper
scripts/create-roadmap-project.shnow 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.mdupdated with Maven/Gradle BOM examplesdocs/howto/use-nostr-java-api.mdupdated 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.Kindfacade — usenostr.base.Kindnostr.base.Encoder.ENCODER_MAPPER_BLACKBIRD— usenostr.event.json.EventJsonMapper#getMapper()nostr.api.NIP01#createTextNoteEvent(Identity, String)and related Identity-based overloads — use instance-configured sendernostr.api.NIP61#createNutzapEvent(Amount, List<CashuProof>, URL, List<EventTag>, PublicKey, String)— use slimmer overload and add amount/unit viaNIP60nostr.event.tag.GenericTag(String, Integer)compatibility ctornostr.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.mdfor complete guidance on deprecated API replacements.