Skip to content

Commit f805521

Browse files
committed
feat: make private key optional for read-only operations
- Added read-only/read-write modes to relay config - Created default read-only key for basic operations - Updated UI to show/hide private key based on mode - Removed unused nostr-websocket-utils package - Updated documentation to clarify private key requirements
1 parent a52755f commit f805521

File tree

5 files changed

+168
-27
lines changed

5 files changed

+168
-27
lines changed

README.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
A [Node-RED](http://nodered.org) node for integrating with the Nostr protocol. This node allows you to connect to Nostr relays, publish events, and subscribe to events in the Nostr network.
44

5-
## ⚠️ Security Warning
5+
## ⚠️ Security Note
66

7-
**IMPORTANT**: When using this plugin to post to Nostr relays, you will need to provide a private key.
7+
A private key is **only required** if you want to:
8+
- Publish events to relays
9+
- Send encrypted direct messages
10+
- Perform any action that requires signing
811

9-
**DO NOT USE YOUR MAIN NOSTR PRIVATE KEY!** Instead:
12+
For just listening to relays or subscribing to events, **no private key is needed**.
13+
14+
If you do need to publish events, follow these security guidelines:
1015

1116
1. Generate a separate key pair specifically for your Node-RED automation using services like:
1217
- [nsec.app](https://nsec.app/)
@@ -83,6 +88,47 @@ Specialized node for event filtering:
8388
- Filter by tags
8489
- Custom filter combinations
8590

91+
## Example Flows
92+
93+
The package includes several example flows that demonstrate common use cases:
94+
95+
### 1. Monitor Jack's Posts
96+
A flow that monitors Jack Dorsey's Nostr posts in real-time:
97+
```json
98+
{
99+
"name": "Monitor Jack's Posts",
100+
"nodes": [
101+
{
102+
"id": "relay-config",
103+
"type": "nostr-relay-config",
104+
"name": "Main Relays",
105+
"relay1": "wss://relay.damus.io",
106+
"relay2": "wss://nos.lol",
107+
"relay3": "wss://relay.nostr.band",
108+
"pingInterval": 30
109+
},
110+
{
111+
"id": "jack-monitor",
112+
"type": "nostr-relay",
113+
"name": "Jack's Posts",
114+
"relayConfig": "relay-config",
115+
"npub": "npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8yz5tc68qysh7j4xz",
116+
"eventKinds": [1]
117+
}
118+
]
119+
}
120+
```
121+
122+
To import this flow:
123+
1. Open Node-RED
124+
2. Click the menu (≡) button
125+
3. Select Import → Examples → node-red-contrib-nostr
126+
4. Choose "Monitor Jack's Posts"
127+
128+
Other example flows include:
129+
- Basic Relay Connection: Simple example of connecting to a Nostr relay
130+
- Multi-User Monitor: Monitor multiple Nostr users simultaneously
131+
86132
## Usage
87133

88134
### Relay Configuration

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
"lint": "eslint . --ext .ts",
1515
"lint:fix": "eslint . --ext .ts --fix",
1616
"prepublishOnly": "npm run lint && npm run test && npm run build",
17-
"postbuild": "for dir in nostr-filter nostr-relay nostr-relay-config; do mkdir -p dist/nodes/$dir && cp src/nodes/$dir/$dir.html dist/nodes/$dir/; done && cp -r src/nodes/*/icons dist/nodes/ && cp -r locales dist/ && cp package.json dist/"
17+
"postbuild": "for dir in nostr-filter nostr-relay nostr-relay-config; do mkdir -p dist/nodes/$dir && cp src/nodes/$dir/$dir.html dist/nodes/$dir/; done && cp -r src/nodes/*/icons dist/nodes/ && cp -r locales dist/ && cp -r examples dist/ && cp package.json dist/"
1818
},
1919
"main": "dist/nodes/index.js",
2020
"files": [
2121
"dist/**/*.js",
2222
"dist/**/*.d.ts",
2323
"dist/**/*.html",
2424
"dist/**/icons/*",
25-
"locales/**/*"
25+
"locales/**/*",
26+
"examples/**/*"
2627
],
2728
"dependencies": {
2829
"@noble/hashes": "^1.3.2",
@@ -31,7 +32,6 @@
3132
"debug": "^4.3.4",
3233
"express": "^4.18.2",
3334
"nostr-tools": "github:HumanjavaEnterprises/nostr-tools",
34-
"nostr-websocket-utils": "^0.2.5",
3535
"pino": "^8.16.2",
3636
"when": "^3.7.8",
3737
"ws": "^8.16.0"
@@ -55,6 +55,11 @@
5555
"nostr-relay": "dist/nodes/nostr-relay/nostr-relay.js",
5656
"nostr-relay-config": "dist/nodes/nostr-relay-config/nostr-relay-config.js",
5757
"nostr-filter": "dist/nodes/nostr-filter/nostr-filter.js"
58+
},
59+
"examples": {
60+
"Monitor Jack's Posts": "examples/jack-monitor.json",
61+
"Basic Relay": "examples/basic-relay.json",
62+
"Multi-User Monitor": "examples/multi-user-monitor.json"
5863
}
5964
}
6065
}

