Proposal 5: Port the best of upstream nos2x
Context
Upstream fiatjaf/nos2x (v2.x, Chromium-only, MV3, plain JS) has shipped several features we lack. We reviewed it and decided to stay on our codebase (TypeScript, multi-profile, PIN encryption at rest, time-boxed grants) — upstream's own README points Firefox users here, and its MV3 state handling is naive (see proposal 3). This proposal cherry-picks their proven ideas, in priority order.
Ports
5a. Kind-scoped accept/reject conditions
The biggest one — the concrete implementation of proposal 2c.
- Data model: adopt upstream's
policies[host][accept][type] = { conditions: { kinds: { 7: true } }, created_at }. Policies can be accept or reject; a reverse policy with identical conditions cancels the existing one. Conditions merge on repeat grants. - Merge with our time-boxed grants: conditions and expiry are
orthogonal — a policy becomes
{ conditions, expires_at? }, so "allow kind 7 for this site for 1 hour" is expressible. - Prompt UI: "authorize kind N forever" / "reject kind N forever" buttons alongside the existing duration choices.
- Migration: existing level-based
activePermissionsentries map onto accept-true policies with empty conditions on first run.
5b. peekPublicKey
window.nostr.peekPublicKey(): returns the pubkey if getPublicKey was
previously authorized for this host, '' otherwise — never prompts. Lets
clients probe on page load without spawning popup storms. Cheap: provider
method + a permission check in the background.
5c. Notification audit log
optional_permissions: ["notifications"] + a toggle in Options; fire a
basic browser notification on every auto-allowed/denied action (kind,
content summary, host). A cheap answer to proposal 2's audit question —
policy grants become visible instead of silent.
5d. Result-in-prompt
Perform the crypto operation before asking, and show the decryption result in the prompt so users see the actual plaintext they're approving. Caveat: crypto happens regardless of consent; only disclosure is gated. Limit to decrypt operations and land alongside proposal 2a (human-readable prompts), not before.
5e. Centered prompt windows
Position prompt popups centered on the last focused window (upstream's
getPosition). Trivial UX win; verify behavior on Safari.
5f. ncryptsec (NIP-49) import
Accept ncryptsec1… paste in the key input (decrypt-on-import via
nostr-tools' nip49, no new dependencies). QR-code scanning is deferred
— it would add qrcode-parser/react-qr-code, against our
no-new-dependencies policy.
5g. Open the options page on first install
browser.runtime.onInstalled with reason === 'install' →
browser.runtime.openOptionsPage(). Onboarding win, one line.
5h. (Deferred) nip60.signSecret
Lives only on upstream's unmerged nip60 branch:
window.nostr.nip60.signSecret(secret) — signs Cashu NUT-10 proof secrets
(NIP-60 wallets) with the nsec (~40 lines, @noble/* comes transitively via
nostr-tools). Small and self-contained, but niche and unmerged upstream —
port only if Cashu wallet clients adopt the convention.
Conscious divergences (keep ours)
getRelays+ relay management UI — upstream removed it; NIP-07 marks it optional but clients use it. Keep.- Prompt queue over single mutex — our persisted
PromptManagerqueue survives MV3 worker restarts (proposal 3c); upstream's in-memory mutex doesn't. - Multi-profile, PIN → passkey path, time-boxed grants, TypeScript — the fork's identity; not negotiable.
Interactions with other proposals
- Proposal 2: 5a is 2c's data model; 5d belongs with 2a; 5c answers 2's audit open question.
- Proposal 3: 5c's notification permission goes into the per-target manifest template; 5e needs Safari verification.