fix: bunker pair-cap bypass + in-sheet cap gate (sprint 5a) - #41
Merged
Conversation
Closes BACKLOG sprint 5a cap-bypass: bunker first-connect handler created ClientPermissions but deferred ConnectedClient row creation until the first signing request arrived (via SharedStorage.updateClient). Multiple bunker pairs landed before any of them counted toward the 5-cap, allowing 7+ pairs in field testing on build 63. Now calls SharedStorage.setClientRelayUrls immediately after saveClientPermissions, mirroring the nostrconnect path's row-creation timing. relayUrls = [SharedConstants.relayURL] also closes the L1 relay-set incompleteness noted in BACKLOG 5a (bunker pairs now contribute to L1's foreground subscription set). Adds LightSignerBunkerConnectTests covering: row creation with bunker URI relay, cap-check at 5 existing rows, repeat-pair idempotency, per- signer scoping.
HomeView.handlePairNewConnectionTap blocks ConnectSheet from opening at cap, but a user already inside the sheet can rotate New Secret past cap during a single session — the HomeView @State clients array is captured at sheet-open and doesn't refresh until dismiss, so subsequent pair attempts land downstream and NSE-reject with no Clave-side warning. Mirror the entry-point alert at the in-sheet action: intercept New Secret tap, read SharedStorage.getConnectedClients(for:) live (via bunkerSecretsTick observation), show "Connection limit reached" alert when count >= maxClientsPerAccount; do not rotate. Copy URI stays enabled so existing paired clients can re-share for re-pair recovery (browser cache cleared, etc.).
DocNR
marked this pull request as ready for review
May 9, 2026 01:32
3 tasks
DocNR added a commit
that referenced
this pull request
May 9, 2026
Internal-only build bundling three merges since build 69 was tagged: - Sprint 5a (#41): bunker pair-cap bypass fix - Petname removal (#42): -108 LOC dead code - Stage 4a (#43): AccountManager extension, -282 AppState LOC Bumps CURRENT_PROJECT_VERSION 69 -> 70 across 4 targets x 2 configs. Cumulative AppState refactor reduction: 2,338 -> 611 LOC (-73.9%). Stage 4b (PendingApprovalCoordinator) is the final refactor PR. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
DocNR added a commit
that referenced
this pull request
May 9, 2026
…tFlight (#46) Internal-only build for verification of Stage 4b (#45) — the final extraction in the AppState god-object refactor sprint. Bumps CURRENT_PROJECT_VERSION 70 -> 71 across all 4 targets in both Debug and Release configs. Build 71 cumulative content (since last external build 62): - Sprint 5a: bunker pair-cap bypass fix + in-sheet cap gate (#41) - AppState refactor sprint, Stages 1+2+3a+3b+3c+petname+4a+4b (8 PRs total, AppState.swift 2,338 -> 348 LOC, -85.1%) - Same-device nostrconnect overlay copy + QR rescan dedup (#36) Verification gates for build 71 (final TF cycle of the sprint): - xcodebuild test passes (231/231; identical pre/post Stage 4b) ✓ - On-device exercise of alert-chain UI binding (most behavior-relevant Stage 4 verification): - Pair client + sign multiple events back-to-back -> "X of N" advances - Lock-screen approve/deny from notification (static handler check) - "Not now" dismiss-all -> chain closes, badge stays accurate - 5+ min wait with pending request -> TTL purge fires + "expired" activity entry - Background -> foreground -> bell badge updates correctly After this verifies clean, the AppState refactor sprint is officially complete. AppState.swift is a slim @observable container delegating all functional concerns to 6 extension files (5 in Clave/, 1 in Shared/). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the bunker pair-cap bypass confirmed in build-63 device testing — user reached 7 successful bunker pairs on a single signer when the documented per-signer cap is 5. Sprint 5a from
~/hq/clave/BACKLOG.mdConnectedClient identity sprint.Shared/LightSigner.swift:209now callsSharedStorage.setClientRelayUrls(...)immediately aftersaveClientPermissions(perms)in the bunker first-connect handler. Pre-fix, theConnectedClientrow was created lazily bySharedStorage.updateClientonly when the first signing request arrived — multiple bunker pairs could land between connect and first-sign without any of them counting toward the cap. Mirrors the nostrconnect path's row-creation timing. Side benefit: closes the L1 relay-set incompleteness noted in BACKLOG sprint 5a (bunker pairs now contribute to L1's foreground subscription set).ConnectBunkerTabViewnow intercepts the "New Secret" tap when count >= 5, showsConnection limit reachedalert, does not rotate. HomeView's existing entry-point gate (handlePairNewConnectionTap) blocks the sheet from opening at cap; this in-sheet gate covers the case where the user enters under cap and rotates past it during a single session. Copy URI stays enabled so paired clients can re-share for re-pair recovery.Pre-fix evidence
clients.jsonsnapshot (~/hq/clave/troubleshooting/2026-05-08-clients-json-pre.json): 26(signer, client)pairs across 15 distinct signers; highest per-signer count is 4. The bypass-test signer is absent from the dump — confirms bunker pairs leave no proxy-side trace (only nostrconnect goes through/pair-client). iOS is the sole enforcement layer for bunker.LightSigner.swift:175-209readsgetConnectedClients(for:).countfor the cap-check, but the post-connect side-effect at line 209 savesClientPermissionsonly — neverConnectedClient.SharedStorage.updateClientlazily creates the row at first signing request; for clients that haven't signed yet, the count stays stale.Test plan
"Pairing limit reached. Unpair an existing client in Clave settings."errorBunker connect rejected: pairing cap reached. Save to~/hq/clave/troubleshooting/2026-05-08-bunker-cap-bypass-post.log.xcodebuild testpasses (already green locally; confirm in CI if any)Tests added
ClaveTests/LightSignerBunkerConnectTests.swift— 4 tests covering:ConnectedClientrow withrelayUrls.contains(SharedConstants.relayURL)(signer, client)doesn't double-countUnblocks
Premium-tier entitlement plan — caps now count correctly across both flows, so tier-aware multipliers (free 5 → premium 30 clients/signer) inherit clean counters.
Out of scope (defer)
extractConnectNamepopulate) — cheap, additive; spawn separately if a signer is observed passing name in connect paramsPost-fix evidence
Captured 2026-05-09 01:27–01:28 UTC on build 69, real device. Full log:
2026-05-08-bunker-cap-bypass-post.log(saved to~/hq/clave/troubleshooting/).Smoking gun: 5a-core row creation at first-connect
Pre-fix: only the
saveClientPermissionsline would appear; thesetClientRelayUrls (new client)row creation was deferred to first signing request. Now both happen atomically at handshake — the countgetConnectedClients(for:)returns is authoritative at the time of the next bunker connect's cap-check.Unpair → re-pair works (regression check)
L1's
refreshRelaySetfires correctly after unpair, confirming the relay-set computation observes ConnectedClient mutations.Existing-client signing path unaffected (regression check)
get_public_keyafter the new pair signs and publishes successfully — the!isExistingClientbranch where the newsetClientRelayUrlscall lives doesn't interfere with the existing-client path.Cap rejection (defense-in-depth, NSE-side)
The 6th-attempt log line (
Bunker connect rejected: pairing cap reached (5)) wasn't captured in this window, but the device-test confirmed the behavior: the 6th client received the"Pairing limit reached. Unpair an existing client in Clave settings."error in its UI on attempt. The rejection path is also covered by the unit testtestBunkerConnect_capCheckBlocksAtFiveExistingRows.