openclaw-nostr 2026.7.30

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +845 -0
  3. package/api.ts +6 -0
  4. package/index.ts +69 -0
  5. package/node_modules/cascadia-ts/cascadia.ts +1187 -0
  6. package/node_modules/cascadia-ts/dist/cascadia.d.ts +607 -0
  7. package/node_modules/cascadia-ts/dist/cascadia.js +705 -0
  8. package/node_modules/cascadia-ts/package.json +21 -0
  9. package/openclaw.plugin.json +153 -0
  10. package/package.json +81 -0
  11. package/runtime-api.ts +12 -0
  12. package/setup-api.ts +1 -0
  13. package/setup-entry.ts +4 -0
  14. package/src/channel-actions.test.ts +304 -0
  15. package/src/channel-actions.ts +199 -0
  16. package/src/channel.dm-access.test.ts +387 -0
  17. package/src/channel.inbound.test.ts +476 -0
  18. package/src/channel.lifecycle.test.ts +768 -0
  19. package/src/channel.outbound.test.ts +1143 -0
  20. package/src/channel.test.ts +175 -0
  21. package/src/channel.ts +2827 -0
  22. package/src/config-schema.test.ts +486 -0
  23. package/src/config-schema.ts +356 -0
  24. package/src/default-relays.ts +1 -0
  25. package/src/fleet-agent.test.ts +423 -0
  26. package/src/fleet-agent.ts +549 -0
  27. package/src/metrics.ts +466 -0
  28. package/src/nip46-cutover.test.ts +324 -0
  29. package/src/nip46-cutover.ts +461 -0
  30. package/src/nip46-doctor.test.ts +191 -0
  31. package/src/nip46-doctor.ts +329 -0
  32. package/src/nip46-enroll.test.ts +346 -0
  33. package/src/nip46-enroll.ts +567 -0
  34. package/src/nip46-signer.test.ts +809 -0
  35. package/src/nip46-signer.ts +1263 -0
  36. package/src/nip46-status.test.ts +186 -0
  37. package/src/nip46-status.ts +250 -0
  38. package/src/nip51-lists.test.ts +404 -0
  39. package/src/nip51-lists.ts +777 -0
  40. package/src/nostr-access.test.ts +175 -0
  41. package/src/nostr-access.ts +134 -0
  42. package/src/nostr-bus-nip29.ts +998 -0
  43. package/src/nostr-bus-relay.ts +477 -0
  44. package/src/nostr-bus-types.ts +392 -0
  45. package/src/nostr-bus.fuzz.test.ts +533 -0
  46. package/src/nostr-bus.integration.test.ts +505 -0
  47. package/src/nostr-bus.protocol.test.ts +2057 -0
  48. package/src/nostr-bus.test.ts +235 -0
  49. package/src/nostr-bus.ts +1868 -0
  50. package/src/nostr-capabilities-extended.test.ts +612 -0
  51. package/src/nostr-capabilities.ts +719 -0
  52. package/src/nostr-discovery.test.ts +568 -0
  53. package/src/nostr-discovery.ts +336 -0
  54. package/src/nostr-extras.test.ts +88 -0
  55. package/src/nostr-extras.ts +136 -0
  56. package/src/nostr-profile-http.test.ts +662 -0
  57. package/src/nostr-profile-http.ts +836 -0
  58. package/src/nostr-profile-import.test.ts +274 -0
  59. package/src/nostr-profile-import.ts +295 -0
  60. package/src/nostr-profile.fuzz.test.ts +480 -0
  61. package/src/nostr-profile.test.ts +410 -0
  62. package/src/nostr-profile.ts +320 -0
  63. package/src/nostr-pubkey.ts +56 -0
  64. package/src/nostr-state-store.test.ts +790 -0
  65. package/src/nostr-state-store.ts +1284 -0
  66. package/src/runtime.ts +9 -0
  67. package/src/seen-tracker.test.ts +25 -0
  68. package/src/seen-tracker.ts +292 -0
  69. package/src/session-route.ts +25 -0
  70. package/src/setup-surface.ts +320 -0
  71. package/src/types.test.ts +783 -0
  72. package/src/types.ts +508 -0
