NIP-07 browser extension for Nostr identity management and event signing. Manages multiple identities in an Argon2id-encrypted vault. Exposes window.nostr to web applications for signing without revealing private keys.
getPublicKey(), signEvent(), getRelays()nip04.encrypt(), nip04.decrypt() (legacy DMs)nip44.encrypt(), nip44.decrypt() (modern DMs, Marmot MLS)Bun (or npm/node 20+).
cd next/signer
bun install
bun run build:firefox # dist/firefox/ (unpacked extension)
bun run xpi # dist/smesh_signer-*.zip
No bun/node required on host:
cd next/signer
docker build -t smesh-signer-build .
mkdir -p out
docker run --rm -v $(pwd)/out:/out smesh-signer-build
Output lands in out/firefox/ (unpacked) and out/smesh_signer-*.zip (packaged).
From XPI (permanent):
about:addons > gear icon > "Install Add-on From File..."smesh_signer-*.zipFrom source (temporary, for development):
about:debugging > "This Firefox" > "Load Temporary Add-on..."dist/firefox/chrome://extensions > enable "Developer mode"dist/firefox/ directoryThree-layer message bridge between web pages and the encrypted vault:
Web page Extension
-------- ---------
window.nostr.signEvent()
|
v
[Injected Script] smesh-signer-extension.js
| window.postMessage() (runs in page context)
v
[Content Script] smesh-signer-content-script.js
| browser.runtime.sendMessage()
v
[Background SW] background.js
| vault decrypt + BIP340 sign
v
(response bubbles back through same chain)
window.nostr.signEvent(event){type: "smesh-signer-request", method, params}, posts to windowbrowser.runtime.sendMessage()Per-identity, per-host, per-method granularity:
allow / deny policies stored in vaultsignEvent (e.g. allow kind 1 but prompt for kind 30023)password
|
v Argon2id (256MB mem, 4 threads, 8 iterations, ~3s)
|
v 32-byte derived key
|
v AES-256-GCM (random 12-byte IV per entry)
|
v encrypted vault blob --> browser.storage.local
Key cached in browser.storage.session while vault is unlocked. Cleared on lock or browser close.
When smesh detects the signer extension, it delegates all crypto to the extension instead of requiring an nsec. The Marmot SW sends crypto requests through the bus:
Marmot SW --> Bus WS --> Shell SW --> Page --> window.nostr.nip44Encrypt()
|
Signer Extension
|
(vault decrypt + ECDH + encrypt)
This is the ProxyCrypto path in pkg/nostr/protocol/marmot/crypto.go. The alternative LocalCrypto path holds the key directly (bridge/bridgebot mode).
Watch mode for iterative development:
bun run watch:firefox
Rebuilds on source change. Reload extension in about:debugging after each rebuild.
Debug logging: uncomment console.log inside debug() at src/smesh-signer-extension.ts:248.
next/signer/
src/
background.ts Background SW (vault, crypto, dispatch)
smesh-signer-extension.ts Injected into page (window.nostr API)
smesh-signer-content-script.ts Content script (message bridge)
prompt.ts Permission prompt popup
options.ts Extension options page
unlock.ts Vault unlock page
app/ Angular popup UI (identity management)
public/
manifest.json MV3 manifest (Firefox + Chrome compatible)
*.html Popup/options/prompt/unlock pages
common/ Shared Angular library (@common)
custom-webpack.config.ts Extra webpack entries (background, content script, etc.)