applesauce-signers

applesauce-signers Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "applesauce-signers" with this command: npx skills add purrgrammer/grimoire/purrgrammer-grimoire-applesauce-signers

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

Related Skills

  • nostr-tools - Event creation and signing utilities

  • applesauce-core - Event stores and queries

  • nostr - Nostr protocol fundamentals

  • svelte - Building Nostr UIs

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

nostr-tools

No summary provided by upstream source.

Repository SourceNeeds Review
General

nostr

No summary provided by upstream source.

Repository SourceNeeds Review
General

react

No summary provided by upstream source.

Repository SourceNeeds Review
General

applesauce-core

No summary provided by upstream source.

Repository SourceNeeds Review