Claude-skill-registry applesauce-signers
This skill should be used when working with applesauce-signers library for Nostr event signing, including NIP-07 browser extensions, NIP-46 remote signing, and custom signer implementations. Provides comprehensive knowledge of signing patterns and signer abstractions.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/applesauce-signers" ~/.claude/skills/majiayu000-claude-skill-registry-applesauce-signers && rm -rf "$T"
manifest:
skills/data/applesauce-signers/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references API keys
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
applesauce-signers Skill
This skill provides comprehensive knowledge and patterns for working with applesauce-signers, a library that provides signing abstractions for Nostr applications.
When to Use This Skill
Use this skill when:
- Implementing event signing in Nostr applications
- Integrating with NIP-07 browser extensions
- Working with NIP-46 remote signers
- Building custom signer implementations
- Managing signing sessions
- Handling signing requests and permissions
- Implementing multi-signer support
Core Concepts
applesauce-signers Overview
applesauce-signers provides:
- Signer abstraction - Unified interface for different signers
- NIP-07 integration - Browser extension support
- NIP-46 support - Remote signing (Nostr Connect)
- Simple signers - Direct key signing
- Permission handling - Manage signing requests
- Observable patterns - Reactive signing states
Installation
npm install applesauce-signers
Signer Interface
All signers implement a common interface:
interface Signer { // Get public key getPublicKey(): Promise<string>; // Sign event signEvent(event: UnsignedEvent): Promise<SignedEvent>; // Encrypt (NIP-04) nip04Encrypt?(pubkey: string, plaintext: string): Promise<string>; nip04Decrypt?(pubkey: string, ciphertext: string): Promise<string>; // Encrypt (NIP-44) nip44Encrypt?(pubkey: string, plaintext: string): Promise<string>; nip44Decrypt?(pubkey: string, ciphertext: string): Promise<string>; }
Simple Signer
Using Secret Key
import { SimpleSigner } from 'applesauce-signers'; import { generateSecretKey } from 'nostr-tools'; // Create signer with existing key const signer = new SimpleSigner(secretKey); // Or generate new key const newSecretKey = generateSecretKey(); const newSigner = new SimpleSigner(newSecretKey); // Get public key const pubkey = await signer.getPublicKey(); // Sign event const unsignedEvent = { kind: 1, content: 'Hello Nostr!', created_at: Math.floor(Date.now() / 1000), tags: [] }; const signedEvent = await signer.signEvent(unsignedEvent);
NIP-04 Encryption
// Encrypt message const ciphertext = await signer.nip04Encrypt( recipientPubkey, 'Secret message' ); // Decrypt message const plaintext = await signer.nip04Decrypt( senderPubkey, ciphertext );
NIP-44 Encryption
// Encrypt with NIP-44 (preferred) const ciphertext = await signer.nip44Encrypt( recipientPubkey, 'Secret message' ); // Decrypt const plaintext = await signer.nip44Decrypt( senderPubkey, ciphertext );
NIP-07 Signer
Browser Extension Integration
import { Nip07Signer } from 'applesauce-signers'; // Check if extension is available if (window.nostr) { const signer = new Nip07Signer(); // Get public key (may prompt user) const pubkey = await signer.getPublicKey(); // Sign event (prompts user) const signedEvent = await signer.signEvent(unsignedEvent); }
Handling Extension Availability
function getAvailableSigner() { if (typeof window !== 'undefined' && window.nostr) { return new Nip07Signer(); } return null; } // Wait for extension to load async function waitForExtension(timeout = 3000) { const start = Date.now(); while (Date.now() - start < timeout) { if (window.nostr) { return new Nip07Signer(); } await new Promise(r => setTimeout(r, 100)); } return null; }
Extension Permissions
// Some extensions support granular permissions const signer = new Nip07Signer(); // Request specific permissions try { // This varies by extension await window.nostr.enable(); } catch (error) { console.log('User denied permission'); }
NIP-46 Remote Signer
Nostr Connect
import { Nip46Signer } from 'applesauce-signers'; // Create remote signer const signer = new Nip46Signer({ // Remote signer's pubkey remotePubkey: signerPubkey, // Relays for communication relays: ['wss://relay.example.com'], // Local secret key for encryption localSecretKey: localSecretKey, // Optional: custom client name clientName: 'My Nostr App' }); // Connect to remote signer await signer.connect(); // Get public key const pubkey = await signer.getPublicKey(); // Sign event const signedEvent = await signer.signEvent(unsignedEvent); // Disconnect when done signer.disconnect();
Connection URL
// Parse nostrconnect:// URL function parseNostrConnectUrl(url) { const parsed = new URL(url); return { pubkey: parsed.pathname.replace('//', ''), relay: parsed.searchParams.get('relay'), secret: parsed.searchParams.get('secret') }; } // Create signer from URL const { pubkey, relay, secret } = parseNostrConnectUrl(connectUrl); const signer = new Nip46Signer({ remotePubkey: pubkey, relays: [relay], localSecretKey: generateSecretKey(), secret: secret });
Bunker URL
// Parse bunker:// URL (NIP-46) function parseBunkerUrl(url) { const parsed = new URL(url); return { pubkey: parsed.pathname.replace('//', ''), relays: parsed.searchParams.getAll('relay'), secret: parsed.searchParams.get('secret') }; } const { pubkey, relays, secret } = parseBunkerUrl(bunkerUrl);
Signer Management
Signer Store
import { SignerStore } from 'applesauce-signers'; const signerStore = new SignerStore(); // Set active signer signerStore.setSigner(signer); // Get active signer const activeSigner = signerStore.getSigner(); // Clear signer (logout) signerStore.clearSigner(); // Observable for signer changes signerStore.signer$.subscribe(signer => { if (signer) { console.log('Logged in'); } else { console.log('Logged out'); } });
Multi-Account Support
class AccountManager { constructor() { this.accounts = new Map(); this.activeAccount = null; } addAccount(pubkey, signer) { this.accounts.set(pubkey, signer); } removeAccount(pubkey) { this.accounts.delete(pubkey); if (this.activeAccount === pubkey) { this.activeAccount = null; } } switchAccount(pubkey) { if (this.accounts.has(pubkey)) { this.activeAccount = pubkey; return this.accounts.get(pubkey); } return null; } getActiveSigner() { return this.activeAccount ? this.accounts.get(this.activeAccount) : null; } }
Custom Signers
Implementing a Custom Signer
class CustomSigner { constructor(options) { this.options = options; } async getPublicKey() { // Return public key return this.options.pubkey; } async signEvent(event) { // Implement signing logic // Could call external API, hardware wallet, etc. const signedEvent = await this.externalSign(event); return signedEvent; } async nip04Encrypt(pubkey, plaintext) { // Implement NIP-04 encryption throw new Error('NIP-04 not supported'); } async nip04Decrypt(pubkey, ciphertext) { throw new Error('NIP-04 not supported'); } async nip44Encrypt(pubkey, plaintext) { // Implement NIP-44 encryption throw new Error('NIP-44 not supported'); } async nip44Decrypt(pubkey, ciphertext) { throw new Error('NIP-44 not supported'); } }
Hardware Wallet Signer
class HardwareWalletSigner { constructor(devicePath) { this.devicePath = devicePath; } async connect() { // Connect to hardware device this.device = await connectToDevice(this.devicePath); } async getPublicKey() { // Get public key from device return await this.device.getNostrPubkey(); } async signEvent(event) { // Sign on device (user confirms on device) const signature = await this.device.signNostrEvent(event); return { ...event, pubkey: await this.getPublicKey(), id: getEventHash(event), sig: signature }; } }
Read-Only Signer
class ReadOnlySigner { constructor(pubkey) { this.pubkey = pubkey; } async getPublicKey() { return this.pubkey; } async signEvent(event) { throw new Error('Read-only mode: cannot sign events'); } async nip04Encrypt(pubkey, plaintext) { throw new Error('Read-only mode: cannot encrypt'); } async nip04Decrypt(pubkey, ciphertext) { throw new Error('Read-only mode: cannot decrypt'); } }
Signing Utilities
Event Creation Helper
async function createAndSignEvent(signer, template) { const pubkey = await signer.getPublicKey(); const event = { ...template, pubkey, created_at: template.created_at || Math.floor(Date.now() / 1000) }; return await signer.signEvent(event); } // Usage const signedNote = await createAndSignEvent(signer, { kind: 1, content: 'Hello!', tags: [] });
Batch Signing
async function signEvents(signer, events) { const signed = []; for (const event of events) { const signedEvent = await signer.signEvent(event); signed.push(signedEvent); } return signed; } // With parallelization (if signer supports) async function signEventsParallel(signer, events) { return Promise.all( events.map(event => signer.signEvent(event)) ); }
Svelte Integration
Signer Context
<!-- SignerProvider.svelte --> <script> import { setContext } from 'svelte'; import { writable } from 'svelte/store'; const signer = writable(null); setContext('signer', { signer, setSigner: (s) => signer.set(s), clearSigner: () => signer.set(null) }); </script> <slot />
<!-- Component using signer --> <script> import { getContext } from 'svelte'; const { signer } = getContext('signer'); async function publishNote(content) { if (!$signer) { alert('Please login first'); return; } const event = await $signer.signEvent({ kind: 1, content, created_at: Math.floor(Date.now() / 1000), tags: [] }); // Publish event... } </script>
Login Component
<script> import { getContext } from 'svelte'; import { Nip07Signer, SimpleSigner } from 'applesauce-signers'; const { setSigner, clearSigner, signer } = getContext('signer'); let nsec = ''; async function loginWithExtension() { if (window.nostr) { setSigner(new Nip07Signer()); } else { alert('No extension found'); } } function loginWithNsec() { try { const decoded = nip19.decode(nsec); if (decoded.type === 'nsec') { setSigner(new SimpleSigner(decoded.data)); nsec = ''; } } catch (e) { alert('Invalid nsec'); } } function logout() { clearSigner(); } </script> {#if $signer} <button on:click={logout}>Logout</button> {:else} <button on:click={loginWithExtension}> Login with Extension </button> <div> <input type="password" bind:value={nsec} placeholder="nsec..." /> <button on:click={loginWithNsec}> Login with Key </button> </div> {/if}
Best Practices
Security
- Never store secret keys in plain text - Use secure storage
- Prefer NIP-07 - Let extensions manage keys
- Clear keys on logout - Don't leave in memory
- Validate before signing - Check event content
User Experience
- Show signing status - Loading states
- Handle rejections gracefully - User may cancel
- Provide fallbacks - Multiple login options
- Remember preferences - Store signer type
Error Handling
async function safeSign(signer, event) { try { return await signer.signEvent(event); } catch (error) { if (error.message.includes('rejected')) { console.log('User rejected signing'); return null; } if (error.message.includes('timeout')) { console.log('Signing timed out'); return null; } throw error; } }
Permission Checking
function hasEncryptionSupport(signer) { return typeof signer.nip04Encrypt === 'function' || typeof signer.nip44Encrypt === 'function'; } function getEncryptionMethod(signer) { // Prefer NIP-44 if (typeof signer.nip44Encrypt === 'function') { return 'nip44'; } if (typeof signer.nip04Encrypt === 'function') { return 'nip04'; } return null; }
Common Patterns
Signer Detection
async function detectSigners() { const available = []; // Check NIP-07 if (typeof window !== 'undefined' && window.nostr) { available.push({ type: 'nip07', name: 'Browser Extension', create: () => new Nip07Signer() }); } // Check stored credentials const storedKey = localStorage.getItem('nsec'); if (storedKey) { available.push({ type: 'stored', name: 'Saved Key', create: () => new SimpleSigner(storedKey) }); } return available; }
Auto-Reconnect for NIP-46
class ReconnectingNip46Signer { constructor(options) { this.options = options; this.signer = null; } async connect() { this.signer = new Nip46Signer(this.options); await this.signer.connect(); } async signEvent(event) { try { return await this.signer.signEvent(event); } catch (error) { if (error.message.includes('disconnected')) { await this.connect(); return await this.signer.signEvent(event); } throw error; } } }
Signer Type Persistence
const SIGNER_KEY = 'nostr_signer_type'; function saveSigner(type, data) { localStorage.setItem(SIGNER_KEY, JSON.stringify({ type, data })); } async function restoreSigner() { const saved = localStorage.getItem(SIGNER_KEY); if (!saved) return null; const { type, data } = JSON.parse(saved); switch (type) { case 'nip07': if (window.nostr) { return new Nip07Signer(); } break; case 'simple': // Don't store secret keys! break; case 'nip46': const signer = new Nip46Signer(data); await signer.connect(); return signer; } return null; }
Troubleshooting
Common Issues
Extension not detected:
- Wait for page load
- Check window.nostr exists
- Verify extension is enabled
Signing rejected:
- User cancelled in extension
- Handle gracefully with error message
NIP-46 connection fails:
- Check relay is accessible
- Verify remote signer is online
- Check secret matches
Encryption not supported:
- Check signer has encrypt methods
- Fall back to alternative method
- Show user appropriate error
References
- applesauce GitHub: https://github.com/hzrd149/applesauce
- NIP-07 Specification: https://github.com/nostr-protocol/nips/blob/master/07.md
- NIP-46 Specification: https://github.com/nostr-protocol/nips/blob/master/46.md
- nostr-tools: https://github.com/nbd-wtf/nostr-tools
Related Skills
- nostr-tools - Event creation and signing utilities
- applesauce-core - Event stores and queries
- nostr - Nostr protocol fundamentals
- svelte - Building Nostr UIs