Skip to content
applesauce-signers logo

applesauce-signers Skill

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.

SKILL.md

Full skill instructions

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

  1. Never store secret keys in plain text - Use secure storage
  2. Prefer NIP-07 - Let extensions manage keys
  3. Clear keys on logout - Don't leave in memory
  4. Validate before signing - Check event content

User Experience

  1. Show signing status - Loading states
  2. Handle rejections gracefully - User may cancel
  3. Provide fallbacks - Multiple login options
  4. 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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View