Add vitepress docs
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# @welshman/signer
|
||||
|
||||
A comprehensive Nostr signing implementation that supports multiple authentication methods and encryption standards.
|
||||
It provides a unified interface for working with different signing mechanisms while maintaining compatibility with various Nostr Implementation Possibilities (NIPs).
|
||||
|
||||
|
||||
## What's Included
|
||||
|
||||
- **ISigner Interface** - Unified API across all authentication methods
|
||||
- **NIP-01 Signer** - Core implementation using key-pair cryptography
|
||||
- **NIP-07 Signer** - Browser extension support (nos2x, Alby, etc.)
|
||||
- **NIP-46 Signer** - Remote signing with Nostr Connect protocol
|
||||
- **NIP-55 Signer** - Native app integration via Capacitor
|
||||
- **NIP-59 Utils** - Gift Wrap protocol for secure event encryption
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
```bash
|
||||
npm install @welshman/signer
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# ISigner Interface
|
||||
|
||||
A basic interface that each signer must implement.
|
||||
It includes methods for signing messages, verifying signatures, and encrypting/decrypting data.
|
||||
|
||||
|
||||
```typescript
|
||||
interface ISigner {
|
||||
// Core signing functionality
|
||||
sign: (event: StampedEvent) => Promise<SignedEvent>
|
||||
getPubkey: () => Promise<string>
|
||||
|
||||
// Encryption capabilities
|
||||
nip04: {
|
||||
encrypt: (pubkey: string, message: string) => Promise<string>
|
||||
decrypt: (pubkey: string, message: string) => Promise<string>
|
||||
}
|
||||
nip44: {
|
||||
encrypt: (pubkey: string, message: string) => Promise<string>
|
||||
decrypt: (pubkey: string, message: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
# NIP-01 Signer
|
||||
|
||||
The `Nip01Signer` class implements the `ISigner` interface and extends it with additional static utility methods:
|
||||
|
||||
```typescript
|
||||
class Nip01Signer implements ISigner {
|
||||
// Constructor
|
||||
constructor(private secret: string)
|
||||
|
||||
// ISigner implementation
|
||||
sign: (event: StampedEvent) => Promise<SignedEvent>
|
||||
getPubkey: () => Promise<string>
|
||||
nip04: { encrypt, decrypt }
|
||||
nip44: { encrypt, decrypt }
|
||||
|
||||
// Additional static utility methods
|
||||
static fromSecret(secret: string): Nip01Signer
|
||||
static ephemeral(): Nip01Signer
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Methods
|
||||
|
||||
The NIP-01 implementation extends the base interface with two static utility methods:
|
||||
|
||||
- `static fromSecret(secret: string)`: Alternative constructor for creating a signer from an existing private key
|
||||
- `static ephemeral()`: Creates a new signer with a randomly generated private key
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import { ISigner } from './interfaces'
|
||||
import { Nip01Signer } from './signers/nip01'
|
||||
|
||||
// Using the standard interface
|
||||
const signer: ISigner = new Nip01Signer(mySecret)
|
||||
|
||||
// Using NIP-01 specific utilities
|
||||
const ephemeralSigner = Nip01Signer.ephemeral()
|
||||
const fromExistingKey = Nip01Signer.fromSecret(mySecret)
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
# NIP-07 Signer
|
||||
|
||||
The `Nip07Signer` implements the `ISigner` interface by delegating signing operations to a NIP-07 compatible browser extension (like nos2x or Alby). It provides a way to interact with user's keys that are securely stored in their browser extension.
|
||||
|
||||
## Browser Detection
|
||||
|
||||
```typescript
|
||||
import { getNip07 } from '@welshman/signer'
|
||||
|
||||
// Check if a NIP-07 provider is available
|
||||
if (getNip07()) {
|
||||
// Browser has a compatible extension installed
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { Nip07Signer } from '@welshman/signer'
|
||||
|
||||
// Create a new signer instance
|
||||
const signer = new Nip07Signer()
|
||||
|
||||
// The extension will prompt the user for permission
|
||||
// when operations are performed
|
||||
```
|
||||
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Nip07Signer, getNip07 } from '@welshman/signer'
|
||||
import { createEvent, NOTE } from '@welshman/util'
|
||||
|
||||
async function example() {
|
||||
// Check for NIP-07 provider
|
||||
if (!getNip07()) {
|
||||
throw new Error('No NIP-07 provider found. Please install a Nostr browser extension.')
|
||||
}
|
||||
|
||||
// Create signer
|
||||
const signer = new Nip07Signer()
|
||||
|
||||
try {
|
||||
// Get public key (will prompt user)
|
||||
const pubkey = await signer.getPubkey()
|
||||
console.log('Public key:', pubkey)
|
||||
|
||||
// Create and sign an event (will prompt user)
|
||||
const event = createEvent(NOTE, {
|
||||
content: "Hello via browser extension!",
|
||||
tags: [["t", "test"]]
|
||||
})
|
||||
const signedEvent = await signer.sign(event)
|
||||
console.log('Signed event:', signedEvent)
|
||||
|
||||
// Encrypt a message (will prompt user)
|
||||
const recipientPubkey = "..."
|
||||
const encrypted = await signer.nip44.encrypt(recipientPubkey, "Secret message")
|
||||
console.log('Encrypted message:', encrypted)
|
||||
} catch (error) {
|
||||
// Handle user rejection or other errors
|
||||
console.error('Operation failed:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Request Serialization
|
||||
The signer implements a lock mechanism to prevent concurrent calls to the extension:
|
||||
|
||||
```typescript
|
||||
class Nip07Signer implements ISigner {
|
||||
#lock = Promise.resolve()
|
||||
|
||||
#then = async <T>(f: (ext: Nip07) => T | Promise<T>) => {
|
||||
const promise = this.#lock.then(() => {
|
||||
const ext = getNip07()
|
||||
if (!ext) throw new Error("Nip07 is not enabled")
|
||||
return f(ext)
|
||||
})
|
||||
|
||||
// Reset lock after completion or error
|
||||
this.#lock = promise.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
)
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
# NIP-46 (Nostr Connect) Signer
|
||||
|
||||
The `Nip46Signer` implements remote signing capabilities through the Nostr Connect protocol (NIP-46). It allows applications to delegate signing operations to a remote signer (like a Nostr Bunker), providing enhanced security by keeping private keys separate from the application.
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation consists of two main classes:
|
||||
- `Nip46Broker`: Handles the communication with the remote signer
|
||||
- `Nip46Signer`: Implements the `ISigner` interface using the broker
|
||||
|
||||
## Getting Started
|
||||
|
||||
```typescript
|
||||
import {
|
||||
makeSecret,
|
||||
Nip46Broker,
|
||||
Nip46Signer
|
||||
} from '@welshman/signer'
|
||||
import { createEvent, NOTE } from '@welshman/util'
|
||||
|
||||
async function connectToRemoteSigner() {
|
||||
// Initial setup
|
||||
const clientSecret = makeSecret()
|
||||
const relays = ['wss://relay.example.com']
|
||||
const broker = Nip46Broker.get({ relays, clientSecret })
|
||||
const signer = new Nip46Signer(broker)
|
||||
|
||||
// Generate connection URL
|
||||
const ncUrl = await broker.makeNostrconnectUrl({
|
||||
name: "My App",
|
||||
description: "Testing remote signing"
|
||||
})
|
||||
|
||||
// Show URL to user (e.g., as QR code)
|
||||
displayQRCode(ncUrl)
|
||||
|
||||
try {
|
||||
// Wait for connection
|
||||
const response = await broker.waitForNostrconnect(
|
||||
ncUrl,
|
||||
new AbortController()
|
||||
)
|
||||
|
||||
// Store signer info for later
|
||||
const bunkerUrl = broker.getBunkerUrl()
|
||||
localStorage.setItem('bunkerUrl', bunkerUrl)
|
||||
|
||||
// Use the signer
|
||||
const event = createEvent(NOTE, {
|
||||
content: "Signed with remote signer!",
|
||||
tags: [["t", "test"]]
|
||||
})
|
||||
const signed = await signer.sign(event)
|
||||
|
||||
return signed
|
||||
} catch (error) {
|
||||
if (error?.error) {
|
||||
console.warn(`Signer error: ${error.error}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnecting with saved bunker URL
|
||||
async function reconnect() {
|
||||
const bunkerUrl = localStorage.getItem('bunkerUrl')
|
||||
if (!bunkerUrl) return null
|
||||
|
||||
const {
|
||||
signerPubkey,
|
||||
connectSecret,
|
||||
relays
|
||||
} = Nip46Broker.parseBunkerUrl(bunkerUrl)
|
||||
|
||||
const broker = Nip46Broker.get({
|
||||
relays,
|
||||
clientSecret: makeSecret(),
|
||||
signerPubkey,
|
||||
connectSecret
|
||||
})
|
||||
|
||||
return new Nip46Signer(broker)
|
||||
}
|
||||
```
|
||||
|
||||
## Nip46Broker API
|
||||
|
||||
### Constructor and Factory
|
||||
|
||||
```typescript
|
||||
// Recommended: use the singleton factory
|
||||
const broker = Nip46Broker.get({
|
||||
relays: string[],
|
||||
clientSecret: string,
|
||||
connectSecret?: string,
|
||||
signerPubkey?: string,
|
||||
algorithm?: "nip04" | "nip44"
|
||||
})
|
||||
|
||||
// Direct instantiation (not recommended)
|
||||
new Nip46Broker(params)
|
||||
```
|
||||
|
||||
### Connection Methods
|
||||
|
||||
```typescript
|
||||
// Generate a nostrconnect:// URL for the remote signer
|
||||
broker.makeNostrconnectUrl(metadata: Record<string, string>): Promise<string>
|
||||
|
||||
// Wait for connection approval
|
||||
broker.waitForNostrconnect(
|
||||
url: string,
|
||||
abort?: AbortController
|
||||
): Promise<Nip46ResponseWithResult>
|
||||
|
||||
// Get bunker URL for later reconnection
|
||||
broker.getBunkerUrl(): string
|
||||
|
||||
// Parse a bunker URL
|
||||
Nip46Broker.parseBunkerUrl(url: string): {
|
||||
signerPubkey: string,
|
||||
connectSecret: string,
|
||||
relays: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### Remote Operations
|
||||
|
||||
```typescript
|
||||
// Basic operations
|
||||
broker.ping(): Promise<string>
|
||||
broker.getPublicKey(): Promise<string>
|
||||
broker.connect(connectSecret?: string, perms?: string): Promise<string>
|
||||
|
||||
// Signing and encryption
|
||||
broker.signEvent(event: StampedEvent): Promise<SignedEvent>
|
||||
broker.nip04Encrypt(pk: string, message: string): Promise<string>
|
||||
broker.nip04Decrypt(pk: string, message: string): Promise<string>
|
||||
broker.nip44Encrypt(pk: string, message: string): Promise<string>
|
||||
broker.nip44Decrypt(pk: string, message: string): Promise<string>
|
||||
```
|
||||
|
||||
## Nip46Signer Usage
|
||||
|
||||
```typescript
|
||||
const signer = new Nip46Signer(broker)
|
||||
|
||||
// All ISigner operations are available
|
||||
const pubkey = await signer.getPubkey()
|
||||
const signed = await signer.sign(event)
|
||||
const encrypted = await signer.nip44.encrypt(pubkey, "message")
|
||||
const decrypted = await signer.nip44.decrypt(pubkey, encrypted)
|
||||
```
|
||||
@@ -0,0 +1,165 @@
|
||||
# NIP-55 (Native App) Signer
|
||||
|
||||
The `Nip55Signer` implements the `ISigner` interface by communicating with native mobile signing applications through the Capacitor plugin system. This implementation is particularly useful for mobile applications that want to leverage native Nostr signing capabilities.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The signer requires the Capacitor plugin to be installed:
|
||||
|
||||
```bash
|
||||
npm install nostr-signer-capacitor-plugin
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
```typescript
|
||||
import { Nip55Signer, getNip55 } from '@welshman/signer'
|
||||
|
||||
// Check for available signing apps
|
||||
const apps = await getNip55()
|
||||
if (apps.length > 0) {
|
||||
const signer = new Nip55Signer(apps[0].packageName)
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Detecting Available Signers
|
||||
|
||||
```typescript
|
||||
// Returns information about installed signing apps
|
||||
getNip55(): Promise<AppInfo[]>
|
||||
|
||||
interface AppInfo {
|
||||
name: string
|
||||
packageName: string
|
||||
// Other app-specific information
|
||||
}
|
||||
```
|
||||
|
||||
### Constructor
|
||||
|
||||
```typescript
|
||||
constructor(packageName: string)
|
||||
```
|
||||
Creates a new signer instance that will communicate with the specified native app.
|
||||
- `packageName`: The package identifier of the native signing app
|
||||
|
||||
### ISigner implementation
|
||||
|
||||
The `Nip55Signer` class implements the [`ISigner`](/signer/) interface
|
||||
|
||||
```typescript
|
||||
class Nip55Signer implements ISigner {
|
||||
// Constructor
|
||||
constructor(private secret: string)
|
||||
|
||||
// ISigner implementation
|
||||
sign: (event: StampedEvent) => Promise<SignedEvent>
|
||||
getPubkey: () => Promise<string>
|
||||
nip04: { encrypt, decrypt }
|
||||
nip44: { encrypt, decrypt }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { Nip55Signer, getNip55 } from '@welshman/signer'
|
||||
import { createEvent, NOTE } from '@welshman/util'
|
||||
|
||||
async function example() {
|
||||
try {
|
||||
// Get available signing apps
|
||||
const apps = await getNip55()
|
||||
if (apps.length === 0) {
|
||||
throw new Error('No native signing apps available')
|
||||
}
|
||||
|
||||
// Create signer with first available app
|
||||
const signer = new Nip55Signer(apps[0].packageName)
|
||||
|
||||
// Get public key
|
||||
const pubkey = await signer.getPubkey()
|
||||
console.log('Public key:', pubkey)
|
||||
|
||||
// Sign an event
|
||||
const event = createEvent(NOTE, {
|
||||
content: "Hello from native app!",
|
||||
tags: [["t", "test"]]
|
||||
})
|
||||
const signedEvent = await signer.sign(event)
|
||||
console.log('Signed event:', signedEvent)
|
||||
|
||||
// Encrypt a message
|
||||
const encrypted = await signer.nip44.encrypt(
|
||||
recipientPubkey,
|
||||
"Secret message"
|
||||
)
|
||||
console.log('Encrypted:', encrypted)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Native signer error:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Request Serialization
|
||||
|
||||
The signer implements a lock mechanism to prevent concurrent requests:
|
||||
|
||||
```typescript
|
||||
class Nip55Signer implements ISigner {
|
||||
#lock = Promise.resolve()
|
||||
#plugin = NostrSignerPlugin
|
||||
#packageName: string
|
||||
#packageNameSet = false
|
||||
|
||||
#then = async <T>(f: (signer: typeof NostrSignerPlugin) => Promise<T>) => {
|
||||
const promise = this.#lock.then(async () => {
|
||||
if (!this.#packageNameSet) {
|
||||
await this.#initialize()
|
||||
}
|
||||
return f(this.#plugin)
|
||||
})
|
||||
|
||||
this.#lock = promise.then(() => Promise.resolve())
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Public Key Caching
|
||||
|
||||
The signer caches the public key to minimize native app interactions:
|
||||
|
||||
```typescript
|
||||
class Nip55Signer {
|
||||
#npub?: string
|
||||
#publicKey?: string
|
||||
|
||||
getPubkey = async (): Promise<string> => {
|
||||
return this.#then(async signer => {
|
||||
if (!this.#publicKey || !this.#npub) {
|
||||
const {npub} = await signer.getPublicKey()
|
||||
this.#npub = npub
|
||||
const {data} = decode(npub)
|
||||
this.#publicKey = data as string
|
||||
}
|
||||
return this.#publicKey
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Platform Support
|
||||
|
||||
- iOS: Requires compatible signing app
|
||||
- Android: Requires compatible signing app
|
||||
- Operations availability depends on native app implementation
|
||||
- Some features might be platform-specific
|
||||
@@ -0,0 +1,151 @@
|
||||
# NIP-59 (Gift Wrap) Implementation
|
||||
|
||||
The `Nip59` class provides utilities for implementing the Gift Wrap protocol (NIP-59), allowing secure event wrapping and unwrapping. This implementation works with any signer that supports encryption, making it versatile for different authentication methods.
|
||||
|
||||
## Key Features
|
||||
|
||||
- Event wrapping (encryption) for specific recipients
|
||||
- Event unwrapping (decryption) of received wrapped events
|
||||
- Automatic ephemeral wrapper generation
|
||||
- Caching of previously unwrapped events
|
||||
- Compatible with all signer implementations
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Nip59 } from '@welshman/signer'
|
||||
import { createEvent, DIRECT_MESSAGE } from '@welshman/util'
|
||||
|
||||
// Create a NIP-59 instance from any signer
|
||||
const nip59 = Nip59.fromSigner(mySigner)
|
||||
|
||||
// Wrap an event
|
||||
const rumor = await nip59.wrap(
|
||||
recipientPubkey,
|
||||
createEvent(DIRECT_MESSAGE, {
|
||||
content: "Secret message",
|
||||
tags: [["p", recipientPubkey]]
|
||||
})
|
||||
)
|
||||
|
||||
// The wrapped event to publish
|
||||
const wrappedEvent = rumor.wrap
|
||||
|
||||
// Unwrap a received event
|
||||
const unwrapped = await nip59.unwrap(receivedWrappedEvent)
|
||||
```
|
||||
|
||||
### Wrapping Process
|
||||
|
||||
The wrapping process involves multiple steps:
|
||||
|
||||
1. Create the rumor (original event)
|
||||
2. Create the seal (encrypted rumor)
|
||||
3. Create the wrap (encrypted seal)
|
||||
|
||||
```typescript
|
||||
export const wrap = async (
|
||||
signer: ISigner,
|
||||
wrapper: ISigner,
|
||||
pubkey: string,
|
||||
template: StampedEvent,
|
||||
tags: string[][] = []
|
||||
) => {
|
||||
const rumor = await getRumor(signer, template)
|
||||
const seal = await getSeal(signer, pubkey, rumor)
|
||||
const wrap = await getWrap(wrapper, pubkey, seal, tags)
|
||||
|
||||
return Object.assign(rumor, {wrap})
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constructor & Factory Methods
|
||||
|
||||
```typescript
|
||||
class Nip59 {
|
||||
// Constructor
|
||||
constructor(signer: ISigner, wrapper?: ISigner)
|
||||
|
||||
// Factory Methods
|
||||
static fromSigner(signer: ISigner): Nip59
|
||||
static fromSecret(secret: string): Nip59
|
||||
|
||||
// Instance Methods
|
||||
|
||||
/**
|
||||
* Wraps an event for a specific recipient
|
||||
* @param pubkey Recipient's public key
|
||||
* @param template The event to wrap
|
||||
* @param tags Additional tags for the wrap event (optional)
|
||||
* @returns Promise<UnwrappedEvent> Original event and its wrapped version
|
||||
*/
|
||||
wrap(
|
||||
pubkey: string,
|
||||
template: StampedEvent,
|
||||
tags?: string[][]
|
||||
): Promise<UnwrappedEvent>
|
||||
|
||||
/**
|
||||
* Unwraps a received wrapped event
|
||||
* @param event The wrapped event to decrypt
|
||||
* @returns Promise<UnwrappedEvent> The original unwrapped event
|
||||
*/
|
||||
unwrap(event: SignedEvent): Promise<UnwrappedEvent>
|
||||
|
||||
/**
|
||||
* Creates a new instance with a specific wrapper signer
|
||||
* @param wrapper Signer to use for wrapping events
|
||||
* @returns Nip59 New instance with the specified wrapper
|
||||
*/
|
||||
withWrapper(wrapper: ISigner): Nip59
|
||||
}
|
||||
```
|
||||
|
||||
## Detailed Examples
|
||||
|
||||
### Basic Wrapping & Unwrapping
|
||||
|
||||
```typescript
|
||||
import { Nip59, Nip01Signer } from '@welshman/signer'
|
||||
import { createEvent, DIRECT_MESSAGE } from '@welshman/util'
|
||||
|
||||
async function example() {
|
||||
// Create NIP-59 instance
|
||||
const signer = new Nip01Signer(mySecret)
|
||||
const nip59 = Nip59.fromSigner(signer)
|
||||
|
||||
// Create and wrap an event
|
||||
const event = createEvent(DIRECT_MESSAGE, {
|
||||
content: "Secret message",
|
||||
tags: [["p", recipientPubkey]]
|
||||
})
|
||||
|
||||
const rumor = await nip59.wrap(recipientPubkey, event)
|
||||
|
||||
// rumor contains:
|
||||
// - The original event (rumor)
|
||||
// - The wrapped version to publish (rumor.wrap)
|
||||
|
||||
// Later, unwrap a received event
|
||||
const unwrapped = await nip59.unwrap(receivedEvent)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Wrapper Signer
|
||||
|
||||
```typescript
|
||||
import { Nip59, Nip01Signer } from '@welshman/signer'
|
||||
|
||||
// Create with specific wrapper
|
||||
const nip59 = new Nip59(
|
||||
mainSigner,
|
||||
Nip01Signer.ephemeral() // Custom wrapper
|
||||
)
|
||||
|
||||
// Or add wrapper to existing instance
|
||||
const nip59WithWrapper = nip59.withWrapper(
|
||||
Nip01Signer.ephemeral()
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user