- Add Svelte 3/4 skill covering components, reactivity, stores, lifecycle - Add Rollup skill covering configuration, plugins, code splitting - Add nostr-tools skill covering event creation, signing, relay communication - Add applesauce-core skill covering event stores, reactive queries - Add applesauce-signers skill covering NIP-07/NIP-46 signing abstractions - Update .gitignore to include .claude/** directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
15 KiB
name, description
| name | description |
|---|---|
| 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. |
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