src/crypto/keys.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,59 @@
11
import * as secp256k1 from '@noble/secp256k1';
2+
import { bech32 } from 'bech32';
23
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
34
import { sha256 } from '@noble/hashes/sha256';
45

6+
// Default read-only key pair for basic operations
7+
// This is a dedicated key for node-red-contrib-nostr that's only used for reading events
8+
export const DEFAULT_READER_KEYS = {
9+
privateKey: '5acf32b3374c8c0aa6e483e0f7c6ba8c4b2d2f35d1d5854f08c8c9555d81903b',
10+
publicKey: '04dbc5c6c357e5f33e19c89a2c0b2c1c41f7b15cc3e8f6a63f5e0e8c5d5c5c5c',
11+
};
12+
13+
/**
14+
* Generate a new Nostr key pair
15+
* @returns {Object} Object containing private and public keys
16+
*/
17+
export function generateKeyPair() {
18+
const privateKey = secp256k1.utils.randomPrivateKey();
19+
const publicKey = secp256k1.getPublicKey(privateKey, true);
20+
21+
return {
22+
privateKey: Buffer.from(privateKey).toString('hex'),
23+
publicKey: Buffer.from(publicKey).toString('hex')
24+
};
25+
}
26+
27+
/**
28+
* Convert a hex public key to npub format
29+
* @param {string} publicKeyHex - Public key in hex format
30+
* @returns {string} npub formatted public key
31+
*/
32+
export function hexToNpub(publicKeyHex: string): string {
33+
const words = bech32.toWords(Buffer.from(publicKeyHex, 'hex'));
34+
return bech32.encode('npub', words);
35+
}
36+
37+
/**
38+
* Convert an npub to hex format
39+
* @param {string} npub - npub formatted public key
40+
* @returns {string} Public key in hex format
41+
*/
42+
export function npubToHex(npub: string): string {
43+
const { words } = bech32.decode(npub);
44+
return Buffer.from(bech32.fromWords(words)).toString('hex');
45+
}
46+
47+
/**
48+
* Get public key from private key
49+
* @param {string} privateKeyHex - Private key in hex format
50+
* @returns {string} Public key in hex format
51+
*/
52+
export function getPublicKey(privateKeyHex: string): string {
53+
const publicKey = secp256k1.getPublicKey(privateKeyHex, true);
54+
return Buffer.from(publicKey).toString('hex');
55+
}
56+
557
export class KeyManager {
658
static generatePrivateKey(): string {
759
const privateKey = secp256k1.utils.randomPrivateKey();

src/nodes/nostr-relay-config/nostr-relay-config.html

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@
88
<input type="text" id="node-config-input-relay" placeholder="wss://relay.damus.io">
99
</div>
1010
<div class="form-row">
11+
<label for="node-config-input-mode"><i class="fa fa-cog"></i> Mode</label>
12+
<select id="node-config-input-mode">
13+
<option value="read-only">Read Only</option>
14+
<option value="read-write">Read & Write</option>
15+
</select>
16+
</div>
17+
<div class="form-row" id="private-key-row">
1118
<label for="node-config-input-privateKey"><i class="fa fa-lock"></i> Private Key</label>
1219
<input type="password" id="node-config-input-privateKey">
20+
<div class="form-tips">Private key is only required for publishing events.</div>
1321
</div>
1422
</script>
1523

@@ -18,13 +26,31 @@
1826
category: 'config',
1927
defaults: {
2028
name: {value:""},
21-
relay: {value:"wss://relay.damus.io", required:true}
29+
relay: {value:"wss://relay.damus.io", required:true},
30+
mode: {value:"read-only"}
2231
},
2332
credentials: {
2433
privateKey: {type: "password"}
2534
},
2635
label: function() {
2736
return this.name || this.relay;
37+
},
38+
oneditprepare: function() {
39+
$("#node-config-input-mode").on("change", function() {
40+
var mode = $(this).val();
41+
if (mode === "read-write") {
42+
$("#private-key-row").show();
43+
} else {
44+
$("#private-key-row").hide();
45+
}
46+
});
47+
48+
// Initial state
49+
if (this.mode === "read-write") {
50+
$("#private-key-row").show();
51+
} else {
52+
$("#private-key-row").hide();
53+
}
2854
}
2955
});
3056
</script>
Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { Node, NodeDef } from 'node-red';
22
import WebSocket from 'ws';
3-
import * as crypto from 'crypto';
3+
import { DEFAULT_READER_KEYS, getPublicKey } from '../../crypto/keys';
44