package/README.md ADDED
@@ -0,0 +1,845 @@
1
+ # openclaw-nostr
2
+
3
+ Nostr channel plugin for OpenClaw — encrypted DMs, public notes, identity resolution, relay discovery, public channels, and 20+ NIP implementations.
4
+
5
+ ## Overview
6
+
7
+ This extension adds Nostr as a full-featured messaging and social channel to OpenClaw. It enables your agent to:
8
+
9
+ - Receive and send encrypted DMs via NIP-04 and NIP-17 (gift-wrapped)
10
+ - Publish and react to public notes (kind:1)
11
+ - Resolve NIP-05 identities and discover relay capabilities
12
+ - Create and moderate public channels (NIP-28)
13
+ - Repost content, publish file metadata, and generate zap requests
14
+ - Encrypt messages with NIP-44 versioned encryption
15
+ - Authenticate to relays (NIP-42) and HTTP services (NIP-98)
16
+ - Delegate signing to a remote NIP-46 bunker (no local private key needed)
17
+ - Access 20+ additional NIP implementations from `nostr-tools`
18
+
19
+ ## Installation
20
+
21
+ ### Normal install
22
+
23
+ ```bash
24
+ openclaw plugins install npm:openclaw-nostr
25
+ ```
26
+
27
+ `openclaw-nostr` is a runtime-only plugin artifact. It contains no bootstrap
28
+ scripts or process-spawning code, so OpenClaw's install-time security scanner can
29
+ scan and install it normally. The OpenClaw plugin id remains `nostr`.
30
+
31
+ ### Optional legacy migration
32
+
33
+ If you previously installed the broken `@openclaw/nostr`, the old combined
34
+ `nostr-claw-bootstrap` package, or a direct-copy override, run:
35
+
36
+ ```bash
37
+ npx openclaw-nostr-bootstrap
38
+ ```
39
+
40
+ The migration CLI:
41
+
42
+ 1. detects the host OpenClaw version
43
+ 2. removes recognized stale direct-copy/activation installs and broken managed
44
+ npm entries left by prior packages
45
+ 3. runs `openclaw plugins install npm:openclaw-nostr --force`
46
+ 4. enables the plugin in `openclaw.json`
47
+ 5. refreshes the persisted plugin registry (via the install)
48
+ 6. runs a best-effort plugin-graph smoke test
49
+
50
+ For an offline migration, install from a local plugin checkout with
51
+ `--plugin-source`:
52
+
53
+ ```bash
54
+ npx openclaw-nostr-bootstrap --plugin-source /path/to/openclaw-nostr
55
+ ```
56
+
57
+ With an explicit OpenClaw checkout / CLI path:
58
+
59
+ ```bash
60
+ npx openclaw-nostr-bootstrap --openclaw /path/to/openclaw.mjs
61
+ ```
62
+
63
+ For machine-readable output:
64
+
65
+ ```bash
66
+ npx openclaw-nostr-bootstrap --json
67
+ ```
68
+
69
+ ### From this repo (Cascadia fork)
70
+
71
+ ```bash
72
+ git clone https://git.sharegap.net/cascadia/openclaw-nostr.git
73
+ ```
74
+
75
+ See [Docker Deployment](#docker-deployment) for containerized setups.
76
+
77
+ ## Cascadia Fleet Role
78
+
79
+ This repo is the **durable maintenance layer** for Cascadia's Nostr fixes when upstream OpenClaw upgrades clobber local agent installs.
80
+
81
+ The repository now publishes two deliberately separate packages:
82
+
83
+ - `openclaw-nostr`: the runtime-only OpenClaw plugin and normal install artifact
84
+ - `openclaw-nostr-bootstrap`: an optional migration CLI for legacy installs
85
+
86
+ Upstream OpenClaw remains the base runtime, while this repo is the source of
87
+ truth for Nostr-specific durability patches and extended features.
88
+
89
+ See:
90
+ - `docs/UPGRADE-WORKFLOW.md`
91
+ - `docs/COMPATIBILITY.md`
92
+ - `scripts/apply-to-agent.sh`
93
+ - `scripts/check-agent.sh`
94
+
95
+ ## Publishing (maintainers)
96
+
97
+ Two independent npm packages are published from this repo. They share a version
98
+ but are released separately.
99
+
100
+ **1. The plugin — `openclaw-nostr`** (runtime-only artifact users install):
101
+
102
+ ```bash
103
+ # from the repo root
104
+ npm login # once, if not already authenticated
105
+ npm publish --dry-run # inspect the tarball: no scripts/, cascadia-ts bundled, 0 child_process
106
+ npm publish --access public # publish openclaw-nostr@<version>
107
+ ```
108
+
109
+ The published tarball is runtime-only (`files[]` excludes `scripts/`) and bundles
110
+ `cascadia-ts` via `bundledDependencies`, so it installs and passes OpenClaw's
111
+ install-time security scanner cleanly. Verify before publishing:
112
+
113
+ ```bash
114
+ npm pack # -> openclaw-nostr-<version>.tgz
115
+ npm run test:scanner # asserts 0 critical scanner findings
116
+ # optional clean-room install check:
117
+ mkdir /tmp/on-verify && cd /tmp/on-verify && npm init -y >/dev/null
118
+ npm install /path/to/openclaw-nostr-<version>.tgz # must resolve cascadia-ts from the bundle
119
+ ```
120
+
121
+ **2. The migration CLI — `openclaw-nostr-bootstrap`** (optional, `npx`-run):
122
+
123
+ ```bash
124
+ cd packages/openclaw-nostr-bootstrap
125
+ npm publish --dry-run
126
+ npm publish --access public # publish openclaw-nostr-bootstrap@<version>
127
+ ```
128
+
129
+ This package intentionally ships `scripts/` (which spawn `child_process`). That is
130
+ fine — it is an `npx` CLI, never installed as an OpenClaw plugin, so the plugin
131
+ scanner never runs against it.
132
+
133
+ > Publishing to a private/self-hosted registry instead of public npm: add
134
+ > `--registry https://<registry>` to each `npm publish`, or set
135
+ > `publishConfig.registry` in the respective `package.json`.
136
+
137
+ Bump the version in **both** `package.json` files before a coordinated release
138
+ (the plugin at the repo root and `packages/openclaw-nostr-bootstrap/package.json`).
139
+
140
+ ## Quick Setup
141
+
142
+ 1. Generate a Nostr keypair (if you don't have one):
143
+
144
+ ```bash
145
+ # Using nak CLI
146
+ nak key generate
147
+
148
+ # Or use any Nostr key generator
149
+ ```
150
+
151
+ 2. Add to your config (`~/.openclaw/openclaw.json`):
152
+
153
+ ```json
154
+ {
155
+ "channels": {
156
+ "nostr": {
157
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
158
+ "relays": ["wss://relay.sharegap.net", "wss://nos.lol"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ 3. Set the environment variable:
165
+
166
+ ```bash
167
+ export NOSTR_PRIVATE_KEY="nsec1..." # or 64-char hex format
168
+ ```
169
+
170
+ 4. Restart the gateway
171
+
172
+ ## Configuration
173
+
174
+ | Key | Type | Default | Description |
175
+ | ------------ | -------- | ------------------------------------------- | ---------------------------------------------------------- |
176
+ | `privateKey` | string | required* | Bot's private key (nsec or hex). *Not required when using NIP-46. |
177
+ | `relays` | string[] | `["wss://relay.sharegap.net", "wss://nos.lol"]` | WebSocket relay URLs |
178
+ | `dmPolicy` | string | `"pairing"` | Access control: `pairing`, `allowlist`, `open`, `disabled` |
179
+ | `allowFrom` | string[] | `[]` | Allowed sender pubkeys (npub or hex) |
180
+ | `enabled` | boolean | `true` | Enable/disable the channel |
181
+ | `name` | string | - | Display name for the account |
182
+
183
+ ### NIP-46 Remote Signing
184
+
185
+ When NIP-46 is enabled, event signing and encryption/decryption are delegated to a remote signer (bunker). No private key is stored locally — only a client secret used for the encrypted communication channel.
186
+
187
+ | Key | Type | Default | Description |
188
+ | ------------------------ | -------- | --------- | ------------------------------------------------------------------ |
189
+ | `nip46` | boolean | `false` | Enable NIP-46 remote signing |
190
+ | `nip46BunkerUrl` | string | - | `bunker://` URL or `user@domain` NIP-05 identifier |
191
+ | `nip46SignerRelays` | string[] | - | Relay URLs for signer communication (defaults to bunker URL relays) |
192
+ | `nip46Secret` | string | - | Client secret key (hex) — use env var, not raw config |
193
+ | `nip46ConnectionTimeoutMs` | number | `60000` | Connection timeout for the NIP-46 session |
194
+
195
+ #### NIP-46 Setup
196
+
197
+ 1. Set up a bunker signer (e.g. [nsecBunker](https://nsecbunker.com), [Amber](https://github.com/nickkurt/amber), or any NIP-46 compatible signer)
198
+
199
+ 2. Generate a client secret (a random 32-byte hex key for the encrypted channel):
200
+
201
+ ```bash
202
+ # Generate a random client secret
203
+ openssl rand -hex 32
204
+ ```
205
+
206
+ 3. Store the client secret securely as an environment variable:
207
+
208
+ ```bash
209
+ export NOSTR_NIP46_SECRET="your-64-char-hex-client-secret"
210
+ ```
211
+
212
+ 4. Configure in `openclaw.json`:
213
+
214
+ ```json
215
+ {
216
+ "channels": {
217
+ "nostr": {
218
+ "nip46": true,
219
+ "nip46BunkerUrl": "bunker://abcdef...?relay=wss://relay.nsecbunker.com",
220
+ "relays": ["wss://relay.sharegap.net", "wss://nos.lol"]
221
+ }
222
+ }
223
+ }
224
+ ```
225
+
226
+ Or with a NIP-05 bunker identifier:
227
+
228
+ ```json
229
+ {
230
+ "channels": {
231
+ "nostr": {
232
+ "nip46": true,
233
+ "nip46BunkerUrl": "user@nsecbunker.com"
234
+ }
235
+ }
236
+ }
237
+ ```
238
+
239
+ 5. Alternatively, use a `SecretRef` to point to the env var explicitly:
240
+
241
+ ```json
242
+ {
243
+ "channels": {
244
+ "nostr": {
245
+ "nip46": true,
246
+ "nip46BunkerUrl": "bunker://abcdef...?relay=wss://relay.nsecbunker.com",
247
+ "nip46Secret": { "source": "env", "provider": "default", "id": "NOSTR_NIP46_SECRET" }
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ #### Secret Sources
254
+
255
+ The `nip46Secret` field supports three `SecretRef` source types for production deployments:
256
+
257
+ | Source | Example | Description |
258
+ | ------ | ------- | ----------- |
259
+ | `env` | `{ "source": "env", "name": "NOSTR_NIP46_SECRET" }` | Read from environment variable (`id` or `name` field) |
260
+ | `file` | `{ "source": "file", "path": "/run/secrets/nip46" }` | Read from a file (Docker secrets, tmpfs, etc.) |
261
+ | `exec` | `{ "source": "exec", "command": "vault kv get -field=secret nostr/nip46" }` | Run a command, use stdout (10s timeout) |
262
+
263
+ All sources fall back to the `NOSTR_NIP46_SECRET` environment variable if the primary source fails.
264
+
265
+ #### Enrollment Flow
266
+
267
+ The plugin provides a programmatic enrollment flow (`enrollNip46Signer()`) that automates the full NIP-46 setup ceremony:
268
+
269
+ 1. Generates a client secret (or reuses an existing one)
270
+ 2. Validates and parses the bunker URL
271
+ 3. Connects to the remote signer
272
+ 4. Verifies the remote pubkey matches expectations
273
+ 5. Runs a self-test on all delegated crypto operations
274
+ 6. Produces a config patch and env var instructions
275
+
276
+ #### Cutover Migration
277
+
278
+ To migrate from local key to NIP-46 remote signing, use `performCutover()` which executes a transactional migration:
279
+
280
+ 1. Validates a local private key exists
281
+ 2. Snapshots the current config as backup
282
+ 3. Runs the enrollment flow (connect, verify, self-test)
283
+ 4. Applies the new config (enables NIP-46, removes `privateKey`)
284
+ 5. Runs post-cutover verification
285
+ 6. **Automatically rolls back** to the original config if any step fails
286
+
287
+ #### Preflight Doctor
288
+
289
+ The `runNip46Doctor()` function performs 7 diagnostic checks:
290
+
291
+ | Check | What it verifies |
292
+ | ----- | ---------------- |
293
+ | `config_valid` | Bunker URL and client secret are provided |
294
+ | `bunker_url_parsed` | URL yields a pubkey (or is valid NIP-05) |
295
+ | `client_secret_decodable` | Hex secret decodes to 32 bytes |
296
+ | `signer_connect` | BunkerSigner connects within timeout |
297
+ | `pubkey_match` | `get_public_key` matches expected identity |
298
+ | `crypto_self_test` | sign, nip44, nip04 all work end-to-end |
299
+ | `secret_recoverable` | Client secret re-decodes consistently (restart safety) |
300
+
301
+ #### What Gets Delegated
302
+
303
+ When NIP-46 is active, **all** identity-level cryptographic operations go through the remote signer:
304
+
305
+ - Event signing (DMs, notes, reactions, deletions, reposts, channel messages, group messages)
306
+ - NIP-04 encryption and decryption
307
+ - NIP-44 encryption and decryption (used in NIP-17 gift wraps)
308
+ - NIP-42 relay authentication
309
+ - NIP-98 HTTP authentication
310
+
311
+ The client secret (`nip46Secret`) is only used to establish the NIP-44 encrypted channel with the bunker — it is **not** the identity private key.
312
+
313
+ #### Startup Self-Test & Health Logging
314
+
315
+ On startup with NIP-46 enabled, the plugin:
316
+
317
+ 1. Logs a **signer health block** showing signing mode, identity pubkey, bunker URL, relay list, and session status
318
+ 2. Runs an automatic **self-test** exercising all delegated crypto operations:
319
+ - `get_public_key` — returns valid 64-char hex pubkey matching expected identity
320
+ - `sign_event` — signs a test kind:1 event
321
+ - `nip44_encrypt_decrypt` — round-trips NIP-44 encryption
322
+ - `nip04_encrypt_decrypt` — round-trips NIP-04 encryption
323
+ 3. Tracks **runtime health** (success/failure counts, last sign/decrypt/encrypt timestamps)
324
+
325
+ Failures are logged as warnings — the bus still starts, but you'll know which operations aren't working.
326
+
327
+ #### Custody Status
328
+
329
+ The `resolveCustodyStatus()` function reports the current signing posture:
330
+
331
+ - **Signing mode**: `local-key`, `nip46-remote`, or `unconfigured`
332
+ - **Key residency**: whether a local private key still exists in config
333
+ - **NIP-46 readiness**: bunker URL + secret configured
334
+ - **Runtime health**: signer success/failure counts and last operation timestamps
335
+ - **Restart safety**: whether the secret source will survive a restart
336
+ - **Warnings**: actionable alerts (e.g. "local key still present after NIP-46 migration")
337
+
338
+ #### Rate-Limit Protection
339
+
340
+ NIP-46 operations go through a **request queue** with:
341
+ - Concurrency limit (default: 2 concurrent requests)
342
+ - Stagger interval (default: 150ms between dispatches)
343
+ - Exponential backoff retry on rate-limit errors (1s → 2s → 4s, up to 3 retries)
344
+
345
+ This prevents bursts of inbound messages from overwhelming the bunker relay with rapid-fire publishes.
346
+
347
+ #### Outbound DM Safety Assertions
348
+
349
+ The NIP-17 outbound path includes runtime assertions that prevent routing bugs:
350
+ - Recipient pubkey must not equal own pubkey (no self-DM)
351
+ - Recipient pubkey must be valid 64-char hex
352
+ - Post-wrap `p` tags must match the intended recipient for each wrap
353
+ - Self-echo wraps are logged and ignored on the inbound path
354
+
355
+ #### Security Considerations
356
+
357
+ - **Never commit the client secret** to config files or version control
358
+ - The `NOSTR_NIP46_SECRET` env var is the recommended storage method
359
+ - The `SecretRef` mechanism supports `env`, `file`, and `exec` backends for production deployments
360
+ - The bunker must be online for the agent to sign events — plan for connectivity
361
+
362
+ ## Access Control
363
+
364
+ ### DM Policies
365
+
366
+ - **pairing** (default): Unknown senders receive a pairing code to request access
367
+ - **allowlist**: Only pubkeys in `allowFrom` can message the bot
368
+ - **open**: Anyone can message the bot (use with caution)
369
+ - **disabled**: DMs are disabled
370
+
371
+ ### Example: Allowlist Mode
372
+
373
+ ```json
374
+ {
375
+ "channels": {
376
+ "nostr": {
377
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
378
+ "dmPolicy": "allowlist",
379
+ "allowFrom": ["npub1abc...", "0123456789abcdef..."]
380
+ }
381
+ }
382
+ }
383
+ ```
384
+
385
+ ## Protocol Support
386
+
387
+ ### Core Messaging (Tier 0)
388
+
389
+ | NIP | Kind(s) | Status | Description |
390
+ | ------ | ------------ | ----------- | ------------------------------------- |
391
+ | NIP-01 | 1 | ✅ Full | Basic event structure & public notes |
392
+ | NIP-04 | 4 | ✅ Full | Encrypted DMs (legacy) |
393
+ | NIP-09 | 5 | ✅ Full | Event deletion |
394
+ | NIP-10 | — | ✅ Full | Thread references (root/reply/mention)|
395
+ | NIP-17 | 1059 | ✅ Full | Gift-wrapped DMs (modern) |
396
+ | NIP-25 | 7 | ✅ Full | Reactions |
397
+ | NIP-40 | — | ✅ Full | Event expiration |
398
+ | NIP-65 | 10002 | ✅ Full | Relay list metadata |
399
+
400
+ ### Tier 1 — Agent-Essential Features
401
+
402
+ | NIP | Kind(s) | Status | Description |
403
+ | ------ | ------------ | ----------- | ------------------------------------- |
404
+ | NIP-05 | — | ✅ Full | Identity resolution + domain search |
405
+ | NIP-11 | — | ✅ Full | Relay information + capability checks |
406
+ | NIP-42 | 22242 | ✅ Full | Relay authentication |
407
+ | NIP-46 | 24133 | ✅ Full | Remote signing (Nostr Connect/Bunker) |
408
+ | NIP-57 | 9734, 9735 | ✅ Re-export| Zaps (Lightning payments) |
409
+ | NIP-94 | 1063 | ✅ Full | File metadata |
410
+ | NIP-98 | 27235 | ✅ Full | HTTP authentication |
411
+ | NIP-B7 | — | ✅ Re-export| Blossom media server |
412
+
413
+ ### Tier 2 — Social & Channel Features
414
+
415
+ | NIP | Kind(s) | Status | Description |
416
+ | ------ | ------------------ | ----------- | ------------------------------- |
417
+ | NIP-13 | — | ✅ Re-export| Proof of Work |
418
+ | NIP-18 | 6, 16 | ✅ Full | Reposts (short text + generic) |
419
+ | NIP-27 | — | ✅ Full | Content parsing (text/URLs/refs)|
420
+ | NIP-28 | 40, 42, 43, 44 | ✅ Full | Public channels (CRUD + mod) |
421
+ | NIP-44 | — | ✅ Full | Versioned encryption |
422
+
423
+ ### Tier 3 — Niche / Advanced (Namespace Re-exports)
424
+
425
+ | NIP | Module | Description |
426
+ | ------ | ------------------ | ------------------------------------- |
427
+ | NIP-29 | `nostr-extras` | Relay-based groups |
428
+ | NIP-30 | `nostr-extras` | Custom emoji |
429
+ | NIP-39 | `nostr-extras` | External identity verification |
430
+ | NIP-47 | `nostr-extras` | Nostr Wallet Connect (NWC) |
431
+ | NIP-49 | `nostr-extras` | Private key encryption (ncryptsec) |
432
+ | NIP-58 | `nostr-extras` | Badges |
433
+ | NIP-75 | `nostr-extras` | Zap goals (fundraising) |
434
+ | NIP-77 | `nostr-extras` | Negentropy sync |
435
+
436
+ ## Architecture
437
+
438
+ The plugin follows a three-layer architecture:
439
+
440
+ ```
441
+ nostr-capabilities.ts ← Event builders (pure functions, no I/O)
442
+ nostr-discovery.ts ← NIP-05/NIP-11/NIP-27 (network I/O with caching)
443
+ nostr-extras.ts ← Tier 3 namespace re-exports
444
+ nip46-signer.ts ← NIP-46 signer abstraction (NostrSigner interface)
445
+ nip46-doctor.ts ← NIP-46 preflight diagnostics (7-check health report)
446
+ nip46-enroll.ts ← NIP-46 enrollment flow (generate secret, connect, verify)
447
+ nip46-cutover.ts ← NIP-46 migration (local key → remote signer, auto-rollback)
448
+ nip46-status.ts ← Custody status reporting (signing mode, health, warnings)
449
+ │
450
+ nostr-bus.ts ← Runtime wiring (signing, publishing, subscriptions)
451
+ │
452
+ channel.ts ← Public API (OpenClaw plugin interface)
453
+ │
454
+ nostr-profile-http.ts ← HTTP endpoints (/api/channels/nostr/...)
455
+ ```
456
+
457
+ ### Key Design Decisions
458
+
459
+ - **Unsigned templates**: All event builders return `EventTemplate` objects. The bus layer handles signing via `finalizeEvent` (local) or `NostrSigner.signEvent` (NIP-46) and publishing via the pool. This keeps builders pure and testable.
460
+ - **NIP-46 signer abstraction**: The `NostrSigner` interface (`nip46-signer.ts`) provides a unified API for signing, encryption, and decryption. When NIP-46 is enabled, all bus operations delegate to the remote `BunkerSigner` through a request queue (rate-limit protection) with retry logic; otherwise they use the local secret key. The abstraction is transparent to upstream consumers.
461
+ - **NIP-46 lifecycle modules**: Enrollment (`nip46-enroll.ts`), cutover migration (`nip46-cutover.ts`), preflight diagnostics (`nip46-doctor.ts`), and custody status (`nip46-status.ts`) are standalone modules that can be called programmatically or wired into CLI commands.
462
+ - **NIP-28 custom builders**: The upstream `nostr-tools/nip28` functions call `finalizeEvent` internally. We provide our own builders that return unsigned templates to fit the fork's `signAndPublish` pattern.
463
+ - **Caching**: NIP-05 uses a 5-minute TTL with 500-entry LRU. NIP-11 uses a 10-minute TTL with 100-entry LRU. Network errors are not cached (retry on next call).
464
+ - **Per-sender serialization**: Inbound messages from the same pubkey are processed serially to prevent race conditions during relay EOSE bursts (see `PATCHES.md`).
465
+ - **Bounded startup catch-up**: DM subscriptions first query a capped historical window through startup time, then promote to live subscriptions after EOSE/timeout so relay backlog and live traffic have distinct lifecycles.
466
+ - **Real outbound IDs**: Outbound channel `messageId` values are real Nostr identifiers. NIP-04 returns the signed kind:4 event ID; NIP-17 returns the recipient rumor ID after at least one recipient wrap is accepted while the bus also tracks accepted gift-wrap IDs.
467
+
468
+ ## HTTP API
469
+
470
+ All endpoints are under `/api/channels/nostr/:accountId/`. Authentication is handled by the OpenClaw gateway.
471
+
472
+ ### Profile Management
473
+
474
+ | Method | Endpoint | Description |
475
+ | ------ | ------------------------------- | --------------------------- |
476
+ | GET | `/profile` | Get current profile state |
477
+ | PUT | `/profile` | Update and publish profile |
478
+ | POST | `/profile/import` | Import profile from relays |
479
+
480
+ ### Identity & Discovery
481
+
482
+ | Method | Endpoint | Description |
483
+ | ------ | ------------------------------- | -------------------------------------- |
484
+ | GET | `/identity/:nip05` | Resolve NIP-05 address to pubkey |
485
+ | GET | `/identity/search/:domain?q=` | Search NIP-05 domain for users |
486
+ | GET | `/relay-info?url=wss://...` | Get relay NIP-11 capability summary |
487
+
488
+ ### Events
489
+
490
+ | Method | Endpoint | Description |
491
+ | ------ | ------------------------------- | --------------------------- |
492
+ | POST | `/note` | Publish a public note |
493
+ | POST | `/reaction` | React to an event |
494
+ | DELETE | `/events` | Delete events |
495
+
496
+ ### Example: Resolve a NIP-05 Identity
497
+
498
+ ```bash
499
+ curl http://localhost:18789/api/channels/nostr/default/identity/alice%40example.com
500
+ ```
501
+
502
+ ```json
503
+ {
504
+ "ok": true,
505
+ "nip05": "alice@example.com",
506
+ "pubkey": "aabbccdd...",
507
+ "relays": ["wss://relay1.example", "wss://relay2.example"]
508
+ }
509
+ ```
510
+
511
+ ### Example: Check Relay Capabilities
512
+
513
+ ```bash
514
+ curl "http://localhost:18789/api/channels/nostr/default/relay-info?url=wss://relay.sharegap.net"
515
+ ```
516
+
517
+ ```json
518
+ {
519
+ "ok": true,
520
+ "url": "wss://relay.sharegap.net",
521
+ "name": "Damus Relay",
522
+ "supportedNips": [1, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40],
523
+ "authRequired": false,
524
+ "paymentRequired": false,
525
+ "restrictedWrites": false
526
+ }
527
+ ```
528
+
529
+ ## Docker Deployment
530
+
531
+ There are four ways to deploy this fork to an existing OpenClaw Docker setup, listed from simplest to most involved.
532
+
533
+ ### Option 1: Volume Mount (Recommended)
534
+
535
+ Mount the fork's source directory into the container and point OpenClaw's plugin loader at it. No image rebuild required.
536
+
537
+ **1. Clone the fork on the Docker host:**
538
+
539
+ ```bash
540
+ git clone https://git.sharegap.net/cascadia/openclaw-nostr.git /opt/openclaw-nostr
541
+ ```
542
+
543
+ **2. Add to your `docker-compose.yml`:**
544
+
545
+ ```yaml
546
+ services:
547
+ openclaw-gateway:
548
+ volumes:
549
+ - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw
550
+ - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace
551
+ # Mount the fork's source
552
+ - /opt/openclaw-nostr:/opt/openclaw-nostr:ro
553
+ ```
554
+
555
+ **3. Tell OpenClaw to load the plugin via `openclaw.json`:**
556
+
557
+ ```json
558
+ {
559
+ "plugins": {
560
+ "load": {
561
+ "paths": ["/opt/openclaw-nostr"]
562
+ }
563
+ },
564
+ "channels": {
565
+ "nostr": {
566
+ "privateKey": "${NOSTR_PRIVATE_KEY}",
567
+ "relays": ["wss://relay.sharegap.net", "wss://nos.lol"]
568
+ }
569
+ }
570
+ }
571
+ ```
572
+
573
+ **4. Restart:**
574
+
575
+ ```bash
576
+ docker compose restart openclaw-gateway
577
+ ```
578
+
579
+ ### Option 2: Config Extensions Directory
580
+
581
+ Copy the fork into OpenClaw's user-level extensions directory, which is auto-scanned on startup.
582
+
583
+ ```bash
584
+ # Copy into the config dir that's already mounted
585
+ cp -r /opt/openclaw-nostr "${OPENCLAW_CONFIG_DIR}/extensions/nostr"
586
+
587
+ # Restart
588
+ docker compose restart openclaw-gateway
589
+ ```
590
+
591
+ OpenClaw discovers plugins from `~/.openclaw/extensions/` automatically — no `plugins.load.paths` config needed.
592
+
593
+ ### Option 3: Custom Dockerfile Layer
594
+
595
+ Build a derived image with the fork baked in. Best for CI/CD pipelines and reproducible deployments.
596
+
597
+ ```dockerfile
598
+ FROM openclaw:latest
599
+
600
+ # Copy in the fork
601
+ COPY openclaw-nostr /app/extensions/nostr
602
+
603
+ # The fork overrides the bundled nostr extension at the same path
604
+ ```
605
+
606
+ Build and run:
607
+
608
+ ```bash
609
+ docker build -t openclaw-nostr:custom .
610
+ OPENCLAW_IMAGE=openclaw-nostr:custom docker compose up -d
611
+ ```
612
+
613
+ ### Option 4: Build-Arg with Full Source
614
+
615
+ If you're building OpenClaw from source, include the nostr extension via the `OPENCLAW_EXTENSIONS` build arg:
616
+
617
+ ```bash
618
+ # From the openclaw source root
619
+ docker build \
620
+ --build-arg OPENCLAW_EXTENSIONS="nostr" \
621
+ -t openclaw:with-nostr .
622
+ ```
623
+
624
+ This uses the `extensions/nostr` directory within the OpenClaw source tree. To use the fork instead, replace `extensions/nostr` with the fork's source before building.
625
+
626
+ ### Plugin Discovery Precedence
627
+
628
+ OpenClaw discovers plugins in this order (first match wins):
629
+
630
+ 1. **`plugins.load.paths`** — Explicit paths from config (Option 1)
631
+ 2. **Workspace extensions** — `<workspace>/.openclaw/extensions/`
632
+ 3. **User extensions** — `~/.openclaw/extensions/` (Option 2)
633
+ 4. **Bundled extensions** — `/app/extensions/` inside the image (Options 3 & 4)
634
+
635
+ The fork at a higher-precedence path will shadow the bundled upstream version.
636
+
637
+ ### Docker + Durability Patches
638
+
639
+ If you also need the runtime durability patches (reconnect fix, subscription handling, etc.), apply them after image build or container start:
640
+
641
+ ```bash
642
+ # For volume-mount setups, run against the container
643
+ docker exec -it openclaw-gateway bash -c '...'
644
+
645
+ # Or use the apply script against a Docker host
646
+ scripts/apply-to-agent.sh user@docker-host
647
+ ```
648
+
649
+ See `PATCHES.md` for the full list of runtime patches.
650
+
651
+ ## Programmatic Usage
652
+
653
+ ### Channel-Level Functions
654
+
655
+ These are available from `channel.ts` and operate on named accounts:
656
+
657
+ ```typescript
658
+ import {
659
+ // Messaging
660
+ publishNostrNote,
661
+ publishNostrReaction,
662
+ deleteNostrEvents,
663
+ // Identity
664
+ resolveNostrIdentity,
665
+ searchNostrDomain,
666
+ validateNostrIdentity,
667
+ // Discovery
668
+ getNostrRelayInfo,
669
+ getNostrRelayCapabilities,
670
+ // Auth
671
+ getNostrHttpAuthToken,
672
+ // Media
673
+ publishNostrFileMetadata,
674
+ // Social
675
+ repostNostrEvent,
676
+ // Parsing
677
+ parseNostrContent,
678
+ } from "./src/channel.js";
679
+
680
+ // Resolve a NIP-05 identity
681
+ const alice = await resolveNostrIdentity("alice@example.com");
682
+ // { nip05: "alice@example.com", pubkey: "aabb...", relays: ["wss://..."] }
683
+
684
+ // Check relay capabilities
685
+ const caps = await getNostrRelayCapabilities("wss://relay.sharegap.net");
686
+ // { supportedNips: [1, 4, ...], authRequired: false, ... }
687
+
688
+ // Parse content into structured blocks
689
+ const blocks = parseNostrContent("Hello https://example.com #nostr");
690
+ // [{ type: "text", ... }, { type: "url", ... }, { type: "hashtag", ... }]
691
+ ```
692
+
693
+ ### Bus Handle (Direct Access)
694
+
695
+ For advanced use, get the bus handle from `getActiveNostrBuses()`:
696
+
697
+ ```typescript
698
+ import { getActiveNostrBuses } from "./src/channel.js";
699
+
700
+ const bus = getActiveNostrBuses().get("default");
701
+
702
+ // Send a DM and keep the real Nostr ID for logging/threading
703
+ const sent = await bus.sendDm(recipientPubkey, "hello from OpenClaw");
704
+ // sent.eventId is the kind:4 ID for NIP-04, or the recipient rumor ID for NIP-17
705
+ // sent.publishedEventIds contains the signed event IDs accepted by relays
706
+
707
+ // NIP-44 encrypt a message
708
+ const key = bus.getNip44ConversationKey(recipientPubkey);
709
+ const encrypted = bus.nip44Encrypt("secret message", key);
710
+
711
+ // Create a public channel
712
+ const channelId = await bus.createChannel({
713
+ name: "My Channel",
714
+ about: "A public channel for discussion",
715
+ });
716
+
717
+ // Send a channel message
718
+ await bus.sendChannelMessage({
719
+ channelId,
720
+ content: "Hello channel!",
721
+ relayUrl: "wss://relay.example",
722
+ });
723
+
724
+ // Generate NIP-98 auth token for a Blossom upload
725
+ const token = await bus.getNip98Token("https://media.example.com/upload", "POST");
726
+ ```
727
+
728
+ ### Tier 3 Extras (Namespace Imports)
729
+
730
+ ```typescript
731
+ import { nip49, nip58, nip29, nip47, nip30, BlossomClient } from "./src/nostr-extras.js";
732
+
733
+ // NIP-49: Encrypt a private key for storage
734
+ const ncryptsec = nip49.encrypt(secretKey, "password");
735
+ const recovered = nip49.decrypt(ncryptsec, "password");
736
+
737
+ // NIP-47: Parse a Nostr Wallet Connect string
738
+ const connection = nip47.parseConnectionString("nostr+walletconnect://...");
739
+
740
+ // NIP-30: Find custom emoji in content
741
+ for (const match of nip30.matchAll(":custom_emoji: hello")) {
742
+ console.log(match.shortcode, match.url);
743
+ }
744
+ ```
745
+
746
+ ## Testing
747
+
748
+ ### Local Relay (Recommended)
749
+
750
+ ```bash
751
+ # Using strfry
752
+ docker run -p 7777:7777 ghcr.io/hoytech/strfry
753
+
754
+ # Configure openclaw to use local relay
755
+ "relays": ["ws://localhost:7777"]
756
+ ```
757
+
758
+ ### Running Tests
759
+
760
+ Tests run via vitest from the parent OpenClaw workspace:
761
+
762
+ ```bash
763
+ # From the openclaw workspace root
764
+ pnpm vitest run --config vitest.extensions.config.ts extensions/nostr/
765
+
766
+ # Run a specific test file
767
+ pnpm vitest run --config vitest.extensions.config.ts extensions/nostr/src/nostr-discovery.test.ts
768
+ ```
769
+
770
+ ### Test Coverage
771
+
772
+ | Test File | Covers |
773
+ | -------------------------------------- | --------------------------------------------------------- |
774
+ | `nostr-bus.protocol.test.ts` | NIP-04/NIP-17 DM pipeline, reply routing, serialization |
775
+ | `nostr-capabilities-extended.test.ts` | NIP-18 reposts, NIP-28 channels, NIP-13/44/57/94/42/98 |
776
+ | `nostr-discovery.test.ts` | NIP-05 identity, NIP-11 relay info, NIP-27 content parsing|
777
+ | `nostr-extras.test.ts` | Tier 3 re-export surface verification |
778
+ | `nostr-profile-http.test.ts` | HTTP API endpoints including identity/relay-info routes |
779
+ | `nip46-signer.test.ts` | NIP-46 client secret encode/decode, bunker URL parsing, request queue |
780
+ | `nip46-doctor.test.ts` | NIP-46 preflight diagnostics (config validation, report formatting) |
781
+ | `nip46-enroll.test.ts` | NIP-46 enrollment flow (secret gen, URL parsing, progress) |
782
+ | `nip46-cutover.test.ts` | NIP-46 cutover migration (preconditions, rollback) |
783
+ | `nip46-status.test.ts` | Custody status resolution and formatting |
784
+ | `config-schema.test.ts` | Config validation including NIP-46 fields |
785
+ | `types.test.ts` | Account resolution including NIP-46 config, env/file/exec secret sources |
786
+
787
+ ### Manual Test
788
+
789
+ 1. Start the gateway with Nostr configured
790
+ 2. Open Damus, Amethyst, or another Nostr client
791
+ 3. Send a DM to your bot's npub
792
+ 4. Verify the bot responds
793
+
794
+ ## Security Notes
795
+
796
+ - Private keys are never logged
797
+ - Event signatures are verified before processing
798
+ - Use environment variables for keys, never commit to config files
799
+ - Consider using `allowlist` mode in production
800
+ - NIP-98 tokens are signed with the bus's key — scope them to specific URLs
801
+ - NIP-44 conversation keys are derived from the bus's secret key
802
+ - HTTP mutation endpoints (PUT, POST, DELETE) are restricted to loopback addresses
803
+ - **NIP-46**: The client secret (`nip46Secret`) is distinct from the identity key — store it via `NOSTR_NIP46_SECRET` env var or a `SecretRef` (`env`, `file`, `exec`), never in raw config
804
+ - **NIP-46 cutover**: Use `performCutover()` for safe migration — it auto-rolls back if verification fails
805
+
806
+ ## Troubleshooting
807
+
808
+ ### Bot not receiving messages
809
+
810
+ 1. Verify private key (or NIP-46 bunker URL + secret) is correctly configured
811
+ 2. Check relay connectivity
812
+ 3. Ensure `enabled` is not set to `false`
813
+ 4. Check the bot's public key matches what you're sending to
814
+
815
+ ### NIP-46 connection failing
816
+
817
+ 1. Verify the bunker is online and reachable
818
+ 2. Check `nip46BunkerUrl` is a valid `bunker://` URL or `user@domain`
819
+ 3. Verify `NOSTR_NIP46_SECRET` env var is set (64-char hex)
820
+ 4. Check relay connectivity to the signer relays
821
+ 5. Increase `nip46ConnectionTimeoutMs` if the bunker is slow to respond
822
+ 6. Check logs for `NIP-46 auth URL` — the bunker may require user approval
823
+
824
+ ### Messages not being delivered
825
+
826
+ 1. Check relay URLs are correct (must use `wss://`)
827
+ 2. Verify relays are online and accepting connections
828
+ 3. Check for rate limiting (reduce message frequency)
829
+
830
+ ### Docker: Plugin not loading
831
+
832
+ 1. Verify the volume mount path is correct and readable
833
+ 2. Check `plugins.load.paths` points to the right directory
834
+ 3. Run `openclaw plugins list` to see discovered plugins
835
+ 4. Check container logs: `docker compose logs openclaw-gateway`
836
+
837
+ ### NIP-05 resolution failing
838
+
839
+ 1. The target domain must serve `/.well-known/nostr.json`
840
+ 2. Check for CORS issues if resolving from a browser context
841
+ 3. Results are cached for 5 minutes — wait or restart to retry
842
+
843
+ ## License
844
+
845
+ MIT