@openclaw/nostr 2026.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ ## 2026.1.29
4
+
5
+ ### Changes
6
+ - Version alignment with core OpenClaw release numbers.
7
+
8
+ ## 2026.1.23
9
+
10
+ ### Changes
11
+ - Version alignment with core OpenClaw release numbers.
12
+
13
+ ## 2026.1.22
14
+
15
+ ### Changes
16
+ - Version alignment with core OpenClaw release numbers.
17
+
18
+ ## 2026.1.21
19
+
20
+ ### Changes
21
+ - Version alignment with core OpenClaw release numbers.
22
+
23
+ ## 2026.1.20
24
+
25
+ ### Changes
26
+ - Version alignment with core OpenClaw release numbers.
27
+
28
+ ## 2026.1.19-1
29
+
30
+ Initial release.
31
+
32
+ ### Features
33
+
34
+ - NIP-04 encrypted DM support (kind:4 events)
35
+ - Key validation (hex and nsec formats)
36
+ - Multi-relay support with sequential fallback
37
+ - Event signature verification
38
+ - TTL-based deduplication (24h)
39
+ - Access control via dmPolicy (pairing, allowlist, open, disabled)
40
+ - Pubkey normalization (hex/npub)
41
+
42
+ ### Protocol Support
43
+
44
+ - NIP-01: Basic event structure
45
+ - NIP-04: Encrypted direct messages
46
+
47
+ ### Planned for v2
48
+
49
+ - NIP-17: Gift-wrapped DMs
50
+ - NIP-44: Versioned encryption
51
+ - Media attachments
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @openclaw/nostr
2
+
3
+ Nostr DM channel plugin for OpenClaw using NIP-04 encrypted direct messages.
4
+
5
+ ## Overview
6
+
7
+ This extension adds Nostr as a messaging channel to OpenClaw. It enables your bot to:
8
+
9
+ - Receive encrypted DMs from Nostr users
10
+ - Send encrypted responses back
11
+ - Work with any NIP-04 compatible Nostr client (Damus, Amethyst, etc.)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ openclaw plugins install @openclaw/nostr
17
+ ```
18
+
19
+ ## Quick Setup
20
+
21
+ 1. Generate a Nostr keypair (if you don't have one):
22
+ ```bash
23
+ # Using nak CLI
24
+ nak key generate
25
+
26
+ # Or use any Nostr key generator
27
+ ```
28
+
29
+ 2. Add to your config:
30
+ ```json
31
+ {
32
+ "channels": {
33
+ "nostr": {
34
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
35
+ "relays": ["wss://relay.damus.io", "wss://nos.lol"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ 3. Set the environment variable:
42
+ ```bash
43
+ export NOSTR_PRIVATE_KEY="nsec1..." # or hex format
44
+ ```
45
+
46
+ 4. Restart the gateway
47
+
48
+ ## Configuration
49
+
50
+ | Key | Type | Default | Description |
51
+ |-----|------|---------|-------------|
52
+ | `privateKey` | string | required | Bot's private key (nsec or hex format) |
53
+ | `relays` | string[] | `["wss://relay.damus.io", "wss://nos.lol"]` | WebSocket relay URLs |
54
+ | `dmPolicy` | string | `"pairing"` | Access control: `pairing`, `allowlist`, `open`, `disabled` |
55
+ | `allowFrom` | string[] | `[]` | Allowed sender pubkeys (npub or hex) |
56
+ | `enabled` | boolean | `true` | Enable/disable the channel |
57
+ | `name` | string | - | Display name for the account |
58
+
59
+ ## Access Control
60
+
61
+ ### DM Policies
62
+
63
+ - **pairing** (default): Unknown senders receive a pairing code to request access
64
+ - **allowlist**: Only pubkeys in `allowFrom` can message the bot
65
+ - **open**: Anyone can message the bot (use with caution)
66
+ - **disabled**: DMs are disabled
67
+
68
+ ### Example: Allowlist Mode
69
+
70
+ ```json
71
+ {
72
+ "channels": {
73
+ "nostr": {
74
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
75
+ "dmPolicy": "allowlist",
76
+ "allowFrom": [
77
+ "npub1abc...",
78
+ "0123456789abcdef..."
79
+ ]
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## Testing
86
+
87
+ ### Local Relay (Recommended)
88
+
89
+ ```bash
90
+ # Using strfry
91
+ docker run -p 7777:7777 ghcr.io/hoytech/strfry
92
+
93
+ # Configure openclaw to use local relay
94
+ "relays": ["ws://localhost:7777"]
95
+ ```
96
+
97
+ ### Manual Test
98
+
99
+ 1. Start the gateway with Nostr configured
100
+ 2. Open Damus, Amethyst, or another Nostr client
101
+ 3. Send a DM to your bot's npub
102
+ 4. Verify the bot responds
103
+
104
+ ## Protocol Support
105
+
106
+ | NIP | Status | Notes |
107
+ |-----|--------|-------|
108
+ | NIP-01 | Supported | Basic event structure |
109
+ | NIP-04 | Supported | Encrypted DMs (kind:4) |
110
+ | NIP-17 | Planned | Gift-wrapped DMs (v2) |
111
+
112
+ ## Security Notes
113
+
114
+ - Private keys are never logged
115
+ - Event signatures are verified before processing
116
+ - Use environment variables for keys, never commit to config files
117
+ - Consider using `allowlist` mode in production
118
+
119
+ ## Troubleshooting
120
+
121
+ ### Bot not receiving messages
122
+
123
+ 1. Verify private key is correctly configured
124
+ 2. Check relay connectivity
125
+ 3. Ensure `enabled` is not set to `false`
126
+ 4. Check the bot's public key matches what you're sending to
127
+
128
+ ### Messages not being delivered
129
+
130
+ 1. Check relay URLs are correct (must use `wss://`)
131
+ 2. Verify relays are online and accepting connections
132
+ 3. Check for rate limiting (reduce message frequency)
133
+
134
+ ## License
135
+
136
+ MIT
package/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+
4
+ import { nostrPlugin } from "./src/channel.js";
5
+ import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js";
6
+ import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
7
+ import { resolveNostrAccount } from "./src/types.js";
8
+ import type { NostrProfile } from "./src/config-schema.js";
9
+
10
+ const plugin = {
11
+ id: "nostr",
12
+ name: "Nostr",
13
+ description: "Nostr DM channel plugin via NIP-04",
14
+ configSchema: emptyPluginConfigSchema(),
15
+ register(api: OpenClawPluginApi) {
16
+ setNostrRuntime(api.runtime);
17
+ api.registerChannel({ plugin: nostrPlugin });
18
+
19
+ // Register HTTP handler for profile management
20
+ const httpHandler = createNostrProfileHttpHandler({
21
+ getConfigProfile: (accountId: string) => {
22
+ const runtime = getNostrRuntime();
23
+ const cfg = runtime.config.loadConfig() as OpenClawConfig;
24
+ const account = resolveNostrAccount({ cfg, accountId });
25
+ return account.profile;
26
+ },
27
+ updateConfigProfile: async (accountId: string, profile: NostrProfile) => {
28
+ const runtime = getNostrRuntime();
29
+ const cfg = runtime.config.loadConfig() as OpenClawConfig;
30
+
31
+ // Build the config patch for channels.nostr.profile
32
+ const channels = (cfg.channels ?? {}) as Record<string, unknown>;
33
+ const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
34
+
35
+ const updatedNostrConfig = {
36
+ ...nostrConfig,
37
+ profile,
38
+ };
39
+
40
+ const updatedChannels = {
41
+ ...channels,
42
+ nostr: updatedNostrConfig,
43
+ };
44
+
45
+ await runtime.config.writeConfigFile({
46
+ ...cfg,
47
+ channels: updatedChannels,
48
+ });
49
+ },
50
+ getAccountInfo: (accountId: string) => {
51
+ const runtime = getNostrRuntime();
52
+ const cfg = runtime.config.loadConfig() as OpenClawConfig;
53
+ const account = resolveNostrAccount({ cfg, accountId });
54
+ if (!account.configured || !account.publicKey) {
55
+ return null;
56
+ }
57
+ return {
58
+ pubkey: account.publicKey,
59
+ relays: account.relays,
60
+ };
61
+ },
62
+ log: api.logger,
63
+ });
64
+
65
+ api.registerHttpHandler(httpHandler);
66
+ },
67
+ };
68
+
69
+ export default plugin;
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "nostr",
3
+ "channels": [
4
+ "nostr"
5
+ ],
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {}
10
+ }
11
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@openclaw/nostr",
3
+ "version": "2026.1.29",
4
+ "type": "module",
5
+ "description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
6
+ "openclaw": {
7
+ "extensions": [
8
+ "./index.ts"
9
+ ],
10
+ "channel": {
11
+ "id": "nostr",
12
+ "label": "Nostr",
13
+ "selectionLabel": "Nostr (NIP-04 DMs)",
14
+ "docsPath": "/channels/nostr",
15
+ "docsLabel": "nostr",
16
+ "blurb": "Decentralized protocol; encrypted DMs via NIP-04.",
17
+ "order": 55,
18
+ "quickstartAllowFrom": true
19
+ },
20
+ "install": {
21
+ "npmSpec": "@openclaw/nostr",
22
+ "localPath": "extensions/nostr",
23
+ "defaultChoice": "npm"
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "openclaw": "workspace:*",
28
+ "nostr-tools": "^2.20.0",
29
+ "zod": "^4.3.6"
30
+ }
31
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nostrPlugin } from "./channel.js";
3
+
4
+ describe("nostrPlugin", () => {
5
+ describe("meta", () => {
6
+ it("has correct id", () => {
7
+ expect(nostrPlugin.id).toBe("nostr");
8
+ });
9
+
10
+ it("has required meta fields", () => {
11
+ expect(nostrPlugin.meta.label).toBe("Nostr");
12
+ expect(nostrPlugin.meta.docsPath).toBe("/channels/nostr");
13
+ expect(nostrPlugin.meta.blurb).toContain("NIP-04");
14
+ });
15
+ });
16
+
17
+ describe("capabilities", () => {
18
+ it("supports direct messages", () => {
19
+ expect(nostrPlugin.capabilities.chatTypes).toContain("direct");
20
+ });
21
+
22
+ it("does not support groups (MVP)", () => {
23
+ expect(nostrPlugin.capabilities.chatTypes).not.toContain("group");
24
+ });
25
+
26
+ it("does not support media (MVP)", () => {
27
+ expect(nostrPlugin.capabilities.media).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe("config adapter", () => {
32
+ it("has required config functions", () => {
33
+ expect(nostrPlugin.config.listAccountIds).toBeTypeOf("function");
34
+ expect(nostrPlugin.config.resolveAccount).toBeTypeOf("function");
35
+ expect(nostrPlugin.config.isConfigured).toBeTypeOf("function");
36
+ });
37
+
38
+ it("listAccountIds returns empty array for unconfigured", () => {
39
+ const cfg = { channels: {} };
40
+ const ids = nostrPlugin.config.listAccountIds(cfg);
41
+ expect(ids).toEqual([]);
42
+ });
43
+
44
+ it("listAccountIds returns default for configured", () => {
45
+ const cfg = {
46
+ channels: {
47
+ nostr: {
48
+ privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
49
+ },
50
+ },
51
+ };
52
+ const ids = nostrPlugin.config.listAccountIds(cfg);
53
+ expect(ids).toContain("default");
54
+ });
55
+ });
56
+
57
+ describe("messaging", () => {
58
+ it("has target resolver", () => {
59
+ expect(nostrPlugin.messaging?.targetResolver?.looksLikeId).toBeTypeOf("function");
60
+ });
61
+
62
+ it("recognizes npub as valid target", () => {
63
+ const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
64
+ if (!looksLikeId) return;
65
+
66
+ expect(looksLikeId("npub1xyz123")).toBe(true);
67
+ });
68
+
69
+ it("recognizes hex pubkey as valid target", () => {
70
+ const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
71
+ if (!looksLikeId) return;
72
+
73
+ const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
74
+ expect(looksLikeId(hexPubkey)).toBe(true);
75
+ });
76
+
77
+ it("rejects invalid input", () => {
78
+ const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
79
+ if (!looksLikeId) return;
80
+
81
+ expect(looksLikeId("not-a-pubkey")).toBe(false);
82
+ expect(looksLikeId("")).toBe(false);
83
+ });
84
+
85
+ it("normalizeTarget strips nostr: prefix", () => {
86
+ const normalize = nostrPlugin.messaging?.normalizeTarget;
87
+ if (!normalize) return;
88
+
89
+ const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
90
+ expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey);
91
+ });
92
+ });
93
+
94
+ describe("outbound", () => {
95
+ it("has correct delivery mode", () => {
96
+ expect(nostrPlugin.outbound?.deliveryMode).toBe("direct");
97
+ });
98
+
99
+ it("has reasonable text chunk limit", () => {
100
+ expect(nostrPlugin.outbound?.textChunkLimit).toBe(4000);
101
+ });
102
+ });
103
+
104
+ describe("pairing", () => {
105
+ it("has id label for pairing", () => {
106
+ expect(nostrPlugin.pairing?.idLabel).toBe("nostrPubkey");
107
+ });
108
+
109
+ it("normalizes nostr: prefix in allow entries", () => {
110
+ const normalize = nostrPlugin.pairing?.normalizeAllowEntry;
111
+ if (!normalize) return;
112
+
113
+ const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
114
+ expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey);
115
+ });
116
+ });
117
+
118
+ describe("security", () => {
119
+ it("has resolveDmPolicy function", () => {
120
+ expect(nostrPlugin.security?.resolveDmPolicy).toBeTypeOf("function");
121
+ });
122
+ });
123
+
124
+ describe("gateway", () => {
125
+ it("has startAccount function", () => {
126
+ expect(nostrPlugin.gateway?.startAccount).toBeTypeOf("function");
127
+ });
128
+ });
129
+
130
+ describe("status", () => {
131
+ it("has default runtime", () => {
132
+ expect(nostrPlugin.status?.defaultRuntime).toBeDefined();
133
+ expect(nostrPlugin.status?.defaultRuntime?.accountId).toBe("default");
134
+ expect(nostrPlugin.status?.defaultRuntime?.running).toBe(false);
135
+ });
136
+
137
+ it("has buildAccountSnapshot function", () => {
138
+ expect(nostrPlugin.status?.buildAccountSnapshot).toBeTypeOf("function");
139
+ });
140
+ });
141
+ });