https://github.com/cmdruid/nostr-p2p
Build your own peer-to-peer messaging protocol, transmitted by relays.
https://github.com/cmdruid/nostr-p2p
nostr p2p
Last synced: about 1 year ago
JSON representation
Build your own peer-to-peer messaging protocol, transmitted by relays.
- Host: GitHub
- URL: https://github.com/cmdruid/nostr-p2p
- Owner: cmdruid
- License: mit
- Created: 2024-11-07T00:27:39.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2025-02-22T21:00:41.000Z (over 1 year ago)
- Last Synced: 2025-04-09T21:51:26.716Z (about 1 year ago)
- Topics: nostr, p2p
- Language: TypeScript
- Homepage:
- Size: 226 KB
- Stars: 4
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# nostr-p2p
Build your own peer-to-peer messaging protocol, transmitted by relays.
## Overview
This project provides a light-weight client for building your own peer-to-peer messaging protocol on-top of the Nostr relay network. The client handles peering, encryption, message routing, and relay management while providing a simple API for developers.
### Features
* **Simplified Messaging**
Provides a `SignedMessage` object for passing structured messages between peers, with support for direct messages, broadcasts, multicasts, and request-response.
* **Event-Driven Routing**
Includes an event-based inbox for handling incoming messages, with emitters for event type, message ID, peer pubkey, and topic tag.
* **Encryption and Validation**
Messages are end-to-end encrypted (AES-256-GCM) between peers, with strict runtime validation (using Zod).
* **Message Receipts**
Detailed promise-based receipts for asynchronous message delivery and response collection.
* **Reference Node**
Implements a generic `NostrNode` class with manageable relay connections, event filters, and timeouts.
## Installation
The `nostr-p2p` package can be installed in both node and browser environments. It's available through the npm registry for node projects, and via the unpkg CDN for browser applications.
### Node Environment
For node environments, simply install the package using your preferred package manager:
```bash
npm install @cmdcode/nostr-p2p
```
Then, import the package in your project:
```ts
import { NostrNode } from '@cmdcode/nostr-p2p'
```
### Browser Environment
You can include the package directly in your HTML file:
```html
const { NostrNode } = window.nostr_p2p
```
You can also import it as a module in modern browsers:
```html
import { NostrNode } from 'https://unpkg.com/@cmdcode/nostr-p2p/dist/module.js'
```
## Basic Usage
The core component of this library is the `NostrNode` client, which handles all the complexities of peer-to-peer messaging.
Each node can:
- Connect to multiple relays for redundancy.
- Send encrypted messages to specific peers.
- Broadcast messages to multiple peers.
- Handle request-response patterns.
- Subscribe to specific message types or peers.
- Manage message delivery receipts.
### Creating a Node
Create a basic Nostr node and connect it to a relay:
```typescript
import { NostrNode } from '@cmdcode/nostr-p2p'
// List of relays to connect to.
const relays = [
'wss://relay.nostrdice.com',
'wss://relay.snort.social'
]
// Create a new Nostr node.
const seckey = '...' // Your private key.
const node = new NostrNode(relays, seckey)
node.on('ready', () => {
// Fires when the node is ready to send and receive messages.
console.log('connected to:', node.relays)
})
// Connect to the relays.
await node.connect()
```
You can configure the node with additional options:
```ts
interface NodeOptions {
envelope ?: Partial // Event envelope configuration.
filter ?: Partial // Event filter configuration.
req_timeout ?: number // Request timeout.
since_offset ?: number // Time offset for filtering events.
start_delay ?: number // Delay before starting to listen for events.
}
```
### Message Structure
Messages are structured in a basic format, with a message `id`, a topic `tag`, and a `data` payload:
```ts
interface MessageTemplate {
data : Json // The message payload, serialized as JSON.
id? : string // Identifier for tracking specific messages.
tag : string // A label to categorize and filter messages.
}
```
When sending a message, you can specify additional options for the delivery:
```ts
interface DeliveryOptions {
cache? : Map // Cache for tracking publishing status.
kind? : number // The kind of event to publish.
tags? : string[][] // Additional tags to include in the event.
timeout? : number // The timeout for the delivery response (if any).
}
```
Delivered messages also include the original nostr event, stored as `env` (short for envelope):
```ts
interface SignedMessage {
data : Json // The message payload as JSON.
env : SignedEvent // Original signed nostr event.
id : string // The message identifier.
tag : string // The message topic.
}
```
### Messaging API
The `NostrNode` client provides several methods for sending and receiving messages.
#### Direct Messages
The `publish` method encrypts and sends a message to a single peer:
```ts
const res : Promise = node.publish(
message : MessageTemplate,
peer_pk : string,
options? : Partial
)
```
The `PubResponse` object includes the following properties:
```ts
interface PubResponse {
acks : string[] // List of relays that acknowledged the message.
fails : string[] // List of relays that failed to respond.
ok : boolean // True if at least one relay acknowledged.
peer_pk : string // The public key of the recipient peer.
}
```
#### Broadcasts
The `broadcast` method sends a message to multiple peers:
```ts
const res : Promise = node.broadcast(
message : MessageTemplate,
peers : string[],
options? : Partial
)
```
All messages share the same message `id` and `tag`, but each recipient receives their own encrypted copy.
The `BroadcastResponse` object includes the following properties:
```ts
interface BroadcastResponse {
cache : Map // Map of each peer pubkey to their PubResponse.
ok : boolean // True if all responses were successful.
peers : string[] // List of peers that received the message.
}
```
#### Request and Response
The `request` method is useful when you expect a response from the peer:
```ts
const res : Promise = node.request(
message : MessageTemplate,
peer_pk : string,
options? : Partial
)
```
The `ReqResponse` object includes the following properties:
```ts
interface ReqResponse {
pub : PubResponse // The PubResponse from publishing to the relays.
sub : SubResponse // The SubResponse from listening for the message id.
}
```
#### Multi-Peer Requests
The `multicast` method is useful when you expect a response from multiple peers:
```ts
const res : Promise = node.multicast(
message : MessageTemplate,
peers : string[],
options? : Partial
)
```
It sends a request to multiple peers and collects their responses, returning an array of all received responses within the timeout period.
The `MulticastResponse` object includes the following properties:
```ts
interface MulticastResponse {
pub : BroadcastResponse // The BroadcastResponse from publishing to the relays.
sub : SubResponse // The SubResponse from listening for the message id.
}
```
#### Custom Subscriptions
The `subscribe` method allows you to implement a custom message listener with a timeout:
```ts
interface SubFilter {
id ?: string // The message id to listen for.
peers ?: string[] // The peers to listen for.
tag ?: string // The message tag to listen for.
}
interface SubConfig {
threshold ?: number // The number of responses to collect.
timeout ?: number // The timeout for the subscription.
}
// Custom subscription
const sub : Promise = node.subscribe (
filter : EventFilter,
options : Partial
)
```
This method is useful when you need to implement a custom message listener that is not covered by the other methods.
The `SubResponse` object includes the following properties:
```ts
type ResolveReason = 'complete' | 'timeout' | 'threshold'
interface SubResponse {
authors : string[] // List of authors that published messages matching the filter.
inbox : SignedMessage[] // List of messages matching the filter.
ok : boolean // True if at least one message was found.
peers : string[] // List of peers that published messages matching the filter.
reason : ResolveReason // The reason the subscription was resolved.
}
```
### Event Handling
The SDK provides an event inbox system for handling incoming messages. You can listen for messages using various filters:
#### On Message ID
For tracking messages with a specific ID, such as responses to requests:
```ts
node.inbox.id.on('deadbeef', (msg : SignedMessage) => {
console.log('Got message:', msg.data)
})
```
#### On Message Peer
For tracking messages from a specific peer, based on their public key:
```ts
node.inbox.peer.on('pubkey123', (msg : SignedMessage) => {
console.log('From peer:', msg.data)
})
```
#### On Message Topic
Listens for messages with a specific topic, allowing you to handle different types of messages with dedicated handlers.
```ts
node.inbox.tag.on('status', (msg : SignedMessage) => {
console.log('Status update:', msg.data)
})
```
#### Inbox Events
The inbox emits several events that you can subscribe to for monitoring message handling:
```ts
// Listen for published messages.
node.inbox.event.on('published', (msg: SignedMessage) => {
console.log('Message published:', msg)
})
// Listen for broadcast results
node.inbox.event.on('broadcast', (res: BroadcastResponse & MessageIdResponse) => {
console.log('Broadcast complete:', response)
})
// Listen for settled messages (completed deliveries)
node.inbox.event.on('settled', (res: PubResponse & MessageIdResponse) => {
console.log('Message settled:', response)
})
```
#### Node Events
The node itself emits several events that provide comprehensive monitoring of its operation:
```ts
// Listen for bounced events
node.on('bounced', (event_id: string, error: string) => console.log('Message bounced:', event_id, error))
// Listen for when the node is closed.
node.on('closed', (node: NostrNode) => console.log('Node closed:', node))
// Listen for debug messages
node.on('debug', (info: unknown) => console.log('Debug:', info))
// Listen for errors.
node.on('error', (error: unknown) => console.error('Node error:', error))
// Listen for info messages.
node.on('info', (info: Json) => console.log('Info:', info))
// Listen for received messages.
node.on('message', (msg: SignedMessage) => console.log('Received message:', msg))
// Listen for when the node is ready to send and receive messages.
node.on('ready', (node: NostrNode) => console.log('Node is ready:', node))
// Listen for new subscriptions to the relays.
node.on('subscribed', (sub_id: string, filter: EventFilter) => console.log('New subscription:', sub_id, filter))
```
### Advanced Features
Here are some advanced features that you can use:
#### Event Handling Options
```ts
// Time-limited subscriptions
node.inbox.tag.within('status', (msg) => {
console.log('Status within 5s:', msg)
}, 5000)
```
The `within` method creates a temporary subscription that automatically unsubscribes after the specified timeout. This is useful for gathering time-sensitive responses.
```ts
// One-time handlers
node.inbox.id.once('deadbeef', (msg) => {
console.log('First response:', msg)
})
```
Use `once` when you only need to handle the first occurrence of a message. The handler automatically unsubscribes after being triggered once.
### Demo Example
Here is a basic example of how to pass a message between two nodes:
```ts
// Create the actors.
const Alice = new NostrNode(relays, alice_sk)
const Bob = new NostrNode(relays, bob_sk)
// Configure the Alice node.
Alice.inbox.tag.on('ping', (msg) => {
const res = { id: msg.id, tag: 'pong', data: 'pong!' }
Alice.publish(res, msg.env.pubkey)
})
Alice.on('ready', () => {
console.log('alice connected')
})
// Configure the Bob node.
Bob.inbox.tag.on('pong', (msg) => {
console.log('received pong message:', msg.data)
cleanup()
})
Bob.on('ready', () => {
console.log('bob connected')
Bob.publish({ tag: 'ping', data: 'ping!' }, Alice.pubkey)
})
```
You can see a full example of this in the `test/demo.ts` file, and run the demo yourself with `npm run demo`.
## Development
The project is built using the `rollup` bundler and `tsx` for live transpilation. The `script/build.sh` script will build the project and copy the necessary files to the `dist` directory.
```bash
# Install dependencies.
npm install
# Run test suite
npm test
# Build package to dist directory.
npm run build
```
The test suite contains a basic implementation of a nostr relay, plus methods for generating a set of nodes. Please refer to the `test/tape.ts` file for more details.
## Roadmap
Here is a list of features that are planned for future releases:
* **Peer Discovery**
Currently, each node must be manually configured with a list of peers. We plan to implement
a message channel for automatic peer discovery, including join/leave notifications.
* **Public Profiles**
Enable nodes to leverage public profiles for advertising their presence and sharing metadata
with the network.
* **Private Stores**
Implement a secure, local storage system for each node to maintain configuration and state
data persistently.
* **Shared Stores**
Develop a distributed data sharing mechanism allowing nodes to maintain and synchronize
shared data repositories.
## Resources
This project uses the following open source libraries:
- [Buff](https://github.com/cmdcode/buff): Swiss-army knife for byte manipulation and encoding.
- [Noble Curves](https://github.com/paulmillr/noble-curves): A fast, lightweight ECC library.
- [Noble Ciphers](https://github.com/paulmillr/noble-ciphers): A fast, lightweight encryption library.
- [Nostr Tools](https://github.com/nostr-tools): Tools for working with the Nostr protocol.
- [Zod](https://github.com/colinhacks/zod): Run-time data validation library.
## License
MIT