Skip to content

v0.8.1

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Oct 02:03

GRAIN馃尵 v0.8.1

Client library fixes for apps serving more than one user
Release 路 10.7.2026
Full Changelog: v0.8.0...v0.8.1


A patch for the client library. Relay deployments should be unaffected. Nothing in this release touches how the relay stores, serves or validates events. If you're building on client/core, or you run grain's web client with more than one person logged in, upgrade.

This was expected. 0.8.0 was the first release of the client library, and the first app built on it outside grain's own web client is a server with many players logged in at once. It found these within days. That's what a 0.x version is for. The library surface is real and stable enough to build on, but it only gets proven when something other than grain uses it, and it will keep moving until it has been. That's why this is 0.8 and not 1.0. The relay features planned for 0.9 still have to land too.

None of that takes away from 0.8.0. It turned grain from a relay into a relay, a client, and an outbox-model client library in pure Go. 0.8.1 makes that library safe to use for more than one person at a time.


One Client, many users

core.Client is meant to serve many users at once, but some per-user state lived on the Client itself:

  • NIP-42 AUTH was shared. A relay authenticates a connection, not a request. AUTH went out on the connection every user of the Client shared, so once one user authenticated, the relay served everyone on that Client as them. That includes the NIP-17 gift wraps a relay only serves to their recipient.
  • Relay holds were one slot. Each login released the previous user's relays, and logout released nothing.
  • Fixed-relay mode and app relays were global. One user turning them on rerouted everyone's reads and writes.

All of it now lives on UserContext. Each user authenticates on their own connection, in a private per-user pool, and only their requests go over it. Everything else still uses the shared pool. The shared pool never authenticates. Relay holds, fixed relays and app relays are per user, layered over the Client's app-wide defaults. uc.Close() releases a user's holds and ends their NIP-42 sessions.

grain's web client now keeps one UserContext per logged-in pubkey and releases it when that user's last session ends. Authenticating to a relay from the relay manager uses a challenge from your own connection. Your fixed relays, app relays and relay connects only affect you.

Queries reach relays the way they were written

Tag filters and time windows were not being encoded right. A #d or #p constraint went out nested under a "#" key, and since/until went out as date strings instead of unix seconds. Relays ignored or rejected them, so looking up an addressable event by its d tag, or anything by p tag or time window, came back wrong. Filter now encodes to the NIP-01 wire format. The relay was never affected because it parses REQs on its own.

Events from relays are verified

Events coming in from relays were handed to the app without checking the id or signature. Any relay could forge events from any pubkey. Every inbound event is now checked before it's delivered. Events that match none of the subscription's filters are also dropped, so a relay can't answer a request for one thing with a validly signed event for something else.

Web client sessions

  • Fixed a data race on the session's last-active time when one session made concurrent requests.
  • The session cookie is now Secure when the request came over HTTPS, directly or through a TLS proxy that sets X-Forwarded-Proto. Plain http on localhost still works.

For library users: what changed

Make one UserContext per user and make that user's requests through it:

uc := client.NewUserContext(pubkey, core.WithSigner(signer))
defer uc.Close()

_ = uc.HoldRelays(relays)                      // per user, others untouched
_ = uc.SignAndAuthenticate(ctx, relayURL)      // NIP-42 on this user's own connection
evs := uc.QueryEvents(ctx, filter, relays)     // served as this user
res, _ := uc.PublishEvent(ctx, event, nil)     // routed under this user's prefs

New on UserContext: Subscribe, StreamEvents, QueryEvents, FetchEvents, PublishEvent, PublishEventWithRetry, PublishEventStream, RouteFetch, RouteMetadata, RoutePublish, HoldRelays, AddHeldRelay, DropHeldRelay, HeldRelays, ReleaseRelays, AppRelays, SetAppRelays, AuthChallenge, Authenticate, SignAndAuthenticate, AuthStates, IsAuthenticated, Deauthenticate, Close. Also core.VerifyEvent and nostr.TagFilterName.

Breaking or changed:

  • Client.SendAuth always returns ErrSharedAuth. Authenticate per user with UserContext.Authenticate or SignAndAuthenticate.
  • RelayPool.SendAuth and MessageRouter.MarkAuthed are gone. MessageRouter.RouteAuth takes the connection the challenge arrived on.
  • UserContext.PinFixedRelays and ClearFixedRelays now apply to that user only. Client.SetFixedRelays is the app-wide default.
  • Filter.Tags keys are bare tag names ("d", not "#d"). FilterBuilder.Tag stores them that way. A leading # is still accepted.
  • The Client-level Subscribe, QueryEvents and the rest are anonymous. ReplaceRelayConnections, SwitchToUserRelays and SwitchToIndexRelays are a single slot for single-user apps.

Still limited: AuthChallenge needs a relay that sends its challenge on connect. Relays that only challenge after a restricted request aren't supported yet.

The client library guide has a new "Many users on one Client" section.