nostr-bray 0.0.0-development
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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +229 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +87 -0
- package/dist/context.d.ts +35 -0
- package/dist/context.js +184 -0
- package/dist/identity/handlers.d.ts +31 -0
- package/dist/identity/handlers.js +32 -0
- package/dist/identity/migration.d.ts +37 -0
- package/dist/identity/migration.js +84 -0
- package/dist/identity/shamir.d.ts +18 -0
- package/dist/identity/shamir.js +46 -0
- package/dist/identity/tools.d.ts +11 -0
- package/dist/identity/tools.js +168 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +125 -0
- package/dist/nip65.d.ts +15 -0
- package/dist/nip65.js +71 -0
- package/dist/relay/handlers.d.ts +36 -0
- package/dist/relay/handlers.js +96 -0
- package/dist/relay/tools.d.ts +3 -0
- package/dist/relay/tools.js +60 -0
- package/dist/relay-pool.d.ts +42 -0
- package/dist/relay-pool.js +150 -0
- package/dist/safety/handlers.d.ts +15 -0
- package/dist/safety/handlers.js +12 -0
- package/dist/safety/tools.d.ts +3 -0
- package/dist/safety/tools.js +29 -0
- package/dist/social/dm.d.ts +28 -0
- package/dist/social/dm.js +90 -0
- package/dist/social/handlers.d.ts +42 -0
- package/dist/social/handlers.js +107 -0
- package/dist/social/notifications.d.ts +32 -0
- package/dist/social/notifications.js +74 -0
- package/dist/social/tools.d.ts +3 -0
- package/dist/social/tools.js +168 -0
- package/dist/trust/handlers.d.ts +65 -0
- package/dist/trust/handlers.js +133 -0
- package/dist/trust/ring.d.ts +19 -0
- package/dist/trust/ring.js +54 -0
- package/dist/trust/spoken.d.ts +33 -0
- package/dist/trust/spoken.js +14 -0
- package/dist/trust/tools.d.ts +3 -0
- package/dist/trust/tools.js +182 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +5 -0
- package/dist/zap/handlers.d.ts +80 -0
- package/dist/zap/handlers.js +165 -0
- package/dist/zap/tools.d.ts +3 -0
- package/dist/zap/tools.js +98 -0
- package/llms-full.txt +281 -0
- package/llms.txt +73 -0
- package/package.json +96 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ForgeSworn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# nostr-bray
|
|
2
|
+
|
|
3
|
+
> Sovereign Nostr identities for AI agents
|
|
4
|
+
|
|
5
|
+
[](https://github.com/forgesworn/bray/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/nostr-bray)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](./tsconfig.json)
|
|
9
|
+
|
|
10
|
+
An MCP server that gives AI agents a full Nostr identity — not just a key pair, but a hierarchical identity tree with personas, attestations, ring signatures, encrypted DMs, and duress detection. 44 tools across 7 groups.
|
|
11
|
+
|
|
12
|
+
## The Problem
|
|
13
|
+
|
|
14
|
+
AI agents interacting with Nostr today are handed a single key pair with no separation of concerns. One compromised session leaks everything. There is no way to rotate keys, prove identity links, or maintain separate personas for different contexts.
|
|
15
|
+
|
|
16
|
+
nostr-bray solves this with nsec-tree hierarchical derivation. A single master secret generates unlimited child identities, each with its own key pair, purpose, and relay set. Private keys are zeroed from memory on eviction. Agents can switch personas mid-conversation, prove they control the master without revealing the derivation path, and activate a duress identity if compromised.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"nostr": {
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["nostr-bray"],
|
|
26
|
+
"env": {
|
|
27
|
+
"NOSTR_SECRET_KEY": "nsec1...",
|
|
28
|
+
"NOSTR_RELAYS": "wss://relay.damus.io,wss://nos.lol"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or with a secret file (recommended):
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"nostr": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["nostr-bray"],
|
|
43
|
+
"env": {
|
|
44
|
+
"NOSTR_SECRET_KEY_FILE": "/path/to/secret.key",
|
|
45
|
+
"NOSTR_RELAYS": "wss://relay.damus.io,wss://nos.lol"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tool Groups
|
|
53
|
+
|
|
54
|
+
### Identity (11 tools)
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `identity_create` | Generate a fresh identity with BIP-39 mnemonic |
|
|
59
|
+
| `identity_derive` | Derive a child identity by purpose and index |
|
|
60
|
+
| `identity_derive_persona` | Derive a named persona (work, personal, anonymous) |
|
|
61
|
+
| `identity_switch` | Switch active identity — all tools operate as the new identity |
|
|
62
|
+
| `identity_list` | List all known identities (public info only) |
|
|
63
|
+
| `identity_prove` | Create blind/full linkage proof |
|
|
64
|
+
| `identity_backup_shamir` | Split master secret into Shamir shard files |
|
|
65
|
+
| `identity_restore_shamir` | Reconstruct secret from shard files |
|
|
66
|
+
| `identity_backup` | Fetch profile, contacts, relay list as portable bundle |
|
|
67
|
+
| `identity_restore` | Re-sign migratable events under the active identity |
|
|
68
|
+
| `identity_migrate` | Full migration with preview, confirmation, and linkage proof |
|
|
69
|
+
|
|
70
|
+
### Social (9 tools)
|
|
71
|
+
|
|
72
|
+
| Tool | Description |
|
|
73
|
+
|------|-------------|
|
|
74
|
+
| `social_post` | Post a text note (kind 1) |
|
|
75
|
+
| `social_reply` | Reply with correct e-tag and p-tag threading |
|
|
76
|
+
| `social_react` | React to an event (kind 7) |
|
|
77
|
+
| `social_profile_get` | Fetch and parse a kind 0 profile |
|
|
78
|
+
| `social_profile_set` | Set profile with overwrite safety guard |
|
|
79
|
+
| `dm_send` | Send encrypted DM (NIP-17 default, NIP-04 opt-in) |
|
|
80
|
+
| `dm_read` | Read and decrypt received DMs |
|
|
81
|
+
| `social_notifications` | Fetch mentions, replies, reactions, zap receipts |
|
|
82
|
+
| `social_feed` | Fetch kind 1 text note feed |
|
|
83
|
+
|
|
84
|
+
### Trust (10 tools)
|
|
85
|
+
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `trust_attest` | Create kind 31000 verifiable attestation |
|
|
89
|
+
| `trust_read` | Read attestations by subject/type/attestor |
|
|
90
|
+
| `trust_verify` | Validate attestation structure |
|
|
91
|
+
| `trust_revoke` | Revoke an attestation (identity check) |
|
|
92
|
+
| `trust_request` | Send attestation request via NIP-17 |
|
|
93
|
+
| `trust_request_list` | Scan DMs for attestation requests |
|
|
94
|
+
| `trust_proof_publish` | Publish linkage proof (kind 30078) with confirmation |
|
|
95
|
+
| `trust_ring_prove` | Anonymous group membership proof (ring signature) |
|
|
96
|
+
| `trust_ring_verify` | Verify ring signature proof |
|
|
97
|
+
| `trust_spoken_challenge` | Generate spoken verification token |
|
|
98
|
+
| `trust_spoken_verify` | Verify spoken token response |
|
|
99
|
+
|
|
100
|
+
### Relay (4 tools)
|
|
101
|
+
|
|
102
|
+
| Tool | Description |
|
|
103
|
+
|------|-------------|
|
|
104
|
+
| `relay_list` | List relays with shared-relay warnings |
|
|
105
|
+
| `relay_set` | Publish kind 10002 relay list |
|
|
106
|
+
| `relay_add` | Add relay to active identity (in-memory) |
|
|
107
|
+
| `relay_info` | Fetch NIP-11 relay information document |
|
|
108
|
+
|
|
109
|
+
### Zap (7 tools)
|
|
110
|
+
|
|
111
|
+
| Tool | Description |
|
|
112
|
+
|------|-------------|
|
|
113
|
+
| `zap_send` | Pay a Lightning invoice via NWC |
|
|
114
|
+
| `zap_balance` | Request wallet balance via NWC |
|
|
115
|
+
| `zap_make_invoice` | Generate a Lightning invoice via NWC |
|
|
116
|
+
| `zap_lookup_invoice` | Check invoice payment status via NWC |
|
|
117
|
+
| `zap_list_transactions` | List recent Lightning transactions |
|
|
118
|
+
| `zap_receipts` | Parse zap receipts (amount, sender, message) |
|
|
119
|
+
| `zap_decode` | Decode bolt11 invoice fields |
|
|
120
|
+
|
|
121
|
+
### Safety (2 tools)
|
|
122
|
+
|
|
123
|
+
| Tool | Description |
|
|
124
|
+
|------|-------------|
|
|
125
|
+
| `safety_configure` | Configure an alternative identity persona |
|
|
126
|
+
| `safety_activate` | Switch to alternative identity |
|
|
127
|
+
|
|
128
|
+
## Identity Switching
|
|
129
|
+
|
|
130
|
+
The killer feature. Every tool operates as the "active identity." Switch with a single call:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
identity_derive_persona("work") → npub1abc...
|
|
134
|
+
identity_switch("work") → now operating as work persona
|
|
135
|
+
social_post("Hello from work!") → signed by npub1abc...
|
|
136
|
+
identity_switch("master") → back to master
|
|
137
|
+
social_post("Back to main") → signed by master npub
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Derived identities are cryptographically unlinkable unless you publish a linkage proof.
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
| Variable | Description |
|
|
145
|
+
|----------|-------------|
|
|
146
|
+
| `NOSTR_SECRET_KEY` | nsec bech32, 64-char hex, or BIP-39 mnemonic |
|
|
147
|
+
| `NOSTR_SECRET_KEY_FILE` | Path to secret key file (takes precedence) |
|
|
148
|
+
| `NOSTR_RELAYS` | Comma-separated relay URLs |
|
|
149
|
+
| `TOR_PROXY` | SOCKS5h proxy for Tor (blocks clearnet relays by default) |
|
|
150
|
+
| `NIP04_ENABLED` | Set `1` to enable legacy NIP-04 DMs |
|
|
151
|
+
| `TRANSPORT` | `stdio` (default) or `http` |
|
|
152
|
+
| `PORT` | HTTP port (default 3000) |
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- **[Usage Guide](./docs/guide.md)** — walkthroughs for identity management, DMs, attestations, NWC payments, Tor, and duress
|
|
157
|
+
- **[Examples](./examples/)** — MCP config files (basic, NWC, Tor) and a CLI workflow script
|
|
158
|
+
- **[Contributing](./CONTRIBUTING.md)** — setup, architecture, testing, and conventions
|
|
159
|
+
|
|
160
|
+
## For AI Assistants
|
|
161
|
+
|
|
162
|
+
See [llms.txt](./llms.txt) for a concise summary optimised for AI context windows, or [llms-full.txt](./llms-full.txt) for complete tool documentation with parameter details.
|
|
163
|
+
|
|
164
|
+
## Licence
|
|
165
|
+
|
|
166
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* nostr-bray CLI — standalone Nostr identity management from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* nostr-bray Start MCP server (default)
|
|
7
|
+
* nostr-bray post "hello nostr" Post a text note
|
|
8
|
+
* nostr-bray reply <event-id> "text" Reply to an event
|
|
9
|
+
* nostr-bray react <event-id> React to an event (+)
|
|
10
|
+
* nostr-bray derive <purpose> Derive a child identity
|
|
11
|
+
* nostr-bray persona <name> Derive a named persona
|
|
12
|
+
* nostr-bray switch <target> Switch active identity
|
|
13
|
+
* nostr-bray list List all identities
|
|
14
|
+
* nostr-bray prove [blind|full] Create a linkage proof
|
|
15
|
+
* nostr-bray profile <pubkey-hex> Fetch a profile
|
|
16
|
+
* nostr-bray relay-info <url> Fetch NIP-11 relay info
|
|
17
|
+
* nostr-bray backup <dir> Shamir backup (threshold=3, shares=5)
|
|
18
|
+
* nostr-bray whoami Show active identity npub
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* nostr-bray CLI — standalone Nostr identity management from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* nostr-bray Start MCP server (default)
|
|
7
|
+
* nostr-bray post "hello nostr" Post a text note
|
|
8
|
+
* nostr-bray reply <event-id> "text" Reply to an event
|
|
9
|
+
* nostr-bray react <event-id> React to an event (+)
|
|
10
|
+
* nostr-bray derive <purpose> Derive a child identity
|
|
11
|
+
* nostr-bray persona <name> Derive a named persona
|
|
12
|
+
* nostr-bray switch <target> Switch active identity
|
|
13
|
+
* nostr-bray list List all identities
|
|
14
|
+
* nostr-bray prove [blind|full] Create a linkage proof
|
|
15
|
+
* nostr-bray profile <pubkey-hex> Fetch a profile
|
|
16
|
+
* nostr-bray relay-info <url> Fetch NIP-11 relay info
|
|
17
|
+
* nostr-bray backup <dir> Shamir backup (threshold=3, shares=5)
|
|
18
|
+
* nostr-bray whoami Show active identity npub
|
|
19
|
+
*/
|
|
20
|
+
import { loadConfig } from './config.js';
|
|
21
|
+
import { IdentityContext } from './context.js';
|
|
22
|
+
import { RelayPool } from './relay-pool.js';
|
|
23
|
+
import { Nip65Manager } from './nip65.js';
|
|
24
|
+
import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet } from './social/handlers.js';
|
|
25
|
+
import { handleIdentityList, handleIdentityProve } from './identity/handlers.js';
|
|
26
|
+
import { handleBackupShamir } from './identity/shamir.js';
|
|
27
|
+
import { handleRelayInfo } from './relay/handlers.js';
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const command = args[0];
|
|
30
|
+
// No command = start MCP server (default behaviour)
|
|
31
|
+
if (!command || command === 'serve') {
|
|
32
|
+
await import('./index.js');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
// Help — no config needed
|
|
36
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
37
|
+
console.log('nostr-bray — Sovereign Nostr identities for AI agents');
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log('Usage: nostr-bray [command] [args]');
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log('Commands:');
|
|
42
|
+
console.log(' (no command) Start MCP server (stdio)');
|
|
43
|
+
console.log(' whoami Show active identity npub');
|
|
44
|
+
console.log(' list List all identities');
|
|
45
|
+
console.log(' derive <purpose> [index] Derive a child identity');
|
|
46
|
+
console.log(' persona <name> [index] Derive a named persona');
|
|
47
|
+
console.log(' switch <target> [index] Switch active identity');
|
|
48
|
+
console.log(' prove [blind|full] Create a linkage proof');
|
|
49
|
+
console.log(' post "message" Post a text note');
|
|
50
|
+
console.log(' reply <event-id> "text" Reply to an event');
|
|
51
|
+
console.log(' react <event-id> [reaction] React to an event');
|
|
52
|
+
console.log(' profile <pubkey-hex> Fetch a profile');
|
|
53
|
+
console.log(' relay-info <wss://url> Fetch NIP-11 relay info');
|
|
54
|
+
console.log(' backup <dir> [t] [n] Shamir backup (default 3-of-5)');
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log('Environment:');
|
|
57
|
+
console.log(' NOSTR_SECRET_KEY nsec, hex, or BIP-39 mnemonic');
|
|
58
|
+
console.log(' NOSTR_SECRET_KEY_FILE Path to secret key file');
|
|
59
|
+
console.log(' NOSTR_RELAYS Comma-separated relay URLs');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
// CLI commands need config + context
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
const pool = new RelayPool({
|
|
65
|
+
torProxy: config.torProxy,
|
|
66
|
+
allowClearnet: config.allowClearnetWithTor || !config.torProxy,
|
|
67
|
+
defaultRelays: config.relays,
|
|
68
|
+
});
|
|
69
|
+
const nip65 = new Nip65Manager(pool, config.relays);
|
|
70
|
+
const ctx = new IdentityContext(config.secretKey, config.secretFormat);
|
|
71
|
+
config.secretKey = '';
|
|
72
|
+
config.nwcUri = undefined;
|
|
73
|
+
// Load master relay list
|
|
74
|
+
const masterRelays = await nip65.loadForIdentity(ctx.activeNpub);
|
|
75
|
+
pool.reconfigure(ctx.activeNpub, masterRelays);
|
|
76
|
+
async function run() {
|
|
77
|
+
switch (command) {
|
|
78
|
+
case 'whoami': {
|
|
79
|
+
console.log(ctx.activeNpub);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'list': {
|
|
83
|
+
const identities = handleIdentityList(ctx);
|
|
84
|
+
console.log(JSON.stringify(identities, null, 2));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'derive': {
|
|
88
|
+
const purpose = args[1];
|
|
89
|
+
if (!purpose) {
|
|
90
|
+
console.error('Usage: nostr-bray derive <purpose> [index]');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const index = parseInt(args[2] ?? '0', 10);
|
|
94
|
+
const result = ctx.derive(purpose, index);
|
|
95
|
+
console.log(JSON.stringify(result, null, 2));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'persona': {
|
|
99
|
+
const name = args[1];
|
|
100
|
+
if (!name) {
|
|
101
|
+
console.error('Usage: nostr-bray persona <name> [index]');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const index = parseInt(args[2] ?? '0', 10);
|
|
105
|
+
const result = ctx.derivePersona(name, index);
|
|
106
|
+
console.log(JSON.stringify(result, null, 2));
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'switch': {
|
|
110
|
+
const target = args[1];
|
|
111
|
+
if (!target) {
|
|
112
|
+
console.error('Usage: nostr-bray switch <target> [index]');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const index = args[2] ? parseInt(args[2], 10) : undefined;
|
|
116
|
+
ctx.switch(target, index);
|
|
117
|
+
console.log(ctx.activeNpub);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'prove': {
|
|
121
|
+
const mode = (args[1] === 'full' ? 'full' : 'blind');
|
|
122
|
+
const proof = handleIdentityProve(ctx, { mode });
|
|
123
|
+
console.log(JSON.stringify(proof, null, 2));
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'post': {
|
|
127
|
+
const content = args[1];
|
|
128
|
+
if (!content) {
|
|
129
|
+
console.error('Usage: nostr-bray post "your message"');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const result = await handleSocialPost(ctx, pool, { content });
|
|
133
|
+
console.log(JSON.stringify({ id: result.event.id, pubkey: result.event.pubkey, publish: result.publish }, null, 2));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'reply': {
|
|
137
|
+
const eventId = args[1];
|
|
138
|
+
const content = args[2];
|
|
139
|
+
if (!eventId || !content) {
|
|
140
|
+
console.error('Usage: nostr-bray reply <event-id> "your reply"');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const result = await handleSocialReply(ctx, pool, {
|
|
144
|
+
content,
|
|
145
|
+
replyTo: eventId,
|
|
146
|
+
replyToPubkey: '', // simplified for CLI
|
|
147
|
+
});
|
|
148
|
+
console.log(JSON.stringify({ id: result.event.id, publish: result.publish }, null, 2));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'react': {
|
|
152
|
+
const eventId = args[1];
|
|
153
|
+
if (!eventId) {
|
|
154
|
+
console.error('Usage: nostr-bray react <event-id> [reaction]');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const result = await handleSocialReact(ctx, pool, {
|
|
158
|
+
eventId,
|
|
159
|
+
eventPubkey: '',
|
|
160
|
+
reaction: args[2] ?? '+',
|
|
161
|
+
});
|
|
162
|
+
console.log(JSON.stringify({ id: result.event.id, publish: result.publish }, null, 2));
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'profile': {
|
|
166
|
+
const pubkeyHex = args[1];
|
|
167
|
+
if (!pubkeyHex) {
|
|
168
|
+
console.error('Usage: nostr-bray profile <pubkey-hex>');
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
const profile = await handleSocialProfileGet(pool, ctx.activeNpub, pubkeyHex);
|
|
172
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'relay-info': {
|
|
176
|
+
const url = args[1];
|
|
177
|
+
if (!url) {
|
|
178
|
+
console.error('Usage: nostr-bray relay-info <wss://relay-url>');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const info = await handleRelayInfo(url);
|
|
182
|
+
console.log(JSON.stringify(info, null, 2));
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'backup': {
|
|
186
|
+
const dir = args[1];
|
|
187
|
+
if (!dir) {
|
|
188
|
+
console.error('Usage: nostr-bray backup <output-dir> [threshold] [shares]');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const threshold = parseInt(args[2] ?? '3', 10);
|
|
192
|
+
const shares = parseInt(args[3] ?? '5', 10);
|
|
193
|
+
const result = handleBackupShamir({
|
|
194
|
+
secret: new Uint8Array(ctx.activePrivateKey),
|
|
195
|
+
threshold,
|
|
196
|
+
shares,
|
|
197
|
+
outputDir: dir,
|
|
198
|
+
});
|
|
199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
console.error(`Unknown command: ${command}`);
|
|
204
|
+
console.error('');
|
|
205
|
+
console.error('Commands:');
|
|
206
|
+
console.error(' whoami Show active identity npub');
|
|
207
|
+
console.error(' list List all identities');
|
|
208
|
+
console.error(' derive <purpose> [index] Derive a child identity');
|
|
209
|
+
console.error(' persona <name> [index] Derive a named persona');
|
|
210
|
+
console.error(' switch <target> [index] Switch active identity');
|
|
211
|
+
console.error(' prove [blind|full] Create a linkage proof');
|
|
212
|
+
console.error(' post "message" Post a text note');
|
|
213
|
+
console.error(' reply <event-id> "text" Reply to an event');
|
|
214
|
+
console.error(' react <event-id> [reaction] React to an event');
|
|
215
|
+
console.error(' profile <pubkey-hex> Fetch a profile');
|
|
216
|
+
console.error(' relay-info <wss://url> Fetch NIP-11 relay info');
|
|
217
|
+
console.error(' backup <dir> [t] [n] Shamir backup (default 3-of-5)');
|
|
218
|
+
console.error('');
|
|
219
|
+
console.error('No command = start MCP server (stdio)');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await run();
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
ctx.destroy();
|
|
228
|
+
pool.close();
|
|
229
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BrayConfig } from './types.js';
|
|
2
|
+
/** Auto-detect key format from string content */
|
|
3
|
+
export declare function detectKeyFormat(key: string): 'nsec' | 'hex' | 'mnemonic';
|
|
4
|
+
/** Load configuration from environment variables and optional secret files */
|
|
5
|
+
export declare function loadConfig(): BrayConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
const NSEC_RE = /^nsec1[a-z0-9]{58}$/;
|
|
3
|
+
const HEX_RE = /^[0-9a-f]{64}$/;
|
|
4
|
+
/** Auto-detect key format from string content */
|
|
5
|
+
export function detectKeyFormat(key) {
|
|
6
|
+
if (NSEC_RE.test(key))
|
|
7
|
+
return 'nsec';
|
|
8
|
+
if (HEX_RE.test(key))
|
|
9
|
+
return 'hex';
|
|
10
|
+
// BIP-39 mnemonic: 12 or 24 space-separated lowercase words
|
|
11
|
+
const words = key.split(/\s+/);
|
|
12
|
+
if (words.length >= 12 && words.every(w => /^[a-z]+$/.test(w)))
|
|
13
|
+
return 'mnemonic';
|
|
14
|
+
throw new Error(`Invalid key format: expected nsec1..., 64-char hex, or BIP-39 mnemonic`);
|
|
15
|
+
}
|
|
16
|
+
/** Read and trim a secret from a file */
|
|
17
|
+
function readSecretFile(path) {
|
|
18
|
+
return readFileSync(path, 'utf-8').trim();
|
|
19
|
+
}
|
|
20
|
+
/** Parse comma-separated relay URLs */
|
|
21
|
+
function parseRelays(csv) {
|
|
22
|
+
return csv.split(',').map(r => r.trim()).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
/** Check that all relays are Tor-safe when a SOCKS proxy is configured */
|
|
25
|
+
function validateTorRelays(relays, torProxy, allowClearnet) {
|
|
26
|
+
if (!torProxy || allowClearnet)
|
|
27
|
+
return;
|
|
28
|
+
const clearnet = relays.filter(r => !r.includes('.onion'));
|
|
29
|
+
if (clearnet.length > 0) {
|
|
30
|
+
throw new Error(`Clearnet relays not allowed with Tor proxy (set ALLOW_CLEARNET_WITH_TOR=1 to override): ${clearnet.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Load configuration from environment variables and optional secret files */
|
|
34
|
+
export function loadConfig() {
|
|
35
|
+
// --- Secret key ---
|
|
36
|
+
const keyFilePath = process.env.NOSTR_SECRET_KEY_FILE;
|
|
37
|
+
const keyEnvVar = process.env.NOSTR_SECRET_KEY;
|
|
38
|
+
let secretKey;
|
|
39
|
+
if (keyFilePath) {
|
|
40
|
+
secretKey = readSecretFile(keyFilePath);
|
|
41
|
+
}
|
|
42
|
+
else if (keyEnvVar) {
|
|
43
|
+
secretKey = keyEnvVar;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new Error('No secret key provided: set NOSTR_SECRET_KEY or NOSTR_SECRET_KEY_FILE');
|
|
47
|
+
}
|
|
48
|
+
const secretFormat = detectKeyFormat(secretKey);
|
|
49
|
+
// --- NWC URI ---
|
|
50
|
+
const nwcFilePath = process.env.NWC_URI_FILE;
|
|
51
|
+
let nwcUri;
|
|
52
|
+
if (nwcFilePath) {
|
|
53
|
+
nwcUri = readSecretFile(nwcFilePath);
|
|
54
|
+
}
|
|
55
|
+
else if (process.env.NWC_URI) {
|
|
56
|
+
nwcUri = process.env.NWC_URI;
|
|
57
|
+
}
|
|
58
|
+
// --- Relays ---
|
|
59
|
+
const relays = parseRelays(process.env.NOSTR_RELAYS ?? '');
|
|
60
|
+
// --- Tor ---
|
|
61
|
+
const torProxy = process.env.TOR_PROXY || undefined;
|
|
62
|
+
const allowClearnetWithTor = process.env.ALLOW_CLEARNET_WITH_TOR === '1';
|
|
63
|
+
validateTorRelays(relays, torProxy, allowClearnetWithTor);
|
|
64
|
+
// --- Transport ---
|
|
65
|
+
const transport = (process.env.TRANSPORT === 'http' ? 'http' : 'stdio');
|
|
66
|
+
const port = parseInt(process.env.PORT ?? '3000', 10);
|
|
67
|
+
const bindAddress = process.env.BIND_ADDRESS ?? '127.0.0.1';
|
|
68
|
+
// --- NIP-04 ---
|
|
69
|
+
const nip04Enabled = process.env.NIP04_ENABLED === '1';
|
|
70
|
+
// --- Clean up secrets from process.env ---
|
|
71
|
+
delete process.env.NOSTR_SECRET_KEY;
|
|
72
|
+
delete process.env.NOSTR_SECRET_KEY_FILE;
|
|
73
|
+
delete process.env.NWC_URI;
|
|
74
|
+
delete process.env.NWC_URI_FILE;
|
|
75
|
+
return {
|
|
76
|
+
secretKey,
|
|
77
|
+
secretFormat,
|
|
78
|
+
relays,
|
|
79
|
+
nwcUri,
|
|
80
|
+
torProxy,
|
|
81
|
+
allowClearnetWithTor,
|
|
82
|
+
nip04Enabled,
|
|
83
|
+
transport,
|
|
84
|
+
port,
|
|
85
|
+
bindAddress,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { LinkageProof } from 'nsec-tree';
|
|
2
|
+
import type { PublicIdentity, SignFn } from './types.js';
|
|
3
|
+
export interface ContextOptions {
|
|
4
|
+
maxCache?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class IdentityContext {
|
|
7
|
+
private root;
|
|
8
|
+
private cache;
|
|
9
|
+
private masterEntry;
|
|
10
|
+
private activeEntry;
|
|
11
|
+
private maxCacheSize;
|
|
12
|
+
constructor(secretKey: string, format: 'nsec' | 'hex' | 'mnemonic', opts?: ContextOptions);
|
|
13
|
+
/** Current active identity's npub */
|
|
14
|
+
get activeNpub(): string;
|
|
15
|
+
/** Derive a child identity by purpose and index */
|
|
16
|
+
derive(purpose: string, index: number): PublicIdentity;
|
|
17
|
+
/** Derive a named persona */
|
|
18
|
+
derivePersona(name: string, index: number): PublicIdentity;
|
|
19
|
+
/** Switch active identity by purpose+index, persona name, or "master" */
|
|
20
|
+
switch(purposeOrName: string, index?: number): void;
|
|
21
|
+
/** Get a signing function bound to the current active identity */
|
|
22
|
+
getSigningFunction(): SignFn;
|
|
23
|
+
/** List all known identities — returns public info only, never private keys */
|
|
24
|
+
listIdentities(): PublicIdentity[];
|
|
25
|
+
/** Create a linkage proof for the active identity */
|
|
26
|
+
prove(mode?: 'blind' | 'full'): LinkageProof;
|
|
27
|
+
/** Get the active identity's private key for NIP-17/44/04 crypto operations */
|
|
28
|
+
get activePrivateKey(): Uint8Array;
|
|
29
|
+
/** Test helper — returns reference to private key bytes for verifying zeroise */
|
|
30
|
+
_getPrivateKeyRefForTesting(npub: string): Uint8Array | undefined;
|
|
31
|
+
/** Destroy all identities and the tree root */
|
|
32
|
+
destroy(): void;
|
|
33
|
+
/** Insert into LRU cache, evicting oldest if at capacity */
|
|
34
|
+
private putCache;
|
|
35
|
+
}
|