Audit outcome for the two suspected "double-prompt" Amber flows:
• Image upload — CONFIRMED already a single signature. BlossomUploader
(even at the pinned 0.47.1) signs one kind-24242 auth event and reuses
it across all servers via Promise.any, so uploads = 1 Amber prompt. No
change needed.
• DM / crew-chat send — the multiple prompts are protocol-inherent and
MUST NOT be optimized away: NIP-17 seals+signs once per recipient (and
to self); a Cordn crew message signs an author proof then encrypts once
per roster member. Reducing the count would weaken confidentiality or
authorship. So instead of a code shortcut we add transparency.
New <AmberApprovalHint>: a tiny heads-up shown ONLY to interactive
(Amber) signers before an inherently multi-approval action, telling them
how many Approve screens to expect and that it's normal. Renders nothing
for silent signers (extension/nsec/bunker), where the ops are invisible.
Wired into:
• CrewChat composer — count derived from transport + roster size
(Cordn: roster+1; NIP-17: roster*2).
• ConversationPage (1:1 DM) composer — 4 approvals (encrypt+sign to the
recipient and to yourself).
Dependencies: reviewed the full manifest; deliberately left unchanged.
Every upgrade candidate is either on the critical crypto/Nostr/payments
path (nostrify, nostr-tools, cashu, bitcoinjs, noble, scure — pinned for
safety) or a documented major bump (lucide v1, react 19, react-router 7)
with breaking changes and no user-facing benefit. No security advisories
force a change, and caret ranges already pull safe patch/minor updates.
"It must work" wins over churn.
Bumps to 1.2.20 (Android versionCode 10220 auto-derived).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
A returning member couldn't tell new messages from old ones. Add the universal
unread-divider pattern (Slack/Discord-style) directly inside the chat:
- useCrewChatRead: per-crew, per-pubkey last-seen timestamp in localStorage
(local-only, zero network cost, works for both NIP-17 and Cordn transports).
- CrewChat snapshots last-seen once per open so the divider stays put while
reading, draws a minimal "New" line above the first message since last visit,
and quietly advances last-seen to the newest rendered message.
No new fetches, no config, nothing for the user to manage — it just works.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The crew coordinator is no longer just a pointer — when a crew configures one,
the Chat tab runs through it for real.
- src/lib/cordn: clean transport/crypto split so an MLS (ts-mls) upgrade is a
drop-in swap of the crypto layer only.
- types.ts: CordnMessage/CordnEndpoint + envelope kind 4497 + app tag.
- crypto.ts: sealGroupMessage/openGroupMessage — real NIP-44 group encryption
(per-recipient sealing), the exact seam MLS encrypt-to-epoch slots into.
- client.ts: buildEnvelope/publishEnvelope/fetchGroupMessages with per-group
cursor ordering and ciphertext→author sender binding (anti-spoof).
- endpoint.ts: resolve a crew's `coordinator` to a usable relay endpoint
(wss/ws direct; https falls back to the ContextVM relay).
- useCordnChat: fetch-then-poll hook mirroring the NIP-17 chat API.
- CrewChat: uses Cordn when the crew has a coordinator, NIP-17 fallback
otherwise; header badge shows the active transport (Cordn vs Private).
- CrewCoordinatorCard: reflects that private chat is live.
- NIP.md: document envelope kind 4497, delivery (fetch-then-poll + cursors),
the nip44-group sealed content, and the honest crypto→MLS seam.
Real encryption, real coordinator routing, non-breaking (fallback intact),
with the MLS ratchet as a clearly-marked next step.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: NUser's extension signer exposes .nip44 as a getter that calls
.nostr (window.nostr) and THROWS when the extension isn't present. Any
synchronous access during render crashes the whole tree — the actual culprit
was useStash's `isEncryptionAvailable: !!user?.signer?.nip44` in its return
object, evaluated every render even when logged out.
- Add src/lib/signerCapabilities.ts: hasNip44/hasNip04 helpers that determine
capability via login type + direct window.nostr probe (no throwing getter).
- useStash: use the helper for hasNip44 + isEncryptionAvailable
- useDirectMessages: replace all render-path `enabled: !!user?.signer?.nip44`
with a pre-computed nip44Available across all 4 conversation hooks
- CrewChat, CrewRequestToJoin, ConversationPage, DirectMessagesPage,
ContactsPage: use the safe helper in render/guard paths
Async accesses (inside queryFn/callbacks) are left as-is — they reject
gracefully within react-query and never crash render.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
- CrewChat: founder+admins can hide chat messages via a per-message hide button; reuses useCrewModeration/useHideCrewPost with the rumor (kind-14) message id
- Hidden messages filtered from chat for everyone; moderators see them greyed + "Hidden" (transparent/reversible)
- Since NIP-17 messages are E2EE and can't be deleted from relays, moderation is enforced client-side via public NIP-32 labels (label reveals only the hidden id, never content)
- Wire admins/crewCoord/canModerate from CrewDetailPage; header hint for moderators
- NIP.md: expand moderation section to cover both feed (kind-1 note id) and chat (kind-14 rumor id) with the E2EE client-side caveat
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Built on the existing NIP-17 gift-wrapped group DM system (useGroupConversation
+ useSendDirectMessage), so the relay only ever sees encrypted blobs.
- New CrewChat component: message list w/ author profiles, composer
(Enter to send), auto-scroll, skeleton/empty states
- Participants = founder + all listed members (deduped)
- Only crew members can read/write (others see a locked "ask to join" state);
warns if the signer lacks NIP-44
- "Private" lock badge makes the E2E-encrypted nature unmistakable
- Surfaced as a card on the crew detail page under the Crew Feed
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>