Files
next.orly.dev/.claude/skills/applesauce-signers/SKILL.md
mleku 8ea91e39d8 Add Claude Code skills for web frontend frameworks
- 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>
2025-12-06 06:56:57 +00:00

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

  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

  • nostr-tools - Event creation and signing utilities
  • applesauce-core - Event stores and queries
  • nostr - Nostr protocol fundamentals
  • svelte - Building Nostr UIs