55
export interface NostrRelayConfigCredentials {
66
privateKey?: string;
77
}
88

99
export interface NostrRelayConfigDef extends NodeDef {
1010
relay: string;
11+
mode: 'read-only' | 'read-write';
1112
}
1213

1314
export interface NostrRelayConfig extends Node {
1415
relay: string;
16+
mode: 'read-only' | 'read-write';
1517
credentials: NostrRelayConfigCredentials;
16-
publicKey?: string;
18+
publicKey: string;
1719
_ws?: WebSocket;
1820
_reconnectTimeout?: NodeJS.Timeout;
1921
_pingInterval?: NodeJS.Timeout;
@@ -24,18 +26,23 @@ module.exports = function(RED: any) {
2426
RED.nodes.createNode(this, config);
2527

2628
this.relay = config.relay;
29+
this.mode = config.mode || 'read-only';
2730

28-
// If we have a private key, derive the public key
29-
if (this.credentials.privateKey) {
31+
// Set up keys based on mode
32+
if (this.mode === 'read-write') {
33+
if (!this.credentials.privateKey) {
34+
this.error("Private key required for read-write mode");
35+
return;
36+
}
3037
try {
31-
// For now, just store a hash of the private key as the public key
32-
// In production, we would use proper key derivation
33-
this.publicKey = crypto.createHash('sha256')
34-
.update(this.credentials.privateKey)
35-
.digest('hex');
38+
this.publicKey = getPublicKey(this.credentials.privateKey);
3639
} catch (err: any) {
3740
this.error("Invalid private key: " + err.message);
41+
return;
3842
}
43+
} else {
44+
// Use default read-only key
45+
this.publicKey = DEFAULT_READER_KEYS.publicKey;
3946
}
4047

4148
// Initialize WebSocket connection
@@ -50,10 +57,10 @@ module.exports = function(RED: any) {
5057
this._ws = new WebSocket(this.relay);
5158

5259
this._ws.on('open', () => {
53-
this.log(`Connected to ${this.relay}`);
60+
this.status({fill:"green",shape:"dot",text:"connected"});
5461
reconnectAttempts = 0;
5562

56-
// Setup ping interval
63+
// Start ping interval
5764
if (this._pingInterval) {
5865
clearInterval(this._pingInterval);
5966
}
@@ -65,7 +72,13 @@ module.exports = function(RED: any) {
6572
});
6673

6774
this._ws.on('close', () => {
68-
this.log(`Disconnected from ${this.relay}`);
75+
this.status({fill:"red",shape:"ring",text:"disconnected"});
76+
77+
// Clear ping interval
78+
if (this._pingInterval) {
79+
clearInterval(this._pingInterval);
80+
}
81+
6982
// Exponential backoff for reconnection
7083
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
7184
reconnectAttempts++;
@@ -76,22 +89,21 @@ module.exports = function(RED: any) {
7689
this._reconnectTimeout = setTimeout(connect, delay);
7790
});
7891

79-
this._ws.on('error', (err: Error) => {
80-
this.error(`WebSocket error: ${err.message}`);
92+
this._ws.on('error', (err) => {
93+
this.error("WebSocket error: " + err.message);
8194
});
8295
};
8396

8497
// Initial connection
8598
connect();
8699

87-
// Cleanup on node removal
88100
this.on('close', (done: () => void) => {
89-
if (this._pingInterval) {
90-
clearInterval(this._pingInterval);
91-
}
92101
if (this._reconnectTimeout) {
93102
clearTimeout(this._reconnectTimeout);
94103
}
104+
if (this._pingInterval) {
105+
clearInterval(this._pingInterval);
106+
}
95107
if (this._ws) {
96108
this._ws.close();
97109
}
@@ -101,7 +113,7 @@ module.exports = function(RED: any) {
101113

102114
RED.nodes.registerType("nostr-relay-config", NostrRelayConfigNode, {
103115
credentials: {
104-
privateKey: { type: "password" }
116+
privateKey: {type: "password"}
105117
}
106118
});
107-
};
119+
}

0 commit comments

Comments
 (0)