Skip to content

fix(signer): switch_relays returns null (NIP-46 responder-only)#7

Merged
DocNR merged 6 commits into
mainfrom
feat/switch-relays-null
Apr 20, 2026
Merged

fix(signer): switch_relays returns null (NIP-46 responder-only)#7
DocNR merged 6 commits into
mainfrom
feat/switch-relays-null

Conversation

@DocNR
Copy link
Copy Markdown
Owner

@DocNR DocNR commented Apr 20, 2026

Summary

Bundles two independent fixes for combined build-20 device testing:

  1. switch_relays returns nullLightSigner.processRequest now returns ("null", nil) instead of ("[\"\(SharedConstants.relayURL)\"]", nil). Matches Amber's responder-only pattern and NDK default. Unblocks Coracle pairing (welshman stalls on non-null responses). Also includes a refactor (processRequest internal) + new unit tests locking the wire format.
  2. Universal Clipboard for bunker URI copy — cherry-picked from clave#8 (commits 80c4d4e + ecd9f89). Drops .localOnly: true from bunker URI copy; keeps 120s iPhone-side expiration. Nsec export stays locked. Enables iPhone → Mac paste for cross-device pairing.

Pbxproj bumped to build 20.

Why they ship together

Testing the switch_relays change required verifying bunker flow on a Mac browser client (Coracle/noStrudel). The existing .localOnly: true blocked that cross-device paste, preventing the regression test. Cherry-picking the UC fix onto this branch let us verify both on one archive.

Tradeoff noted: fevela signing regression (temporary)

switch_relays → null removes the accidental pool migration that nostr-tools-family clients (fevela, Primal, Snort) relied on to route sign_event through relay.powr.build. With null, those clients keep publishing sign_event to their own URI relays, which the proxy doesn't subscribe to. fevela nostrconnect signing will fail on this build.

Resolution path: do not promote this to external TestFlight until proxy-per-client-relay V2 ships. V2 subscribes the proxy to each paired client's URI relays, catches sign_event regardless of which relay it lands on, and restores fevela signing while keeping Coracle pairing working.

Build 18 stays on external until V2 is ready. Build 20 is internal-only verification.

Test plan (build 20)

  • Unit: LightSignerProcessRequestTests (2 tests) pass; full suite 30/30 green
  • Coracle nostrconnect: pairing advances past the stall screen (previously stalled on welshman migration)
  • Nostur bunker: signs end-to-end (no regression)
  • fevela nostrconnect: pairing works; signing fails with null switch_relays (expected, resolved by V2)
  • UC bunker copy iPhone → Mac → URI pastes
  • Coracle / noStrudel bunker from Mac paste: login completes
  • Nsec export: Mac clipboard stays empty
  • iPhone-side 120s expiration fires

Expiration caveat (UC)

The 120s .expirationDate only applies to iPhone pasteboard. Apple's Universal Clipboard has no cross-device expiration mechanism. Mac-side clipboard stays populated until next copy. Mitigated by SharedStorage.rotateBunkerSecret() on successful pair. Documented in audit note A10.2.

Merge note

Two merge paths:

🤖 Generated with Claude Code

@DocNR DocNR changed the title fix(signer): switch_relays returns null (NIP-46 responder-only) fix(signer+ui): switch_relays returns null + Universal Clipboard for bunker URI Apr 20, 2026
DocNR added 6 commits April 19, 2026 23:28
Matches Amber and NDK default. Previously returned
["wss://relay.powr.build"], which triggered welshman pool migration
in Coracle's NIP-46 client and stalled the pairing UI indefinitely.
Bunker flow unaffected (switch_relays is only emitted during nostrconnect
handshakes).

Validated end-to-end on TestFlight builds 16+17 during the 2026-04-19
proxy-per-client-relay diagnostic. Unit test locks the wire format.
The 120s .expirationDate only fires on iPhone; Universal Clipboard does
not propagate expiration metadata to Mac-side NSPasteboard. Residual
exposure on the Mac is bounded by SharedStorage.rotateBunkerSecret()
firing on successful pair, which makes stale clipboard copies dead.

No behavior change — comment-only clarification after build 20
device-testing revealed the earlier comment's promise of 'auto-clear
after 2 min' was ambiguous about which device that applies to.
@DocNR DocNR force-pushed the feat/switch-relays-null branch from fa648f2 to 4c36850 Compare April 20, 2026 03:28
@DocNR DocNR changed the title fix(signer+ui): switch_relays returns null + Universal Clipboard for bunker URI fix(signer): switch_relays returns null (NIP-46 responder-only) Apr 20, 2026
@DocNR DocNR marked this pull request as ready for review April 20, 2026 03:29
@DocNR DocNR merged commit 096d486 into main Apr 20, 2026
@DocNR DocNR deleted the feat/switch-relays-null branch April 20, 2026 03:29
DocNR added a commit that referenced this pull request Apr 21, 2026
* proxy: embed caught event in APNs push payload (≤3500B)

