Captures what's done (parse, RelayLimitsTracker, cleanup), the audit fixes to fold in (kotlinx parse leniency to match Jackson on iOS, drop stale NIP-22 labels, make LimitsMessage a data class), and the remaining roadmap (client apply helpers, server-side emit, NIP-11 bridge, amy tooling). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01464jkunWPtYhTReoc3fUQQ
5.4 KiB
Relay LIMITS message — status & roadmap
Support for the relay-to-client LIMITS frame (from
nostr-protocol/nips#1434),
where a relay advertises the current connection's rights and limits — on
connect and again whenever they change (e.g. after a NIP-42 AUTH flips
can_write). Live-verified against wss://pipe.imwald.eu/ (which sends AUTH
then LIMITS).
Note: LIMITS is not NIP-22 (that's Comment / kind 1111); it's the #1434
proposal and is label-based on the wire, so the code keys off the "LIMITS"
label rather than a NIP number.
Done (branch claude/limits-message-support-g5plrd, PR #3597)
- Parse —
LimitsMessagemodel (nip01Core/relay/commands/toClient/) with every #1434 field plus the two non-spec extensions this relay sends (auth_for_read/auth_for_write). Wired into both codecs: Jackson (LimitsDeserializer+MessageSerializer, jvmAndroid) and kotlinx (LimitsKSerializer+MessageKSerializer, iOS/native). Unknown fields are ignored, so a future field can't re-trigger the original "Message LIMITS is not supported" crash. - Cache/expose —
RelayLimitsTracker(nip01Core/relay/client/limits/), a passiveRelayConnectionListeneraccessory (modeled onRelayAuthenticator) that caches the latestLimitsMessageperNormalizedRelayUrland publishes a Compose-stableStateFlow<PersistentMap<url, LimitsMessage>>. Connection-scoped (dropped on disconnect). Wired inAppModulesasAmethyst.instance.relayLimits. - Cleanup — removed the unused experimental prototype
(
experimental/limits/Limits.kt+LimitProcessor.kt); renamed the client accessory to…Trackerto avoid colliding with the server-siderelay/server/policies/RelayLimits.
Audit fixes to fold in (small, do first)
From the 2026-07-16 review of the branch:
- kotlinx parse should be as lenient as Jackson.
MessageKSerializeris the iOS incoming path; todayLimitsKSerializer.deserializeFromElementthrows on a mistyped field ((tag as JsonArray),.jsonPrimitive.int) or a payload-less["LIMITS"](array[1].jsonObject), where Jackson coerces. Same frame → parsed on Android, dropped on iOS. UseintOrNull/booleanOrNull/longOrNull, guard the array cast, and default a missing payload object to an emptyLimitsMessage. - Drop the stale "NIP-22" labels in
LimitsKSerializerKDoc and the// NIP-22 wire formatcomments inMessageKSerializer/MessageSerializer(already removed fromLimitsMessage.kt). - Make
LimitsMessageadata classsoStateFlow.distinctUntilChangedsuppresses no-op emissions when a relay re-advertises identical limits, and tests get value equality. - Fold the explicit-
null→false/0coercion nuance into (1) via the…OrNullaccessors on both sides.
Roadmap
1. Client-side apply/enforcement (highest user-facing value)
#1434 says clients MUST apply limits when sending. Provide pure, testable
helpers on LimitsMessage (mirror the server's LimitsPolicy logic; the
deleted LimitProcessor in git history is a starting sketch):
clamp(filter)/clamp(filters)→ cap each filterlimittomax_limit.rejectionForPublish(event): String?→can_write,accepted/blocked _event_kinds,max_content_length,max_event_tags,min_pow_difficulty,created_at_msecs_ago/aheadwindow,required_tags.canRead()/canWrite()convenience.
Keep the helpers in quartz (pure, no side effects). Wire them into the send
path at the app layer (subscription/filter-assembly + publish), reading from
RelayLimitsTracker. Decide the UX for a rejected publish (surface vs
silently drop) with the maintainer — this is the one opinionated call.
2. Server-side emit (geode) — the symmetric half
The relay side already enforces (LimitsPolicy + RelaySession) and
advertises via NIP-11 (RelayLimits.toNip11Limitation()), but never sends
the dynamic LIMITS frame. Add:
RelayLimits.toLimitsMessage(canRead, canWrite, authForRead, authForWrite)next totoNip11Limitation(), so one source of truth drives NIP-11 and the frame.RelaySession/EventSourceServersendsLIMITSon connect and re-sends after AUTH when effective rights change.- Then geode ↔ our own client becomes interop-testable (relayBench / the amy serve path).
3. NIP-11 ↔ LIMITS bridge
The two overlap ~80% (RelayInformationLimitation vs LimitsMessage). Add
LimitsMessage.toNip11Limitation() / RelayInformationLimitation .toLimitsMessage(), and let RelayLimitsTracker seed from the NIP-11 doc
before the socket connects, then override with the live frame — one "limits of
relay X" model regardless of source.
4. Tooling, tests, docs
amy relay infoalready prints the NIP-11 doc; add the liveLIMITSframe (the interop tool the repo already leans on).- Catalog
RelayLimitsTrackerinnip01Core/relay/client/accessories/README.md. - Test the AUTH → re-LIMITS transition (
auth_for_writeflipping after a successful AUTH) — the one dynamic behavior not yet exercised live.
5. Spec follow-up
auth_for_read / auth_for_write are not in #1434. Worth a note on the PR;
we tolerate them as extras either way (they pair with the AUTH this relay
also sends).
Suggested sequencing
audit fixes → (1) client apply helpers → (2) server emit → (3) NIP-11 bridge → (4) tooling.