This commit is contained in:
gzuuus
2025-07-17 23:53:29 +02:00
parent daaaa4607b
commit c081efa3a0
27 changed files with 3197 additions and 33 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.md) 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.md) 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.md)**, which handles another critical aspect of Nostr communication: managing connections to relays.