Commit Graph
14640 Commits
Author SHA1 Message Date
Claude d7e21fb3bd Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-04 19:04:22 +00:00
Vitor PamplonaandGitHub c6be3ee3a3 Merge pull request #3118 from mstrofnone/docs/namecoin-design-refresh
docs(namecoin): refresh NIP-05 design doc to match current main
2026-06-04 15:03:37 -04:00
Vitor PamplonaandGitHub 8a156261cd Merge pull request #3132 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-04 15:03:24 -04:00
Crowdin Bot 336695dd55 New Crowdin translations by GitHub Action 2026-06-04 19:03:17 +00:00
Vitor PamplonaandGitHub 38724cc3f5 Merge pull request #3133 from vitorpamplona/claude/wizardly-fermat-y7NBf
Move authenticated identity from policy to connection scope
2026-06-04 15:00:58 -04:00
Claude 4ce5f6b1a3 feat(dm): per-relay reach markers for NIP-17, like NIP-04
The gift-wrap history loader exposed only relayCount + reachedBack, so the
conversation's in-stream marker trail showed NIP-04 relays paging back but
never NIP-17 ones. Give AccountGiftWrapsHistoryEoseManager the same
relayProgress map (per-relay reachedUntil / done / stalled) the
per-conversation NIP-04 loader publishes, refreshed on every page, stall,
CLOSE, and cannot-connect.

Move the shared RelayPagingProgress data class out of the NIP-04 UI-package
assembler into service/relayClient/eoseManagers so the gift-wrap manager
can produce it without the service layer depending on a UI package.

ChatroomView now merges both protocols' progress into the gap markers, each
contributing only while it is still paging; a relay that serves both (the
DM inbox relays) collapses to one marker. Markers hide once both protocols
are exhausted.
2026-06-04 18:59:03 +00:00
Claude 9f0ecd549f refactor(dm): page rooms-list NIP-04 history per relay; drop dead pager code
Convert ChatroomListNip04HistorySubAssembler from the round-based model to
the per-relay-independent one, matching the per-conversation NIP-04 loader
and the gift-wrap history loader. A single loadMore opens one window
spanning the whole walk; each relay continues itself on its own non-empty
EOSE (beginRound([relay]) + invalidateFilters), and silent/unreachable
relays are marked stalled (kept open) with exhaustion coming from the
WindowLoadTracker's silence + connect-grace backstops (tracksReqSends=true).
The rooms list now pages both protocols identically.

This was the last user of the round-tally + give-up machinery, so remove it
from UntilLimitPager: roundEventCount(), onClosed()/giveUp() and the
givenUp/closedStreak cursor state + GIVE_UP_AFTER_CLOSES. activeRelays now
excludes only done relays. Delete UntilLimitPagerGiveUpTest (tested the
removed give-up path; abandonment is now the WindowLoadTracker's job, which
its own test covers).

