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.

mattnigh/skills_collection0installs24stars

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 mattnigh

spring-framework-patterns logo
mattnigh/skills_collection

spring-framework-patterns

Comprehensive Spring Framework and Spring Boot best practices including dependency injection patterns, bean lifecycle and scopes, REST API development, Spring Data JPA, service layer design, Spring Security, testing strategies, caching, AOP, async processing, error handling, and common anti-patte...

24 0
View

TypeScript厳格モードによる型安全性設計を専門とするスキル。 📖 参照書籍: - 『Effective TypeScript』(Dan Vanderkam): 型設計 📚 リソース参照: - `resources/Level1_basics.md`: レベル1の基礎ガイド - `resources/Level2_intermediate.md`: レベル2の実務ガイド - `resources/Level3_advanced.md`: レベル3の応用ガイド - `resources/Level4_expert.md`: レベル4の専門ガイド - `resources/di...

24 0
View
deploying-cloud-k8s logo
mattnigh/skills_collection

deploying-cloud-k8s

Deploys applications to cloud Kubernetes (AKS/GKE/DOKS) with CI/CD pipelines. Use when deploying to production, setting up GitHub Actions, troubleshooting deployments. Covers build-time vs runtime vars, architecture matching, and battle-tested debugging.

24 0
View
moai-alfred-agent-guide logo
mattnigh/skills_collection

moai-alfred-agent-guide

19-agent team structure, decision trees for agent selection, Haiku vs Sonnet model selection, and agent collaboration principles. Use when deciding which sub-agent to invoke, understanding team responsibilities, or learning multi-agent orchestration.

24 0
View
mermaid-diagram-generator logo
mattnigh/skills_collection

mermaid-diagram-generator

Converts architecture descriptions, module specs, or workflow docs into Mermaid diagrams. Use when visualizing brick module relationships, workflows (DDD, investigation), or system architecture. Supports: flowcharts, sequence diagrams, class diagrams, state machines, entity relationship diagrams,...

24 0
View
pre-flight-check logo
mattnigh/skills_collection

pre-flight-check

INVOKE FIRST before any code work. Validates git workflow (branch, issue, worklog) and checks approach. Use at START of every task and END before completing. Prevents skipped steps.

24 0
View
browser-dev-tools logo
mattnigh/skills_collection

browser-dev-tools

This skill should be used when working with browser-rendered artifacts (i.e. Bun, React/React Native) to proactively validate that development work on localhost is being built appropriately and to support debugging browser-rendered content. Use this skill after making frontend changes to verify t...

24 0
View
lead-dev logo
mattnigh/skills_collection

lead-dev

Lead Développeur - Coordination technique opérationnelle, code review, mentoring et livraison. Pair de web-dev-process au niveau OPÉRATIONS.

24 0
View
worker-monitor logo
mattnigh/skills_collection

worker-monitor

Monitor Docker workers, RQ queue health, and auto-scale workers (max 4). Use when: checking job progress, monitoring queue depth, scaling workers up/down, diagnosing slow processing, or waiting for jobs to complete. Referenced by data-quality skill during reprocessing.

24 0
View
quality-advisor logo
mattnigh/skills_collection

quality-advisor

Proactive quality guidance system that monitors artifact creation and provides real-time feedback on documentation quality

24 0
View
bump-version logo
mattnigh/skills_collection

bump-version

This skill should be used when the user wants to bump the version number in the workspace. It updates versions across all pyproject.toml files (root, tde, and tda packages) and the CHANGELOG.md to keep them synchronized.

24 0
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