This commit is contained in:
gzuuus
2025-07-17 23:54:28 +02:00
parent c081efa3a0
commit c8913d025d
27 changed files with 87 additions and 121 deletions

View File

@@ -0,0 +1,93 @@
---
title: Custom Signer Development
description: Learn how to create a custom signer for the @contextvm/sdk.
---
# Custom Signer Development
One of the key design features of the `@contextvm/sdk` is its modularity, which is exemplified by the [`NostrSigner`](./nostr-signer-interface) interface. By creating your own implementation of this interface, you can integrate the SDK with any key management system, such as a hardware wallet, a remote signing service (like an HSM), or a browser extension.
## Why Create a Custom Signer?
While the [`PrivateKeySigner`](./private-key-signer) is suitable for many development and server-side scenarios, a custom signer is often necessary when:
- **Security is paramount**: You need to keep private keys isolated from the main application logic, for example, in a hardware security module (HSM) or a secure enclave.
- **Interacting with external wallets**: Your application needs to request signatures from a user's wallet, such as a browser extension (e.g., Alby, Noster) or a mobile wallet.
- **Complex key management**: Your application uses a more complex key management architecture that doesn't involve direct access to raw private keys.
## Implementing the `NostrSigner` Interface
To create a custom signer, you need to create a class that implements the `NostrSigner` interface. This involves implementing two main methods: `getPublicKey()` and `signEvent()`, as well as an optional `nip44` object for encryption.
### Example: A NIP-07 Browser Signer (window.nostr)
A common use case for a custom signer is in a web application that needs to interact with a Nostr browser extension (like Alby, nos2x, or Blockcore) that exposes the `window.nostr` object according to [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md). This allows the application to request signatures and encryption from the user's wallet without ever handling private keys directly.
Here is how you could implement a `NostrSigner` that wraps the `window.nostr` object:
```typescript
import { NostrSigner } from '@ctxvm/sdk/core';
import { UnsignedEvent, NostrEvent } from 'nostr-tools';
// Define the NIP-07 window.nostr interface for type-safety
declare global {
interface Window {
nostr?: {
getPublicKey(): Promise<string>;
signEvent(event: UnsignedEvent): Promise<NostrEvent>;
nip44?: {
encrypt(pubkey: string, plaintext: string): Promise<string>;
decrypt(pubkey: string, ciphertext: string): Promise<string>;
};
};
}
}
class Nip07Signer implements NostrSigner {
constructor() {
if (!window.nostr) {
throw new Error('NIP-07 compatible browser extension not found.');
}
}
async getPublicKey(): Promise<string> {
if (!window.nostr) throw new Error('window.nostr not found.');
return await window.nostr.getPublicKey();
}
async signEvent(event: UnsignedEvent): Promise<NostrEvent> {
if (!window.nostr) throw new Error('window.nostr not found.');
return await window.nostr.signEvent(event);
}
nip44 = {
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
if (!window.nostr?.nip44) {
throw new Error('The extension does not support NIP-44 encryption.');
}
return await window.nostr.nip44.encrypt(pubkey, plaintext);
},
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
if (!window.nostr?.nip44) {
throw new Error('The extension does not support NIP-44 decryption.');
}
return await window.nostr.nip44.decrypt(pubkey, ciphertext);
},
};
}
```
### Implementing `nip44` for Decryption
When using a NIP-07 signer, the `nip44` implementation is straightforward, as you can see in the example above. You simply delegate the calls to the `window.nostr.nip44` object.
It's important to include checks to ensure that the user's browser extension supports `nip44`, as it is an optional part of the NIP-07 specification. If the extension does not support it, you should throw an error to prevent unexpected behavior.
## Using Your Custom Signer
Once your custom signer class is created, you can instantiate it and pass it to any component that requires a `NostrSigner`, such as the `NostrClientTransport` or `NostrServerTransport`. The rest of the SDK will use your custom implementation seamlessly.
## Next Steps
With the `Signer` component covered, let's move on to the **[Relay](./relay-handler-interface)**, which handles another critical aspect of Nostr communication: managing connections to relays.

View File