Avoids the NSE ephemeral-fetch race when the relay drops kind:24133
between proxy dispatch and NSE wake. Oversized events fall through
to the existing fetch path (no regression).

Part of build 22 response-delivery hotfix. See
~/hq/clave/specs/2026-04-20-response-delivery-fixes-design.md

* ios: prefer embedded event from push payload over relay fetch

NSE and AppDelegate foreground-push handler now check userInfo["event"]
first; fall through to LightRelay.fetchEvents only if absent. Closes
the ephemeral-fetch race for ≤3500B events. Oversized events fall
through as today.

Part of build 22 response-delivery hotfix.

* ios: distinguish cast-miss from key-missing in embed fallback log

Spec Risk #1 mitigation: when userInfo["event"] is present but not a
dict, log a warning so proxy payload-shape regressions are visible
in production triage. Unchanged: behavior on happy path + missing-key
path.

* ios: thread relay URL through PendingRequest approve-later flow

PendingRequest gains relayUrl: String? captured at queue-time from the
incoming responseRelayUrl. approvePendingRequest threads it back into
LightSigner.handleRequest, so signed responses for protected kinds
publish to the relay the client is actually subscribed on — not the
powr.build fallback.

Bug was latent since PR #7 (switch_relays → null) — nostr-tools clients
stopped migrating to powr.build, so the fallback stopped matching.
Bunker flow unaffected because Clave's bunker URIs are already pinned
to powr.build.

Codable's default synthesis handles missing keys on Optional fields,
so pre-build-22 pending rows decode as nil → fall back to powr.build
(unchanged broken behavior for pre-upgrade stragglers).

Part of build 22 response-delivery hotfix.

* ios: rename PendingRequest.relayUrl to responseRelayUrl

Matches the parameter name on LightSigner.handleRequest so the
call site reads `responseRelayUrl: request.responseRelayUrl` instead
of `responseRelayUrl: request.relayUrl` — self-documenting round
trip. Also corrects "forward" → "backward" compatibility in the
field's doc comment (new code reading pre-upgrade rows is backward
compat, not forward).
DocNR added a commit that referenced this pull request May 2, 2026
Brainstorm review of design-system.md against shipped code surfaced 9
inconsistencies + 1 anti-pattern still present. Fixed everything in one
batch so the next TestFlight archive carries it all.

Code:
- HomeView: drop .padding(.bottom, 8) on SlimIdentityBar invocation —
  slim banner owns its outer bottom padding (12pt); stacking another 8pt
  on top was double-counting (review #4)
- HomeView: drop .padding(.bottom, 8) inside statsRow — listSectionSpacing(0)
  carries the gap to Connected Clients; the residual padding kept the
  visible gap excessive after polish round 2 (review #2)
- AccountDetailView: avatarLarge letter fallback opacity 0.25 → 0.22 to
  align with SlimIdentityBar's 0.22 (review #3)
- ConnectSheet: add .presentationBackground(Color(.systemGroupedBackground))
  — was the last sheet still defaulting to translucent (review #9)
- ApprovalSheet: rename @State capExceeded → showConnectionCapAlert for
  naming convention parity with HomeView (review #7)

design-system.md:
- New "Cross-platform applicability" section at the top — clarifies what
  carries directly to clave.casa web companion (color tokens, displayLabel
  rule, identity-vs-functional zone philosophy, avatar treatments, copy
  patterns, anti-patterns) vs what's iOS-only (SwiftUI modifiers, haptics,
  sheet/toolbar conventions)
- §3 Typography: corrected initial-letter font scale — AvatarView uses
  size*0.35 mono (pubkey) or size*0.4 proportional (name); was wrongly
  documented as a single 0.37 (review #1)
- §4 Avatars: added Treatment Selection Rule table (B on neutral bg,
  C on saturated theme gradient) + clarified 1-vs-2 letter behavior
  (review #5, #6)
- §4 Sizing scale: expanded table to include initial font + border
  thickness per slot, with the ~5% border scaling rule (review #8)
- §5 Spacing: explicit "single source of truth" note on slim banner
  bottom padding; new "Stats row" subsection capturing the
  ultraThinMaterial-on-small-cards-OK rule (review gap #10, #4)
- §6 HomeView gradient: documented palette[0] defensive fallback when
  currentAccount is nil (review #13)
- §7 Patterns: new "State variable naming" subsection with the
  showCapAlert / showAccountCapAlert / showConnectionCapAlert
  convention (review #7 doc side)
- §11 Anti-patterns: audit-point note that ConnectSheet was the last
  surface to acquire .presentationBackground (review #9 doc side)

Build green on iOS Simulator 26.4. pbxproj still 41 — assumes user hasn't
yet archived 41; bump to 42 if needed before re-archive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant