Skip to content

feat(proxy): add signer_pubkey to APNs payload (Stage A)#22

Merged
DocNR merged 2 commits into
mainfrom
feat/multi-account-proxy
May 4, 2026
Merged

feat(proxy): add signer_pubkey to APNs payload (Stage A)#22
DocNR merged 2 commits into
mainfrom
feat/multi-account-proxy

Conversation

@DocNR
Copy link
Copy Markdown
Owner

@DocNR DocNR commented May 1, 2026

Summary

Stage A of the multi-account sprint. Adds a single field to the APNs push payload so iOS NSE can route to the correct account when one device has multiple Nostr identities.

  • One-line addition: to the pushPayload object literal at line ~498.
  • Embed gate adjusted from 3500 → 3415 to preserve the original ~300B aps-container headroom under the 4KB APNs payload cap (the new field adds ~85B with quoting/comma).
  • Forward-compatible: build 31 and earlier iOS clients ignore the new field.

Test plan

  • Syntax check (node --check) passes
  • All 5 existing test suites still pass (apnsClient, clients, nip98, relayPool, storage) — fail 0
  • Deployed to /opt/clave-proxy-test/ (test instance on port 3047, proxy-test.clave.casa)
  • Test proxy /health returns ok=true post-deploy
  • Production proxy proxy.clave.casa unaffected (verified sendOk=18072, sessionAlive=true post-deploy)
  • End-to-end iOS test: Stage B build (feat/multi-account, commit 4b633d5) consumes the field via SharedKeychain.resolveSignerPubkey → routes NSE wakes per-account
  • Production deploy gated on Stage B internal-TestFlight verification

iOS counterpart

  • iOS branch: feat/multi-account (commit 4b633d5)
  • iOS reads this field via SharedKeychain.resolveSignerPubkey(userInfo:) (added in Task 6)

Plan + audit

  • Sprint plan: ~/hq/clave/plans/2026-04-30-multi-account-sprint.md
  • Architecture: ~/.claude/plans/doesnt-each-account-have-dreamy-journal.md
  • Security audit C8 (embed gate adjustment): ~/hq/clave/security-audits/2026-04-30-multi-account-pre-implementation.md

🤖 Generated with Claude Code

NSE multi-account routing requires the proxy to identify which signer
each push is for, since one APNs token may be registered for multiple
signer pubkeys (one per account on the device). The legacy payload
only had relay_url + event_id + (optionally) the embedded event,
forcing NSE to either iterate every Keychain entry attempting decrypt
or fall back to a single-signer assumption.

Adds signer_pubkey: targetPubkey to the pushPayload object literal at
the dispatch site. targetPubkey is already in scope — it's the p-tag
recipient used to filter matching tokens. iOS-side, NSE reads this
field via SharedKeychain.resolveSignerPubkey(userInfo:) (added in iOS
Task 6) and loads the matching nsec via loadNsec(for: pubkey).

Adjusts the embed gate from 3500 → 3415 to preserve the original ~300B
aps-container headroom under the 4KB APNs payload cap. The new field
adds ~85B (key name + 64-char hex value + JSON quoting/comma);
shrinking the gate by the same amount keeps the safety margin intact.
Oversized events still fall through to NSE's relay-fetch path (no
regression).

Forward-compatible: build 31 and earlier iOS clients ignore the new
field (dict deserialization tolerates extra keys). The Stage B iOS
build that consumes this field is already on feat/multi-account
(commit 29ef9db).

Deployed to /opt/clave-proxy-test (test proxy on port 3047,
proxy-test.clave.casa). Production proxy unchanged — Stage A ships
to production after Stage B iOS is verified end-to-end via the test
proxy + TestFlight cohort.

Verified post-deploy: clave-proxy-test.service active (running),
/health endpoint returns ok=true with clean APNs counters, production
proxy.clave.casa unchanged (sendOk=18072, sessionAlive=true).

Plan: ~/hq/clave/plans/2026-04-30-multi-account-sprint.md
iOS counterpart: feat/multi-account commit 29ef9db (Task 6)
Security audit C8: ~/hq/clave/security-audits/2026-04-30-multi-account-pre-implementation.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 1, 2026
For internal-TestFlight build of feat/multi-account. Two changes:

1. pbxproj CURRENT_PROJECT_VERSION 31 → 32 across all 8 build configs
   (Clave + ClaveNSE × Debug/Release/Profile/Test).

2. SharedConstants.defaultProxyURL flipped from proxy.clave.casa →
   proxy-test.clave.casa. The test proxy on Dell already has Stage A
   (`signer_pubkey` payload field) deployed; production proxy does
   NOT yet (gated on Stage A PR #22 merging to main and being
   deployed).

⚠️ This commit is TEMP. The defaultProxyURL flip MUST be reverted
before merging feat/multi-account to main — at that point Stage A
should already be live on production proxy, and external TestFlight
builds need to point at the production URL.

Suggested last-commit-on-branch:
    revert: flip defaultProxyURL back to production
    Reverts the test-proxy redirect. Stage A is now live on the
    production proxy; merging to main means external builds.

Build: 32 (internal TestFlight only — do not promote to external
without first reverting the proxy URL flip + verifying production
proxy has Stage A deployed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously hardcoded `https://proxy.clave.casa/...` in 4 expectedUrl
constants (/register, /unregister, /pair-client, /unpair-client). This
worked for prod but silently broke the test proxy at proxy-test.clave.casa
— every NIP-98-authed request 401'd with "URL mismatch" because iOS
correctly signs the URL it's calling but the proxy was validating against
the prod hostname.

Discovered during build 33 multi-account smoke test: zero accounts ever
registered on the test proxy this whole sprint, masked by L1's
foreground-only path which made current-account signing appear to work.

Fix: introduce PUBLIC_PROXY_URL env var, default to the prod hostname so
production deploys are unaffected. Test proxy .env adds
PUBLIC_PROXY_URL=https://proxy-test.clave.casa.

Backward-compatible. Build 31 external users (currently on prod) see no
behavior change — default value matches the previous hardcoded constant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 1, 2026
Build 33 (commit 788c495) was archived for internal-TF and burned the
TestFlight build number. Bumping to 34 so we can re-archive with the
HomeView scoping fix (0e2a0de) + registerAllAccounts refactor (14c7489).

Internal-TF only — defaultProxyURL still points at proxy-test.clave.casa
(commit 11ad90f). URL revert + another bump will be required before any
external promotion, after PR #22 ships to production proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 2, 2026
Build 38 carries Stage C UX (Tasks 1-7):
- AccountTheme palette helper (12 hash-derived gradient entries; 6
  unit tests) — Task 1
- AccountStripView + SlimIdentityBar replacing the build-37 interim
  Menu, full-bleed gradient HomeView background — Task 2
- AddAccountSheet (generate / paste-nsec modal, current-account
  autoswitch on success) — Task 3
- AccountDetailView (gradient banner + petname rename + delete; Profile
  + Actions sections including Refresh / Rotate / Export-current-only)
  — Tasks 4-5
- AppState fetchProfile(for:) refactor enabling per-account refresh —
  Task 5
- SettingsView Accounts section + Add Account row replacing the old
  single-account 'Signer Key' section (orphaned state cleanup) —
  Task 6
- ApprovalSheet 'Connect as @account' header + ClientDetailView unpair
  alert with named account + PendingApprovalsView per-request signer
  label — Task 7

Internal-TF only. URL flip still active to test proxy. Prod rollout
sequence (PR #22 to prod proxy + URL revert + final bump) gated on
2026-05-02+ user comm to existing build-31 testers per the hold from
the spec brainstorm.

Build green; full test suite (172 pre-sprint + 6 new AccountThemeTests)
passes on iPhone 17 / iOS 26.4 simulator.
DocNR added a commit that referenced this pull request May 3, 2026
Internal-only build pointing at proxy-test.clave.casa (URL revert
to prod proxy + MARKETING_VERSION 0.1.0→0.2.0 deferred to a
separate commit before the external archive). Includes the
8-task AccountDetailView redesign sprint + Phase B Universal Links
wiring (applinks:clave.casa entitlement + DeeplinkRouter https
case + 6 unit tests).

Test proxy already has Stage A signer_pubkey routing (PR #22 deployed
there only); prod proxy still pending PR #22 merge. Internal smoke
on test proxy validates the full feat/multi-account stack
independent of the prod-proxy switch.

AASA cache caveat from clave.casa coordination: Apple CDN may still
serve the old single-component AASA for ~few hours. Use trailing-slash
form `https://clave.casa/connect/?uri=...` for Universal Links
verification today — matches both old and new AASA. The no-trailing-
slash form will work after CDN refresh (within ~6h).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 3, 2026
Internal-only build pointing at proxy-test.clave.casa. URL revert to
prod proxy + MARKETING_VERSION 0.1.0→0.2.0 still deferred — both flip
in the build 47 external rollout commit, after PR #22 ships to prod.

What's new vs build 45:
- CachedProfile.name extraction (48b4fa6)
- petname dropped from UI; Username row added; displayLabel chain
  becomes displayName → name → prefix(8) (1a130bb)
- AccountDetailView banner gets rounded corners + shadow matching
  SlimIdentityBar's chrome — same size, just no longer squared off
  (46989f3)
- Caption "Opens in Safari · pairs with clave.casa if needed" under
  the Edit on clave.casa row (2db6fa9)

Also surfaced + queued: ConnectedClient identity sprint (3 iOS-side
gaps that prevent Clave.Casa name from showing in Connected Clients
list). Documented in BACKLOG High Priority. Not blocking external —
sprint targets v0.2.1 or v0.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 3, 2026
…internal smoke

Internal-only build for smoking the two device-test fixes from build 46:
- ef1e250: AccountStripView always renders (no auto-hide at single-account)
  so the "+" pill stays accessible from Home for adding new accounts
- 98e441c: AccountDetailView swap Form→List + .listStyle(.plain) to match
  Home/Activity (kills the insetGrouped extra horizontal margin)

URL stays at proxy-test.clave.casa, MARKETING_VERSION stays 0.1.0.
External promotion + URL revert + MARKETING_VERSION bump deferred to a
later commit pending Dell→VPS prod proxy migration plan + PR #22 prod
merge timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 3, 2026
Build 47 was archived to ASC; TestFlight burns build numbers
permanently. Build 48 includes the swipe-delete account race fix
(1729ada) that wasn't in build 47.

URL stays proxy-test.clave.casa, MARKETING_VERSION stays 0.1.0
— still internal-only smoke. External rollout deferred pending
PR #22 prod merge / Dell→VPS prod migration timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 3, 2026
Build 48 was archived to ASC for the swipe-delete race fix; TestFlight
burns build numbers permanently. Build 49 includes the audit-5 spec-
compliance fix (635bfac) for NIP-46 error responses — should resolve
clave.casa's 45s timeout on stale-session recovery.

URL stays proxy-test.clave.casa, MARKETING_VERSION stays 0.1.0
— still internal-only smoke. External rollout deferred pending
PR #22 prod merge / Dell→VPS prod migration timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 3, 2026
Build 49 was archived to ASC for the audit-5 NIP-46 result-field fix;
TestFlight burns build numbers permanently. Build 50 includes the
kind:0 cross-relay merge fix (ef3821c) for "edit profile on clave.casa
but Clave iOS keeps showing old fields after pull-to-refresh."

URL stays proxy-test.clave.casa, MARKETING_VERSION stays 0.1.0
— still internal-only smoke. External rollout deferred pending
PR #22 prod merge / Dell→VPS prod migration timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@DocNR DocNR marked this pull request as ready for review May 4, 2026 00:53
@DocNR DocNR merged commit cd07ab8 into main May 4, 2026
@DocNR DocNR deleted the feat/multi-account-proxy branch May 4, 2026 00:53
DocNR added a commit that referenced this pull request May 4, 2026
- Flip defaultProxyURL: proxy-test.clave.casa → proxy.clave.casa now that
  PR #22 (Stage A signer_pubkey payload) is on prod.
- Bump pbxproj 50 → 51.
- Bump MARKETING_VERSION 0.1.0 → 0.2.0 — first external build with
  multi-account + Universal Links.

Build 50 internal-only stays as the pre-flip diagnostic baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocNR added a commit that referenced this pull request May 4, 2026
…inks (#23)

First external build with multi-account support. Highlights:

- Multi-account: up to 4 accounts on one device with per-account ambient gradient identity (Stage B + Stage C).
- AccountDetailView redesign (Phase A, 8 tasks) with profile + connection management and clave.casa profile-editor handoff.
- Universal Links (Phase B): clave.casa Sign In QRs open directly in Clave on iOS, no scheme-squat from other clients.
- audit-5 closure: NIP-46 error responses include `result: ""` per spec — web companions recover from stale sessions in <1s instead of 45s timeout.
- kind:0 cross-relay merge by `created_at` — fixes stale-relay profile field bug surfaced by 5-field Profile layout.
- Proxy URL flipped from proxy-test.clave.casa to proxy.clave.casa (PR #22 Stage A signer_pubkey payload merged + deployed to prod separately).

pbxproj 50→51, MARKETING_VERSION 0.1.0→0.2.0.
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