@@ -0,0 +1,60 @@
---
title: NostrSigner Interface
description: An interface for signing Nostr events.
---
# `NostrSigner` Interface
The `NostrSigner` interface is a central component of the `@contextvm/sdk`, defining the standard for cryptographic signing operations. Every Nostr event must be signed by a private key to be considered valid, and this interface provides a consistent, pluggable way to handle this requirement.
## Purpose and Design
The primary purpose of the `NostrSigner` is to abstract the process of event signing. By depending on this interface rather than a concrete implementation, the SDK's transports and other components can remain agnostic about how and where private keys are stored and used.
This design offers several key benefits:
- **Security**: Private keys can be managed in secure environments (e.g., web extensions, hardware wallets, dedicated signing services) without exposing them to the application logic.
- **Flexibility**: Developers can easily swap out the default signer with a custom implementation that meets their specific needs.
- **Modularity**: The signing logic is decoupled from the communication logic, leading to a cleaner, more maintainable codebase.
## Interface Definition
The `NostrSigner` interface is defined in [`core/interfaces.ts`](/core/interfaces#nostrsigner).
```typescript
export interface NostrSigner {
getPublicKey(): Promise<string>;
signEvent(event: EventTemplate): Promise<NostrEvent>;
// Optional NIP-04 encryption support (deprecated)
nip04?: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
// Optional NIP-44 encryption support
nip44?: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
}
```
- `getPublicKey()`: Asynchronously returns the public key corresponding to the signer's private key.
- `signEvent(event)`: Takes an unsigned Nostr event, signs it, and returns the signature.
- `nip04`: (Deprecated) Provides NIP-04 encryption support.
- `nip44`: Provides NIP-44 encryption support.
Any class that implements this interface can be used as a signer throughout the SDK.
## Implementations
The SDK provides a default implementation for common use cases and allows for custom implementations for advanced scenarios.
- **[PrivateKeySigner](./private-key-signer)**: The default implementation, which takes a raw private key string and performs signing operations locally.
- **[Custom Signer Development](./custom-signer-development)**: A guide to creating your own signer by implementing the `NostrSigner` interface.
## Next Steps
- Learn about the default implementation: **[PrivateKeySigner](./private-key-signer)**
- Learn how to create your own: **[Custom Signer Development](./custom-signer-development)**

View File

@@ -0,0 +1,70 @@
---
title: PrivateKeySigner
description: A default signer implementation for the @contextvm/sdk.
---
# `PrivateKeySigner`
The `PrivateKeySigner` is the default implementation of the [`NostrSigner`](./nostr-signer-interface) interface provided by the `@contextvm/sdk`. It is a straightforward and easy-to-use signer that operates directly on a raw private key provided as a hexadecimal string.
## Overview
The `PrivateKeySigner` is designed for scenarios where the private key is readily available in the application's environment. It handles all the necessary cryptographic operations locally, including:
- Deriving the corresponding public key.
- Signing Nostr events.
- Encrypting and decrypting messages using NIP-44.
## `constructor(privateKey: string)`
The constructor takes a single argument:
- **`privateKey`**: A hexadecimal string representing the 32-byte Nostr private key.
When instantiated, the `PrivateKeySigner` will immediately convert the hex string into a `Uint8Array` and derive the public key, which is then cached for future calls to `getPublicKey()`.
## Usage Example
```typescript
import { PrivateKeySigner } from '@ctxvm/sdk/signer';
// Replace with a securely stored private key
const privateKeyHex = 'your-32-byte-private-key-in-hex';
const signer = new PrivateKeySigner(privateKeyHex);
// You can now pass this signer instance to a transport
const transportOptions = {
signer: signer,
// ... other options
};
```
## Key Methods
The `PrivateKeySigner` implements all the methods required by the `NostrSigner` interface.
### `async getPublicKey(): Promise<string>`
Returns a promise that resolves with the public key corresponding to the private key provided in the constructor.
### `async signEvent(event: UnsignedEvent): Promise<NostrEvent>`
Takes an unsigned Nostr event, signs it using the private key, and returns a promise that resolves with the finalized, signed `NostrEvent`.
### `nip44`
The `PrivateKeySigner` also provides a `nip44` object that implements NIP-44 encryption and decryption. This is used internally by the transports when encryption is enabled, and it allows the `decryptMessage` function to work seamlessly with this signer.
- `encrypt(pubkey, plaintext)`: Encrypts a plaintext message for a given recipient public key.
- `decrypt(pubkey, ciphertext)`: Decrypts a ciphertext message received from a given sender public key.
## Security Considerations
While the `PrivateKeySigner` is convenient, it requires you to handle a raw private key directly in your application code. **It is crucial to manage this key securely.** Avoid hard-coding private keys in your source code. Instead, use environment variables or a secure secret management system to load the key at runtime.
For applications requiring a higher level of security, consider creating a custom signer that interacts with a hardware wallet or a remote signing service.
## Next Steps
- Learn how to build a custom signer: **[Custom Signer Development](./custom-signer-development)**