Also drop the now-dead loadEverything/autoLoadAll and the no-progress retry
(no callers; with independent paging one loadMore already walks each relay
to its bottom, and the tracker's backstops cover cold starts).
2026-06-04 18:55:44 +00:00
Claude 9497739c7e docs(quartz/relay): remove auth-scope component diagram 2026-06-04 18:48:26 +00:00
Claude 60b8629a27 refactor(dm): page NIP-17 gift-wrap history per relay, like NIP-04
The gift-wrap history loader was round-based: loadMore asked every active
relay together and the next page only went out after the whole round
settled, so one slow or auth-walled relay throttled the cadence and fast
relays idled until the laggards finished. The per-conversation NIP-04
loader already pages each relay independently; this brings NIP-17 to the
same model.

Now a single loadMore opens one window spanning the whole walk, and each
relay continues itself the instant it EOSEs a non-empty page
(pager.beginRound([relay]) + invalidateFilters, which the sub layer diffs
so only the advanced relay re-REQs). Fast relays race to the bottom while
slow ones catch up in the background. The WindowLoadTracker switches to
tracksReqSends=true with an onAbandoned handler that marks silent/
unreachable relays stalled (kept open, still trying) rather than giving up
on them - exhaustion comes from the window settling via the tracker's
silence + connect-grace backstops, which also removes the need for the old
no-progress retry loop.

Drop the now-dead loadEverything/autoLoadAll (no callers; with independent
paging one loadMore already walks each relay to its bottom). Pin the
history floor per window to keep un-advanced relays' filters stable across
the per-EOSE invalidateFilters, and keep reachedBack monotonic over all
relays.
2026-06-04 18:36:07 +00:00
Claude d2e64a0339 docs(quartz/relay): update component diagram to the merged design
Reframe from a forward-looking plan diff to the current architecture, and fix
the onAuthenticated signature to (event): Boolean (pubKey was dropped). All
structural elements already matched the merged code.
2026-06-04 18:29:52 +00:00
Claude 89f55a2509 feat(chats): show relay count and reach-back on the reply loader
The unloaded-reply loader matched the history card's chrome but dropped
its status line. Surface the same detail: which protocol, how many relays
it's still asking, and how far back it has paged - reusing the card's
historySubtitle so the inline loader and the oldest-end card read
identically.
2026-06-04 17:42:52 +00:00
Claude 0791ae9d40 test(quartz/relay): assert authenticatedUsers is scoped per connection
The auth set is already a per-RelaySession instance field (one session per
connect(), fresh policy per connection), so two connections never share auth
state. Add a regression test: authenticate different pubkeys on two connections
of the same server and assert each scope holds only its own — no union leak.
2026-06-04 17:42:48 +00:00
Claude 6fb735018c refactor(quartz/relay): drop redundant pubKey param from onAuthenticated/authorize
pubKey was always event.pubKey (the NIP-42 signer is the authenticated
identity), so the parameter was pure redundancy. onAuthenticated(event) and
authorize(event) now read event.pubKey directly; the engine still commits
cmd.event.pubKey. Removes the now-unused HexKey imports from IRelayPolicy and
PolicyStack.
2026-06-04 17:32:24 +00:00
Claude a54d9c92a7 refactor(quartz/relay): move auth identity from policy into connection scope
Authentication state (who is logged in on a connection) is connection scope,
not a policy decision. It was stored on FullAuthPolicy, which forced the
AuthScopedPolicy marker, the PolicyStack union, and a downcast in
RequestContext just to route it back out as scope.

Now the engine owns it: RelaySession holds the authenticatedUsers set behind
the (now public) requestContext; the data plane and policies read it through
RequestContext. The policy stays pure decision —
- onConnect(scope, send): a per-connection policy captures the read-only scope
  to gate on; shared singletons ignore it.
- onAuthenticated(): Boolean: the policy's vote on whether to record the
  verified pubkey (default false, so blind-accept policies never record an
  unverified identity). FullAuthPolicy runs authorize() then votes true.
- RelaySession.handleAuth performs the single, engine-side commit after the
  whole chain approves and a verifying policy votes to record.

FullAuthPolicy keeps all auth logic (challenge, accept(AuthCmd), gating,
authorize) and gains a protected authenticatedUsers accessor over the scope for
subclasses (restricted content / filter rewrite). Deletes AuthScopedPolicy,
PolicyStack.authenticatedUsers, and the RequestContext downcast.
2026-06-04 16:45:03 +00:00
davotoula 21f722767d refactor: use lambda Log overload for interpolated log calls 2026-06-04 18:15:17 +02:00
Claude 6943c1a056 docs(quartz/relay): component & data-flow diagram for the auth-scope plan 2026-06-04 16:03:00 +00:00
Claude efecbc40ab style(chats): match unloaded-reply loader to the history loading card
Give LoadingReplyNote the same chrome as DmHistoryLoadingCard - rounded
translucent surface, spinner-in-a-box, status line - so an unloaded reply
reads as the same 'reaching back into history' state, just inline in the
quote instead of at the oldest end.
2026-06-04 16:01:32 +00:00
Claude 0008f676b6 docs(quartz/relay): plan to move auth identity from policy to connection scope 2026-06-04 15:56:36 +00:00
Claude 81701ea751 feat(chats): walk DM history to load a reply's unloaded target
Reply quotes inside a conversation rendered the same 'post not found'
BlankNote as the main feeds when the target message had not been paged in
yet. But a reply target in a DM is not missing - it is simply older than
the loaded window, and for NIP-17 the inner rumor id is not even queryable
on relays (only the outer gift-wrap id is), so the only way to surface it
is to keep paging gift-wrap history until the wrap carrying it decrypts.

Add LoadingReplyNote: a custom inner-quote placeholder that shows a small
spinner + 'looking for the original message' and drives the matching
history pager's loadMore in a loop (gated on its own loadingMore/exhausted)
until the target decrypts - at which point WatchNoteEvent crossfades the
real message in and disposes the loader - or the protocol's history runs
dry, settling into the terminal 'not found' text. It runs regardless of
scroll position so opening a thread pulls an off-screen reply target in on
its own; the loop is idempotent so multiple loaders and the scroll loader
coalesce onto one paging window.

Wire it in via a new optional onBlank slot on ChatroomMessageCompose,
chosen by the parent message's protocol (gift-wraps vs NIP-04). Public
chats and marmot groups keep the default blank.
2026-06-04 15:47:32 +00:00
Claude a98f5fddd2 refactor(quartz/relay): move authenticatedUsers off IRelayPolicy to a marker
Keep the universal policy interface free of NIP-42: instead of a default
authenticatedUsers on IRelayPolicy, add an opt-in AuthScopedPolicy mixin that
only FullAuthPolicy (and PolicyStack, which unions its auth-tracking members)
implements. RequestContext.authenticatedUsers resolves it via an `as?
AuthScopedPolicy` downcast, defaulting to empty — so non-auth relays carry no
auth concept, and the accessor now earns its keep by encapsulating that cast
(ctx.policy is IRelayPolicy and no longer exposes the set directly).
2026-06-04 15:36:40 +00:00
Claude 3d6acde472 feat(quartz/relay): thread a per-connection RequestContext into EventSource
Non-storage relays answer a REQ purely from filters: EventSource.events()
got no session/auth context, even though the connection's policy already
knows the authenticated pubkey(s). That walled the auth state off from the
code that produces events, forcing a shared mutable holder + hand-wired
RelaySession to build any caller-aware relay (NIP-50 search scored from the
viewer, DM-style restricted content, paid/allow-listed sets, per-connection
tenancy).

Introduce RequestContext (connectionId, authenticatedUsers, policy) and pass
it through the read path: RelaySession -> SessionBackend.query/count/
countResult -> EventSourceBackend -> EventSource. authenticatedUsers is now
exposed on IRelayPolicy (default empty, overridden by FullAuthPolicy and
unioned by PolicyStack) and read live, so a REQ after AUTH sees the freshly
authenticated pubkey(s). For richer per-connection state, downcast ctx.policy.

EventSourceServer.serve { } is now usable for auth-scoped relays without a
side channel. Adds a test proving ctx.authenticatedUsers reaches the source
after a NIP-42 handshake; updates RELAY.md.
2026-06-04 15:25:39 +00:00
Claude bd77e93818 fix(chats): surface streamed strangers in New Requests
The additive predicate in ChatroomListNewFeedFilter required
room.senderIntersects(followingKeySet) to be true, the exact inverse of
the full feed() rebuild, which includes a room only when the sender is
NOT followed. So new gift wraps / NIP-04 DMs from strangers streaming in
through the additive update path were all rejected, and the New Requests
list never grew past whatever feed() had computed at screen-open time.

On a heavy account this showed as the list freezing at a handful of rooms
while tens of thousands of events decrypted and history exhausted -
'loading but not changing the screen'. Negate the senderIntersects term
so the additive path matches feed().
2026-06-04 15:05:18 +00:00
Claude 0077c136a5 fix: keep the DM history 'reached back' date monotonic
The round-model history cards computed reachedBack = deepestUntil over
the ACTIVE relays only. When the deepest relay finished paging and
dropped out of the active set, the min jumped to the next-active (newer)
relay's cursor, so 'back to X' lurched FORWARD to a more recent date —
un-reaching history it had already loaded. Visible in the giftwrap trace:
the deepest relays reach ~2023, then once they finish and only the
shallow inbox.nostr.wine (~70d) remains, deepestUntil(active) snaps back
to ~70d.

Compute it over ALL relays (including finished ones, which keep their
deep cursor), matching the convo path. Now it only ever moves older.
Applied to both round-model managers (giftwrap + rooms NIP-04).

Note: a large *forward* jump (e.g. ~3 years in one step) is still
expected and correct — a dense relay can return a full 10k-event page
spanning years, so the oldest-loaded date legitimately leaps. No
messages are skipped; each relay pages contiguously.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 23:24:59 +00:00
Vitor Pamplona 3db3037f92 seeing if this works for jitpack 2026-06-03 19:22:29 -04:00
Vitor Pamplona b0a6baaddc Tries to fix the compilation issue with jitpack 2026-06-03 18:58:19 -04:00
Claude 7fd44155d3 Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ 2026-06-03 22:41:14 +00:00
Vitor PamplonaandClaude Opus 4.8 d92c4be096 fix: recover Tor from a wedged Arti guard sample on startup
On a flaky network Arti records circuit failures past the first hop as
"indeterminate" and, once a guard's indeterminate ratio crosses 0.7,
permanently disables it. Disabled guards are never re-enabled nor removed
from the sample (60-day lifetime), and the sample is capped at 60. Arti
normally refills usable guards when they fall below 20, but a full sample
of unusable guards leaves no room — replenishment wedges and every circuit
returns AllGuardsDown. The state persists in guards.json and bootstrap
still "succeeds", so no existing self-heal path fires: Tor stays broken
across restarts. This is the long-standing, hard-to-reproduce production
"can't connect to Tor" bug.

On init, scan guards.json and, if any non-empty guard selection has zero
usable guards (disabled or unlisted_since set), wipe Arti state so the
next bootstrap rebuilds a fresh sample. A single usable guard is enough to
build circuits, so recovery only triggers at the last resort to preserve
guard-set stability (anonymity) and avoid pointless churn on bad networks.

Verified on emulator: poisoned 60/60 -> wipe -> fresh 20/20 usable sample
-> .onion OnOpen, zero AllGuardsDown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 18:27:31 -04:00
Claude edbdbdcc8e fix: give up on unreachable relays so the rooms list resolves
Follow-up to the no-progress retry: in this trace vitor/mom/nos.lol never
connect at all (only damus gets a REQ — the other three cannot-connect
for both live and history). The retry correctly re-attempts, but it can't
reach relays that won't connect, so every round is 0 events, no relay is
ever 'done', exhausted never completes, and it retries forever — a cold,
empty feed stays on the spinner.

The round-model history had no give-up for repeated cannot-connect (only
for CLOSE), unlike the convo path. Route onCannotConnect through the same
pager give-up as onClosed, so a relay that's unreachable for a few rounds
is abandoned, exhaustion completes, and the screen resolves (to the
loaded rooms, or empty + retry) instead of spinning forever. The streak
resets on any contact, so a merely slow relay that connects within a few
attempts isn't dropped. Applied to both round-model managers.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 21:52:38 +00:00
Vitor PamplonaandGitHub 99ab2cb720 Merge pull request #3131 from vitorpamplona/claude/quartz-relay-ergonomics-7sTEN
Add ReqResponderServer for non-storage relays + NIP-50 search support
2026-06-03 17:45:49 -04:00
Claude 1c54a877e2 refactor(quartz): split relay server package into engine / backend / policies
The relay server package had 14 top-level files. Group by concern:

- relay/server/          engine + entry points: NostrServer, EventSourceServer,
                         RelayServerBase, RelaySession, ConnectionRegistry,
                         RelayServerListener, NegSessionRegistry
- relay/server/backend/  data plane: SessionBackend, EventSource,
                         EventSourceBackend, LiveEventStore, IngestQueue
- relay/server/policies/ policy model + impls: IRelayPolicy(+PolicyResult) and
                         RelayLimits move in alongside the existing policies
- relay/server/inprocess/ unchanged

Layering is acyclic: engine -> {backend, policies} -> nip01Core.

This moves three already-published classes (IRelayPolicy, LiveEventStore,
IngestQueue), so external quartz consumers re-import — a source-only change,
no behavior change. In-repo callers (geode) updated. Full quartz + geode
suites green; RELAY.md source map updated.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 21:44:12 +00:00
Claude a3b4df8e6e fix: rooms-list history stuck on 'Loading feed' after a no-progress round
On a cold start with no cached rooms and no recent (<7d) DMs, the rooms
list relies entirely on history paging. If a round settles via
cannot-connect / CLOSE (relays dropped during a connect storm) instead of
a clean empty-EOSE, no relay is marked done, exhausted stays false, and
the no-progress guard then refuses to retry — recovery would only come if
the relays happened to reconnect AND re-EOSE the open subscription on
their own. Meanwhile the empty feed shows LoadingFeed() forever (it needs
BOTH protocols exhausted to show 'no conversations').

When a round makes no progress but isn't exhausted, actively retry after
a 5s backoff (clearing the no-progress gate) instead of waiting
passively. Paced so a fast-CLOSE / rate-limited relay isn't hammered, and
it stops once the protocol exhausts or a round makes progress. Applied to
both round-model history managers (giftwrap + rooms NIP-04).

Not caused by the stall-gate drop — the empty-feed search path is
unchanged by that commit; this is a pre-existing fragility the severe
76s connect storm in this cold start exposed.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 21:34:50 +00:00
Claude 66481dac05 refactor(quartz): rename ReqResponder -> EventSource; drop JwtAuthPolicy doc
The SPI name leaned on "Req" (the NIP-01 command), which isn't self-explanatory.
Rename to read as what it is — a source of events for a query:

- ReqResponder        -> EventSource          (method respond() -> events())
- ReqResponderBackend -> EventSourceBackend   (param responder -> source)
- ReqResponderServer  -> EventSourceServer
- *Test + RELAY.md + KDoc references updated to match.

Also drop the JwtAuthPolicy snippet from RELAY.md (it was only an illustrative
doc example, never a real class); the surrounding prose still documents the
`authorize` bridge hook.

Pure rename + doc edit, no behavior change; full suite green.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 21:09:24 +00:00
Claude 98fb872005 feat: drop the rooms-list stall-gate — page to exhaustion while in view
The stall-gate stopped widening once a load surfaced older messages but
no new conversation row, leaving the boundary card in a confusing paused
state (not loading, not caught-up). It existed to brake the OLD
pagination model, where every widen re-downloaded the whole window; the
per-relay until+limit paging doesn't re-download, so the brake is
obsolete — and a visible boundary card means the user is waiting for
more, so pausing there made no sense.

Now while the boundary is in view each protocol pages round after round
until genuinely exhausted (empty page), then shows 'all caught up'. The
card is only ever loading or caught-up, never paused. Removes the
autoFillRoomMark mark plumbing and the getMark/setMark gate.

Tradeoff: a user with few conversations but deep message history pages
that history to the end on reaching the bottom — bounded and efficient
now (no re-download), terminated by exhaustion + the no-progress guard.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 20:57:27 +00:00
Claude c13dbad99d refactor(quartz): simplify relay server internals (no behavior change)
Cleanup pass (4 review angles), behaviour-preserving, full suite green:

- Extract RelayServerBase: NostrServer and ReqResponderServer duplicated
  connect/serve/buildPolicy/activeConnections and the connection scope verbatim
  (ConnectionRegistry only unified the bookkeeping). They now share one engine
  base and contribute only their backend + teardown; ReqResponderServer is ~10
  lines, NostrServer just its ingest/store wiring.
- HyperLogLog.leadingZeroBits: replace the hand-rolled per-byte bit loop with
  stdlib Int.countLeadingZeroBits() (- 24 for the 0..255 byte).
- LimitsPolicy.clampLimits: drop the `var changed` + throwaway-list map for a
  `none{} -> map{}` that's simpler and allocates nothing when no filter is
  clamped (the common case once maxLimit is set).
- PolicyStack: collapse the two first-non-null hook loops to firstNotNullOfOrNull.
- RelaySession.handleAuth: use OkMessage.rejected(MachineReadablePrefix.ERROR, …)
  instead of hand-writing the "error:" prefix, matching the COUNT path.

Skipped: rewriting FullAuthPolicy's pre-existing auth-required:/invalid: reason
strings to MachineReadablePrefix — unchanged context lines, out of this diff's
scope.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 20:43:52 +00:00
Claude 432a19f7c0 feat: label the in-stream relay markers 'Relay sync:'
The bare glyph markers (✓ 8 · ↓ 1) had no context. Prefix each with a
translatable 'Relay sync:' label and add '·' separators between states,
so a marker reads e.g. 'Relay sync: ✓ 8 · ↓ 1' or 'Relay sync: ↓ nostr.wine'.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 20:25:57 +00:00
Claude 9a19a0f346 refactor(quartz): remove pendingNewlyAdded — commit auth once, not commit-then-rollback
The pendingNewlyAdded field was a rollback token faked through instance state:
FullAuthPolicy.accept(AuthCmd) committed the pubkey eagerly, so a later
rejection (composed policy or a throwing hook) had to be undone, and the field
existed only to avoid dropping a pubkey that was already authenticated.

Fix the root cause — the eager commit. accept(AuthCmd) now only validates; the
pubkey is recorded in FullAuthPolicy.onAuthenticated (made final), which the
engine calls only after accept AND the whole policy chain approve the AUTH.
External-auth bridges override a new open `authorize` hook that runs before the
commit; throwing rejects the login with nothing committed to undo.

This deletes pendingNewlyAdded, IRelayPolicy.onAuthenticationFailed, its
PolicyStack override, and both rollback call-sites in RelaySession.handleAuth —
and makes the prior compose-after-reject / failed-re-AUTH cases correct by
construction (no rollback to get wrong). Tests updated to override `authorize`;
the two regression tests pass unchanged.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 20:25:12 +00:00
Claude 81dd2585dc fix: DM history card showed text but a blank icon when paused
The status card's icon slot only handled two states — caught-up (✓) and
loading (spinner) — leaving it blank in the third: not exhausted but not
actively loading (the rooms-list auto-fill stops short of exhaustion via
the no-new-rooms stall-gate, or between round-model pages). The card then
read 'Older … messages · N relays' with nothing on the left. Fill that
paused state with a static '⋯' glyph so the slot is never empty.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 20:19:44 +00:00
Claude a21f811e87 refactor(quartz): clarity pass on relay server internals
Behaviour-preserving readability improvements (full suite green):

- LimitsPolicy: drop the <T : Command> generic reject helpers (which forced
  rejectSubId<ReqCmd>(...) call-site type args). Each check is now a
  "rejection reason or null" function (eventRejection / subscriptionRejection)
  and the accept overloads just wrap a non-null reason — the three overloads
  read almost identically.
- Extract ConnectionRegistry: NostrServer and ReqResponderServer duplicated
  the connection bookkeeping (stable-id keying, active gauge, once-only
  teardown accounting). That subtle logic now lives in one named class both
  servers delegate to; their connect()/close() shrink to the parts that
  actually differ (the backend and what else teardown closes).
- HyperLogLog.addPubKey: rename `ri` -> `registerIndex`.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 20:15:06 +00:00
Claude 366d9435d7 fix(quartz): pre-merge review fixes — auth rollback, listener rename
Final-review findings on the relay tooling:

- Auth bypass (security): FullAuthPolicy.accept(AuthCmd) commits the pubkey,
  but handleAuth only rolled back on the hook-throw path — a policy composed
  AFTER FullAuthPolicy that rejects the AuthCmd left the connection
  authenticated behind an OK false. handleAuth now also rolls back on the
  reject path, preserving OK-true-iff-authenticated for any composition order.
- Failed re-AUTH no longer drops a prior valid auth: onAuthenticationFailed
  removes only the pubkey THIS AUTH newly added (tracked via Set.add's return),
  not one already authenticated earlier on the connection.
- Rename the server-side RelayConnectionListener -> RelayServerListener to
  avoid colliding with the existing client-side
  relay.client.listeners.RelayConnectionListener (published-API clarity).
- RelayLimits.toNip11Limitation clamps created_at bounds to Int range so a
  post-2038 epoch second can't wrap negative in the NIP-11 document.
- Add regression tests for both auth cases (reject-after-FullAuth; failed
  re-AUTH keeps prior auth).

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 19:41:55 +00:00
Claude 648a00ef63 feat(quartz): add missing NIP-11 banner field to relay info document
Nip11RelayInformation modeled `icon` but not `banner` (NIP-11's wide
promotional image, distinct from the square icon), so relays advertising a
banner couldn't round-trip it. Add the field + a round-trip test.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 19:22:37 +00:00
David KasparandGitHub 5901f2e453 Merge pull request #3130 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-06-03 21:06:42 +02:00
Claude 37b07057b2 refactor(quartz): enforce session-level limits through policy hooks
Previously max_message_length and max_subscriptions were hard-coded in
RelaySession behind a parallel `limits` param, while the per-command limits
went through LimitsPolicy — two mechanisms, and a custom policy couldn't
influence the session-level ones.

Unify them: add two default-noop hooks to IRelayPolicy —
acceptMessage(raw) (pre-parse) and acceptSubscription(subId, openCount) —
chained through PolicyStack so they compose across multiple policies.
LimitsPolicy now implements all limit checks; RelaySession just invokes the
hooks and no longer takes a `limits` param. Servers compose LimitsPolicy
whenever `limits` is set and keep `limits` only to advertise via NIP-11.

Behaviour is unchanged (oversized -> NOTICE invalid:, sub cap -> CLOSED
rate-limited:); the enforcement now lives in the policy layer. Adds direct
hook unit tests; existing end-to-end limit tests still pass.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:58:20 +00:00
Claude 40b60e7bcc fix: compact relay markers + no '0 relays' flash on the DM loading card
Two UI papercuts in the per-relay DM history surface:

- RelayReachMarker listed every relay name comma-joined per state, so a
  gap shared by many relays (e.g. all nine clustered at the live-tail
  floor on first open) overflowed into an unreadable line. Now each state
  shows the relay's host name only when it is the sole one of its state at
  that depth (the usual converged case); otherwise just a count, with
  maxLines/ellipsis as a backstop.
- The history status card briefly read 'loading from 0 relays': the count
  populated a beat after loadingMore flipped true (and again as the last
  relay settled). Set the relay count before raising the spinner in
  loadMore, and defensively drop the relay clause from the card subtitle
  when the count is 0.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 18:50:13 +00:00
Claude 70dcaa1b10 feat(quartz): relay limits as single source of truth + NIP-11 serving
A relay author no longer hand-rolls limit policies or wires NIP-11 twice.

- RelayLimits: one config that is both enforced and advertised. Pass it to
  NostrServer/ReqResponderServer and every limit is applied; toNip11Limitation()
  renders the same numbers into the NIP-11 limitation block so they can't drift.
- LimitsPolicy: per-command enforcement (max_content_length, max_event_tags,
  created_at bounds reject EVENT; max_filters / max_subid_length reject
  REQ/COUNT; max_limit clamps, default_limit fills) with invalid: prefixes.
  Servers prepend it automatically when limits declares command caps.
- RelaySession: session-level caps that a policy can't see — max_message_length
  (NOTICE before parse) and max_subscriptions (rate-limited: CLOSED on new sub).
- NIP-11 serving: Nip11RelayInformation.toJson() + CONTENT_TYPE
  (application/nostr+json); the existing model was parse-only.
- Tests for the policy, the session-level caps end-to-end, and the
  limits->NIP-11 round trip; RELAY.md Limits + Serving NIP-11 sections.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:41:06 +00:00
Claude 952da685d8 feat(quartz): NIP-45 approximate COUNT — HyperLogLog construction + wire support
The HLL aggregation side (estimate/merge/encode) existed but the construction
side did not, and the Jackson wire (de)serializer silently dropped the `hll`
field — so a JVM/Android relay could not actually answer COUNT with HLL.

- HyperLogLog.addPubKey(): the NIP-45 construction (register index = pubkey
  byte at the filter offset; value = leading-zero-bits from offset+1, +1),
  KMP-safe. Plus HyperLogLog.builderFor(filter) and an HllBuilder that streams
  event pubkeys into registers and yields an approximate CountResult.
- Plumb CountResult through the count path: SessionBackend.countResult /
  ReqResponder.countResult (default = exact count(); override for approximate/
  hll); RelaySession sends the returned CountResult.
- Fix CountResultSerializer/Deserializer (jvmAndroid) to write/read the `hll`
  hex field, matching the kotlinx serializer the native targets already use.
- Tests for construction (index/value/merge-idempotence) and the wire path;
  RELAY.md Approximate COUNT section.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:29:01 +00:00
Crowdin Bot bd9f927041 New Crowdin translations by GitHub Action 2026-06-03 18:21:24 +00:00
Claude 319ffb729a feat(quartz): relay connection observability + stable connection ids
Gives relay operators metrics/logging hooks without patching the engine, and
removes the hashCode()-keyed connection registry the audit flagged.

- RelaySession gains a stable, process-unique `id` (monotonic counter).
- RelayConnectionListener (onConnect/onDisconnect, no-op default) is accepted
  by NostrServer and ReqResponderServer; both now key their connection
  registry by `id` instead of hashCode() (no more identity-collision hole).
- Both servers expose a live `activeConnections` gauge. Teardown accounting is
  idempotent (double close counted once) and onDisconnect fires for any
  connections still open at server close.
- Tests + RELAY.md Observability section.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 18:21:05 +00:00
davotoulaandClaude Opus 4.8 54f746edc3 feat: translate settings-search and share-to-DM strings (cs, de, sv, pt-BR)
Add cs-rCZ, de-rDE, sv-rSE and pt-rBR translations for the new settings
search field (placeholder + no-results) and the share-to-DM flow strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:16:36 +02:00
Claude bcb4b2b964 fix(quartz): roll back authentication when the post-auth hook rejects
Audit finding: FullAuthPolicy.accept(AuthCmd) added the pubkey to the
authenticated set before onAuthenticated ran, so a bridge that threw from
onAuthenticated (its whole point — reject when e.g. a JWT exchange fails)
produced an OK false while the connection stayed authenticated server-side.
Subsequent REQ/EVENT/COUNT were then allowed despite the failed login — an
auth bypass.

- Add IRelayPolicy.onAuthenticationFailed(pubKey) (default no-op), forwarded
  by PolicyStack and overridden by FullAuthPolicy to drop the pubkey.
- RelaySession.handleAuth calls it when onAuthenticated throws, restoring the
  invariant that a client treated as authenticated is exactly one that got
  OK true. The rollback is itself guarded so a misbehaving policy can't also
  swallow the failing OK.
- Tests: failed-hook now asserts the connection is NOT authenticated and that
  a follow-up REQ is rejected with auth-required.
- RELAY.md: note that throwing from onAuthenticated rolls auth back.

https://claude.ai/code/session_016YBS2pWCBSDgAMthHzfCTr
2026-06-03 17:45:52 +00:00
Claude ab7aa44d21 fix: pin the convo history floor per window to stop re-REQ churn
startUntil() is 'now - 1week' and was recomputed on every updateFilter,
so it drifted forward in real time. A relay that hadn't advanced its
cursor (first page in flight, or empty) has until = that floor, so its
filter changed every time ANY other relay's EOSE triggered
invalidateFilters — the subscription saw a 'new' filter and re-REQed it.
The trace showed nostr.oxtr.dev asked twice (until 1779903206 then
...207, +1s) and reaching the bottom (done) twice, and it fed extra
ditto REQ->CLOSE churn and rate-limiting on the busy relays.

Pin the floor once when the window starts and reuse it for the window's
life (which ends in ~2s once every relay is done or stalled), so an
un-advanced relay's filter stays stable and only a relay whose cursor
genuinely advanced is re-REQed.

https://claude.ai/code/session_01B1fmmmX8JjQWH3amMLdvcW
2026-06-03 17:41:45 +00:00