nostr-bray 1.13.0 → 1.15.0
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/dist/bunker-context.d.ts +42 -0
- package/dist/bunker-context.js +87 -0
- package/dist/bunker.d.ts +25 -0
- package/dist/bunker.js +140 -0
- package/dist/cli.js +37 -1
- package/dist/config.js +11 -1
- package/dist/help.js +3 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +12 -2
- package/dist/types.d.ts +1 -0
- package/dist/util/ncryptsec.d.ts +17 -0
- package/dist/util/ncryptsec.js +39 -0
- package/dist/util/tools.js +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BunkerContext — an IdentityContext-compatible interface backed by a NIP-46 bunker.
|
|
3
|
+
*
|
|
4
|
+
* Instead of holding the secret key locally, all signing is delegated to a
|
|
5
|
+
* remote bunker via encrypted Nostr relay messages.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* BUNKER_URI=bunker://pk?relay=wss://...
|
|
9
|
+
* or
|
|
10
|
+
* BUNKER_URI=bunker://pk?relay=wss://...&secret=clienthex
|
|
11
|
+
*/
|
|
12
|
+
import type { PublicIdentity, SignFn } from './types.js';
|
|
13
|
+
export interface BunkerConfig {
|
|
14
|
+
pubkey: string;
|
|
15
|
+
relay: string;
|
|
16
|
+
secret?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Parse a bunker:// URI */
|
|
19
|
+
export declare function parseBunkerUri(uri: string): BunkerConfig;
|
|
20
|
+
export declare class BunkerContext {
|
|
21
|
+
private signer;
|
|
22
|
+
private pool;
|
|
23
|
+
private pubkeyHex;
|
|
24
|
+
private clientSk;
|
|
25
|
+
private constructor();
|
|
26
|
+
/** Connect to a remote bunker. Blocks until the connection is established. */
|
|
27
|
+
static connect(uri: string, timeoutMs?: number): Promise<BunkerContext>;
|
|
28
|
+
/** The remote identity's npub */
|
|
29
|
+
get activeNpub(): string;
|
|
30
|
+
/** The remote identity's hex pubkey */
|
|
31
|
+
get activePublicKeyHex(): string;
|
|
32
|
+
/** Sign an event via the remote bunker */
|
|
33
|
+
getSigningFunction(): SignFn;
|
|
34
|
+
/** List identities — bunker mode only has one (the remote key) */
|
|
35
|
+
listIdentities(): PublicIdentity[];
|
|
36
|
+
/** NIP-44 encrypt via the remote bunker */
|
|
37
|
+
nip44Encrypt(recipientPubkey: string, plaintext: string): Promise<string>;
|
|
38
|
+
/** NIP-44 decrypt via the remote bunker */
|
|
39
|
+
nip44Decrypt(senderPubkey: string, ciphertext: string): Promise<string>;
|
|
40
|
+
/** Clean up */
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BunkerContext — an IdentityContext-compatible interface backed by a NIP-46 bunker.
|
|
3
|
+
*
|
|
4
|
+
* Instead of holding the secret key locally, all signing is delegated to a
|
|
5
|
+
* remote bunker via encrypted Nostr relay messages.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* BUNKER_URI=bunker://pk?relay=wss://...
|
|
9
|
+
* or
|
|
10
|
+
* BUNKER_URI=bunker://pk?relay=wss://...&secret=clienthex
|
|
11
|
+
*/
|
|
12
|
+
import { BunkerSigner } from 'nostr-tools/nip46';
|
|
13
|
+
import { useWebSocketImplementation, SimplePool } from 'nostr-tools/pool';
|
|
14
|
+
import { generateSecretKey } from 'nostr-tools/pure';
|
|
15
|
+
import { npubEncode } from 'nostr-tools/nip19';
|
|
16
|
+
import WebSocket from 'ws';
|
|
17
|
+
useWebSocketImplementation(WebSocket);
|
|
18
|
+
/** Parse a bunker:// URI */
|
|
19
|
+
export function parseBunkerUri(uri) {
|
|
20
|
+
// bunker://<pubkey>?relay=<url>&secret=<hex>
|
|
21
|
+
const url = new URL(uri);
|
|
22
|
+
const pubkey = url.hostname || url.pathname.replace('//', '');
|
|
23
|
+
const relay = url.searchParams.get('relay');
|
|
24
|
+
const secret = url.searchParams.get('secret') ?? undefined;
|
|
25
|
+
if (!pubkey || !relay) {
|
|
26
|
+
throw new Error('Invalid bunker URI: missing pubkey or relay');
|
|
27
|
+
}
|
|
28
|
+
return { pubkey, relay, secret };
|
|
29
|
+
}
|
|
30
|
+
export class BunkerContext {
|
|
31
|
+
signer;
|
|
32
|
+
pool;
|
|
33
|
+
pubkeyHex;
|
|
34
|
+
clientSk;
|
|
35
|
+
constructor(signer, pool, clientSk) {
|
|
36
|
+
this.signer = signer;
|
|
37
|
+
this.pool = pool;
|
|
38
|
+
this.clientSk = clientSk;
|
|
39
|
+
}
|
|
40
|
+
/** Connect to a remote bunker. Blocks until the connection is established. */
|
|
41
|
+
static async connect(uri, timeoutMs = 15_000) {
|
|
42
|
+
const config = parseBunkerUri(uri);
|
|
43
|
+
const clientSk = config.secret
|
|
44
|
+
? Buffer.from(config.secret, 'hex')
|
|
45
|
+
: generateSecretKey();
|
|
46
|
+
const pool = new SimplePool();
|
|
47
|
+
const signer = BunkerSigner.fromBunker(clientSk, { pubkey: config.pubkey, relays: [config.relay], secret: null }, { pool });
|
|
48
|
+
// Connect and verify
|
|
49
|
+
await signer.connect();
|
|
50
|
+
await signer.ping();
|
|
51
|
+
const ctx = new BunkerContext(signer, pool, clientSk);
|
|
52
|
+
ctx.pubkeyHex = await signer.getPublicKey();
|
|
53
|
+
return ctx;
|
|
54
|
+
}
|
|
55
|
+
/** The remote identity's npub */
|
|
56
|
+
get activeNpub() {
|
|
57
|
+
return npubEncode(this.pubkeyHex);
|
|
58
|
+
}
|
|
59
|
+
/** The remote identity's hex pubkey */
|
|
60
|
+
get activePublicKeyHex() {
|
|
61
|
+
return this.pubkeyHex;
|
|
62
|
+
}
|
|
63
|
+
/** Sign an event via the remote bunker */
|
|
64
|
+
getSigningFunction() {
|
|
65
|
+
return async (template) => {
|
|
66
|
+
return this.signer.signEvent(template);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** List identities — bunker mode only has one (the remote key) */
|
|
70
|
+
listIdentities() {
|
|
71
|
+
return [{ npub: this.activeNpub, purpose: 'bunker', index: 0 }];
|
|
72
|
+
}
|
|
73
|
+
/** NIP-44 encrypt via the remote bunker */
|
|
74
|
+
async nip44Encrypt(recipientPubkey, plaintext) {
|
|
75
|
+
return this.signer.nip44Encrypt(recipientPubkey, plaintext);
|
|
76
|
+
}
|
|
77
|
+
/** NIP-44 decrypt via the remote bunker */
|
|
78
|
+
async nip44Decrypt(senderPubkey, ciphertext) {
|
|
79
|
+
return this.signer.nip44Decrypt(senderPubkey, ciphertext);
|
|
80
|
+
}
|
|
81
|
+
/** Clean up */
|
|
82
|
+
destroy() {
|
|
83
|
+
this.signer.close();
|
|
84
|
+
this.pool.destroy();
|
|
85
|
+
this.clientSk.fill(0);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/bunker.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIP-46 Bunker — remote signing daemon.
|
|
3
|
+
*
|
|
4
|
+
* Holds the user's secret key and responds to signing requests from
|
|
5
|
+
* authorised clients over Nostr relays. Clients never see the nsec.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: NIP-46 (Nostr Connect)
|
|
8
|
+
* - Client sends kind 24133 encrypted request
|
|
9
|
+
* - Bunker responds with kind 24133 encrypted response
|
|
10
|
+
* - All communication NIP-44 encrypted between client and bunker
|
|
11
|
+
*/
|
|
12
|
+
import type { IdentityContext } from './context.js';
|
|
13
|
+
export interface BunkerOptions {
|
|
14
|
+
ctx: IdentityContext;
|
|
15
|
+
relays: string[];
|
|
16
|
+
authorizedKeys?: string[];
|
|
17
|
+
quiet?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface BunkerInstance {
|
|
20
|
+
url: string;
|
|
21
|
+
pubkey: string;
|
|
22
|
+
npub: string;
|
|
23
|
+
close: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function startBunker(opts: BunkerOptions): BunkerInstance;
|
package/dist/bunker.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIP-46 Bunker — remote signing daemon.
|
|
3
|
+
*
|
|
4
|
+
* Holds the user's secret key and responds to signing requests from
|
|
5
|
+
* authorised clients over Nostr relays. Clients never see the nsec.
|
|
6
|
+
*
|
|
7
|
+
* Protocol: NIP-46 (Nostr Connect)
|
|
8
|
+
* - Client sends kind 24133 encrypted request
|
|
9
|
+
* - Bunker responds with kind 24133 encrypted response
|
|
10
|
+
* - All communication NIP-44 encrypted between client and bunker
|
|
11
|
+
*/
|
|
12
|
+
import { getConversationKey, encrypt, decrypt } from 'nostr-tools/nip44';
|
|
13
|
+
import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure';
|
|
14
|
+
import { npubEncode } from 'nostr-tools/nip19';
|
|
15
|
+
import { useWebSocketImplementation, SimplePool } from 'nostr-tools/pool';
|
|
16
|
+
import WebSocket from 'ws';
|
|
17
|
+
useWebSocketImplementation(WebSocket);
|
|
18
|
+
export function startBunker(opts) {
|
|
19
|
+
const { ctx, relays, quiet } = opts;
|
|
20
|
+
const authorizedKeys = new Set(opts.authorizedKeys ?? []);
|
|
21
|
+
const log = quiet ? () => { } : (...args) => console.error('[bunker]', ...args);
|
|
22
|
+
// Generate a bunker-specific keypair for NIP-46 communication
|
|
23
|
+
const bunkerSk = generateSecretKey();
|
|
24
|
+
const bunkerPk = getPublicKey(bunkerSk);
|
|
25
|
+
const bunkerNpub = npubEncode(bunkerPk);
|
|
26
|
+
const pool = new SimplePool();
|
|
27
|
+
// Subscribe to kind 24133 events addressed to us
|
|
28
|
+
const sub = pool.subscribeMany(relays, { kinds: [24133], '#p': [bunkerPk] }, {
|
|
29
|
+
onevent: async (event) => {
|
|
30
|
+
try {
|
|
31
|
+
await handleRequest(event);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
log(`Error handling request: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
async function handleRequest(event) {
|
|
39
|
+
const clientPk = event.pubkey;
|
|
40
|
+
// Check authorization
|
|
41
|
+
if (authorizedKeys.size > 0 && !authorizedKeys.has(clientPk)) {
|
|
42
|
+
log(`Rejected request from unauthorized key: ${clientPk.slice(0, 12)}...`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Decrypt the request
|
|
46
|
+
const conversationKey = getConversationKey(bunkerSk, clientPk);
|
|
47
|
+
let request;
|
|
48
|
+
try {
|
|
49
|
+
const plaintext = decrypt(event.content, conversationKey);
|
|
50
|
+
request = JSON.parse(plaintext);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
log('Failed to decrypt request');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
log(`Request ${request.id}: ${request.method}`);
|
|
57
|
+
let result;
|
|
58
|
+
let error;
|
|
59
|
+
switch (request.method) {
|
|
60
|
+
case 'connect':
|
|
61
|
+
result = 'ack';
|
|
62
|
+
break;
|
|
63
|
+
case 'ping':
|
|
64
|
+
result = 'pong';
|
|
65
|
+
break;
|
|
66
|
+
case 'get_public_key':
|
|
67
|
+
result = ctx.activePublicKeyHex;
|
|
68
|
+
break;
|
|
69
|
+
case 'sign_event': {
|
|
70
|
+
const template = JSON.parse(request.params[0]);
|
|
71
|
+
const sign = ctx.getSigningFunction();
|
|
72
|
+
const signed = await sign(template);
|
|
73
|
+
result = JSON.stringify(signed);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'nip04_encrypt': {
|
|
77
|
+
const [thirdPartyPk, plaintext] = request.params;
|
|
78
|
+
const { encrypt: nip04encrypt } = await import('nostr-tools/nip04');
|
|
79
|
+
result = nip04encrypt(ctx.activePrivateKey, thirdPartyPk, plaintext);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'nip04_decrypt': {
|
|
83
|
+
const [thirdPartyPk, ciphertext] = request.params;
|
|
84
|
+
const { decrypt: nip04decrypt } = await import('nostr-tools/nip04');
|
|
85
|
+
result = nip04decrypt(ctx.activePrivateKey, thirdPartyPk, ciphertext);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case 'nip44_encrypt': {
|
|
89
|
+
const [thirdPartyPk, plaintext] = request.params;
|
|
90
|
+
const ck = getConversationKey(ctx.activePrivateKey, thirdPartyPk);
|
|
91
|
+
result = encrypt(plaintext, ck);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'nip44_decrypt': {
|
|
95
|
+
const [thirdPartyPk, ciphertext] = request.params;
|
|
96
|
+
const ck = getConversationKey(ctx.activePrivateKey, thirdPartyPk);
|
|
97
|
+
result = decrypt(ciphertext, ck);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
result = '';
|
|
102
|
+
error = `unsupported method: ${request.method}`;
|
|
103
|
+
}
|
|
104
|
+
// Encrypt and send response
|
|
105
|
+
const response = error
|
|
106
|
+
? JSON.stringify({ id: request.id, result: '', error })
|
|
107
|
+
: JSON.stringify({ id: request.id, result });
|
|
108
|
+
const encrypted = encrypt(response, conversationKey);
|
|
109
|
+
const responseEvent = finalizeEvent({
|
|
110
|
+
kind: 24133,
|
|
111
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
112
|
+
tags: [['p', clientPk]],
|
|
113
|
+
content: encrypted,
|
|
114
|
+
}, bunkerSk);
|
|
115
|
+
await Promise.any(pool.publish(relays, responseEvent));
|
|
116
|
+
log(`Response ${request.id}: ${error ?? 'ok'}`);
|
|
117
|
+
}
|
|
118
|
+
// Build bunker:// URI
|
|
119
|
+
const relayParams = relays.map(r => `relay=${encodeURIComponent(r)}`).join('&');
|
|
120
|
+
const bunkerUrl = `bunker://${bunkerPk}?${relayParams}`;
|
|
121
|
+
log(`Bunker started: ${bunkerNpub}`);
|
|
122
|
+
log(`URI: ${bunkerUrl}`);
|
|
123
|
+
log(`Signing as: ${ctx.activeNpub}`);
|
|
124
|
+
if (authorizedKeys.size > 0) {
|
|
125
|
+
log(`Authorized keys: ${authorizedKeys.size}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
log('WARNING: No authorized keys set — accepting requests from anyone');
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
url: bunkerUrl,
|
|
132
|
+
pubkey: bunkerPk,
|
|
133
|
+
npub: bunkerNpub,
|
|
134
|
+
close: () => {
|
|
135
|
+
sub.close();
|
|
136
|
+
pool.destroy();
|
|
137
|
+
bunkerSk.fill(0);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { handleTrustSpokenChallenge, handleTrustSpokenVerify } from './trust/spo
|
|
|
19
19
|
import { handleDuressConfigure, handleDuressActivate } from './safety/handlers.js';
|
|
20
20
|
import { handleZapSend, handleZapBalance, handleZapMakeInvoice, handleZapLookupInvoice, handleZapListTransactions, handleZapReceipts, handleZapDecode } from './zap/handlers.js';
|
|
21
21
|
import { handleDecode, handleEncodeNpub, handleEncodeNote, handleEncodeNprofile, handleEncodeNevent, handleVerify, handleEncrypt, handleDecrypt, handleCount, handleFetch, handleKeyPublic, handleEncodeNsec, handleFilter, handleNipList, handleNipShow } from './util/handlers.js';
|
|
22
|
+
import { handleKeyEncrypt, handleKeyDecrypt } from './util/ncryptsec.js';
|
|
22
23
|
import { getCommandHelp } from './help.js';
|
|
23
24
|
import * as fmt from './format.js';
|
|
24
25
|
const args = process.argv.slice(2);
|
|
@@ -28,6 +29,30 @@ if (!command) {
|
|
|
28
29
|
await import('./index.js');
|
|
29
30
|
process.exit(0);
|
|
30
31
|
}
|
|
32
|
+
// Bunker = NIP-46 remote signer
|
|
33
|
+
if (command === 'bunker' && !args.includes('--help')) {
|
|
34
|
+
const { startBunker } = await import('./bunker.js');
|
|
35
|
+
const config = (await import('./config.js')).loadConfig();
|
|
36
|
+
const { IdentityContext: IC } = await import('./context.js');
|
|
37
|
+
const bCtx = new IC(config.secretKey, config.secretFormat);
|
|
38
|
+
config.secretKey = '';
|
|
39
|
+
const authorizedKeys = args.includes('--authorized-keys')
|
|
40
|
+
? args[args.indexOf('--authorized-keys') + 1].split(',')
|
|
41
|
+
: undefined;
|
|
42
|
+
const bunker = startBunker({
|
|
43
|
+
ctx: bCtx,
|
|
44
|
+
relays: config.relays,
|
|
45
|
+
authorizedKeys,
|
|
46
|
+
quiet: args.includes('--quiet'),
|
|
47
|
+
});
|
|
48
|
+
console.error(`nostr-bray bunker running`);
|
|
49
|
+
console.error(`URI: ${bunker.url}`);
|
|
50
|
+
console.error(`Signing as: ${bCtx.activeNpub}`);
|
|
51
|
+
console.error('Press Ctrl+C to stop');
|
|
52
|
+
process.on('SIGINT', () => { bunker.close(); bCtx.destroy(); process.exit(0); });
|
|
53
|
+
process.on('SIGTERM', () => { bunker.close(); bCtx.destroy(); process.exit(0); });
|
|
54
|
+
await new Promise(() => { });
|
|
55
|
+
}
|
|
31
56
|
// Serve = in-memory test relay
|
|
32
57
|
if (command === 'serve' && !args.includes('--help')) {
|
|
33
58
|
const { startRelay } = await import('./serve.js');
|
|
@@ -133,6 +158,8 @@ Utility:
|
|
|
133
158
|
encode-nevent <hex> [relay,...] Encode event ID + relays as nevent
|
|
134
159
|
encode-nsec <hex> Encode hex private key as nsec
|
|
135
160
|
key-public <nsec-or-hex> Derive pubkey from secret key
|
|
161
|
+
key-encrypt <nsec-or-hex> <pass> Encrypt key as ncryptsec (NIP-49)
|
|
162
|
+
key-decrypt <ncryptsec> <pass> Decrypt ncryptsec to verify pubkey
|
|
136
163
|
filter <event-json> <filter-json> Test if event matches filter
|
|
137
164
|
nips List all official NIPs
|
|
138
165
|
nip <number> Show a specific NIP
|
|
@@ -145,11 +172,13 @@ Utility:
|
|
|
145
172
|
Modes:
|
|
146
173
|
(no command) Start MCP server (stdio)
|
|
147
174
|
serve [--port N] [--events file] Start in-memory test relay
|
|
175
|
+
bunker [--authorized-keys pk,pk] Start NIP-46 remote signer daemon
|
|
148
176
|
shell Interactive REPL (persistent relay connection)
|
|
149
177
|
|
|
150
178
|
Environment:
|
|
151
179
|
NOSTR_SECRET_KEY nsec, hex, or BIP-39 mnemonic
|
|
152
180
|
NOSTR_SECRET_KEY_FILE Path to secret key file
|
|
181
|
+
BUNKER_URI / BUNKER_URI_FILE bunker:// URI (use INSTEAD of secret key)
|
|
153
182
|
NOSTR_RELAYS Comma-separated relay URLs
|
|
154
183
|
NWC_URI / NWC_URI_FILE Nostr Wallet Connect URI
|
|
155
184
|
TOR_PROXY SOCKS5h proxy URL
|
|
@@ -601,6 +630,12 @@ async function run(cmdArgs) {
|
|
|
601
630
|
case 'key-public':
|
|
602
631
|
out(handleKeyPublic(req(1, 'key-public <nsec-or-hex>')));
|
|
603
632
|
break;
|
|
633
|
+
case 'key-encrypt':
|
|
634
|
+
out(handleKeyEncrypt(req(1, 'key-encrypt <nsec-or-hex> <password>'), req(2, 'key-encrypt <nsec-or-hex> <password>')));
|
|
635
|
+
break;
|
|
636
|
+
case 'key-decrypt':
|
|
637
|
+
out(handleKeyDecrypt(req(1, 'key-decrypt <ncryptsec> <password>'), req(2, 'key-decrypt <ncryptsec> <password>')));
|
|
638
|
+
break;
|
|
604
639
|
case 'filter':
|
|
605
640
|
out(handleFilter(JSON.parse(req(1, 'filter <event-json> <filter-json>')), JSON.parse(req(2, 'filter <event-json> <filter-json>'))));
|
|
606
641
|
break;
|
|
@@ -660,7 +695,8 @@ const ALL_COMMANDS = [
|
|
|
660
695
|
'blossom-upload', 'blossom-list', 'blossom-delete',
|
|
661
696
|
'group-info', 'group-chat', 'group-send', 'group-members',
|
|
662
697
|
'decode', 'encode-npub', 'encode-note', 'encode-nprofile', 'encode-nevent', 'encode-nsec',
|
|
663
|
-
'key-public', 'filter', 'nips', 'nip', 'verify', 'encrypt', 'decrypt', 'count', 'fetch',
|
|
698
|
+
'key-public', 'key-encrypt', 'key-decrypt', 'filter', 'nips', 'nip', 'verify', 'encrypt', 'decrypt', 'count', 'fetch',
|
|
699
|
+
'bunker',
|
|
664
700
|
'help', 'exit',
|
|
665
701
|
];
|
|
666
702
|
async function shell() {
|
package/dist/config.js
CHANGED
|
@@ -36,14 +36,21 @@ export function loadConfig() {
|
|
|
36
36
|
const keyFilePath = process.env.NOSTR_SECRET_KEY_FILE;
|
|
37
37
|
const keyEnvVar = process.env.NOSTR_SECRET_KEY;
|
|
38
38
|
let secretKey;
|
|
39
|
+
const bunkerUri = process.env.BUNKER_URI ?? process.env.BUNKER_URI_FILE
|
|
40
|
+
? (process.env.BUNKER_URI_FILE ? readSecretFile(process.env.BUNKER_URI_FILE) : process.env.BUNKER_URI)
|
|
41
|
+
: undefined;
|
|
39
42
|
if (keyFilePath) {
|
|
40
43
|
secretKey = readSecretFile(keyFilePath);
|
|
41
44
|
}
|
|
42
45
|
else if (keyEnvVar) {
|
|
43
46
|
secretKey = keyEnvVar;
|
|
44
47
|
}
|
|
48
|
+
else if (bunkerUri) {
|
|
49
|
+
// Bunker mode — no local secret needed
|
|
50
|
+
secretKey = '';
|
|
51
|
+
}
|
|
45
52
|
else {
|
|
46
|
-
throw new Error('No secret key provided: set NOSTR_SECRET_KEY or
|
|
53
|
+
throw new Error('No secret key provided: set NOSTR_SECRET_KEY, NOSTR_SECRET_KEY_FILE, or BUNKER_URI');
|
|
47
54
|
}
|
|
48
55
|
const secretFormat = detectKeyFormat(secretKey);
|
|
49
56
|
// --- NWC URI ---
|
|
@@ -72,10 +79,13 @@ export function loadConfig() {
|
|
|
72
79
|
delete process.env.NOSTR_SECRET_KEY_FILE;
|
|
73
80
|
delete process.env.NWC_URI;
|
|
74
81
|
delete process.env.NWC_URI_FILE;
|
|
82
|
+
delete process.env.BUNKER_URI;
|
|
83
|
+
delete process.env.BUNKER_URI_FILE;
|
|
75
84
|
return {
|
|
76
85
|
secretKey,
|
|
77
86
|
secretFormat,
|
|
78
87
|
relays,
|
|
88
|
+
bunkerUri: bunkerUri ?? undefined,
|
|
79
89
|
nwcUri,
|
|
80
90
|
torProxy,
|
|
81
91
|
allowClearnetWithTor,
|
package/dist/help.js
CHANGED
|
@@ -84,6 +84,9 @@ export const COMMAND_HELP = {
|
|
|
84
84
|
count: { usage: 'count [--kinds N,N] [--authors X,X] [--since N]', description: 'Count events matching a filter on relays.', examples: ['nostr-bray count --kinds 1', 'nostr-bray count --kinds 1,7 --authors abc123...'] },
|
|
85
85
|
fetch: { usage: 'fetch <nip19>', description: 'Fetch events by nip19 code (note, nevent, nprofile, npub, naddr). Resolves the entity and queries relays.', examples: ['nostr-bray fetch note1...', 'nostr-bray fetch nevent1...', 'nostr-bray fetch npub1...'] },
|
|
86
86
|
shell: { usage: 'shell', description: 'Start an interactive REPL with a persistent relay connection. Supports tab autocomplete. Type "help" for commands, "exit" to quit.', examples: ['nostr-bray shell'] },
|
|
87
|
+
'key-encrypt': { usage: 'key-encrypt <nsec-or-hex> <password>', description: 'Encrypt a secret key with a password using NIP-49. Returns an ncryptsec string safe for storage. Also returns the derived pubkey for verification.', examples: ['nostr-bray key-encrypt nsec1... mypassword'], notes: 'WARNING: secret key is passed as a CLI argument. Use only on trusted systems.' },
|
|
88
|
+
'key-decrypt': { usage: 'key-decrypt <ncryptsec> <password>', description: 'Decrypt an ncryptsec (NIP-49) with a password. Returns the pubkey for verification — never the raw secret key.', examples: ['nostr-bray key-decrypt ncryptsec1... mypassword'] },
|
|
89
|
+
bunker: { usage: 'bunker [--authorized-keys pk1,pk2] [--quiet]', description: 'Start a NIP-46 remote signer daemon. Holds your secret key and responds to signing requests from authorised clients over Nostr relays. Clients never see your nsec.', examples: ['nostr-bray bunker', 'nostr-bray bunker --authorized-keys abc123...,def456...', 'nostr-bray bunker --quiet'], notes: 'Without --authorized-keys, accepts requests from anyone. Set authorized keys for production use.' },
|
|
87
90
|
serve: { usage: 'serve [--port N] [--hostname H] [--events file.jsonl] [--quiet]', description: 'Start an in-memory Nostr relay for testing. Implements NIP-01 (events, subscriptions, EOSE) and NIP-11 (relay info). Events live in memory only — no persistence. Optionally pre-load events from a JSONL file.', examples: ['nostr-bray serve', 'nostr-bray serve --port 7777', 'nostr-bray serve --events test-data.jsonl', 'nostr-bray serve --port 7777 --quiet'] },
|
|
88
91
|
};
|
|
89
92
|
/** Get formatted help for a single command */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { IdentityContext } from './context.js';
|
|
3
2
|
import { RelayPool } from './relay-pool.js';
|
|
4
3
|
import { Nip65Manager } from './nip65.js';
|
|
5
4
|
export declare const deps: {
|
|
6
|
-
ctx:
|
|
5
|
+
ctx: any;
|
|
7
6
|
pool: RelayPool;
|
|
8
7
|
nip65: Nip65Manager;
|
|
9
8
|
nwcUri: string | undefined;
|
package/dist/index.js
CHANGED
|
@@ -18,10 +18,20 @@ const pool = new RelayPool({
|
|
|
18
18
|
defaultRelays: config.relays,
|
|
19
19
|
});
|
|
20
20
|
const nip65 = new Nip65Manager(pool, config.relays);
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// Connect to bunker or use local key
|
|
22
|
+
let ctx;
|
|
23
|
+
if (config.bunkerUri) {
|
|
24
|
+
const { BunkerContext } = await import('./bunker-context.js');
|
|
25
|
+
ctx = await BunkerContext.connect(config.bunkerUri);
|
|
26
|
+
console.error(`Connected to bunker — signing as ${ctx.activeNpub}`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
ctx = new IdentityContext(config.secretKey, config.secretFormat);
|
|
30
|
+
}
|
|
31
|
+
export const deps = { ctx: ctx, pool, nip65, nwcUri: config.nwcUri };
|
|
23
32
|
config.secretKey = '';
|
|
24
33
|
config.nwcUri = undefined;
|
|
34
|
+
config.bunkerUri = undefined;
|
|
25
35
|
// Load master identity relay list
|
|
26
36
|
const masterRelays = await nip65.loadForIdentity(ctx.activeNpub);
|
|
27
37
|
pool.reconfigure(ctx.activeNpub, masterRelays);
|
package/dist/types.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface BrayConfig {
|
|
|
31
31
|
readonly secretKey: string;
|
|
32
32
|
readonly secretFormat: 'nsec' | 'hex' | 'mnemonic';
|
|
33
33
|
readonly relays: string[];
|
|
34
|
+
readonly bunkerUri?: string;
|
|
34
35
|
readonly nwcUri?: string;
|
|
35
36
|
readonly torProxy?: string;
|
|
36
37
|
readonly allowClearnetWithTor: boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIP-49 ncryptsec — password-encrypted secret keys.
|
|
3
|
+
*
|
|
4
|
+
* Encrypt an nsec with a passphrase for safe storage.
|
|
5
|
+
* Decrypt an ncryptsec back to the raw key.
|
|
6
|
+
*/
|
|
7
|
+
/** Encrypt a secret key (nsec or hex) with a password. Returns ncryptsec string. */
|
|
8
|
+
export declare function handleKeyEncrypt(secret: string, password: string): {
|
|
9
|
+
ncryptsec: string;
|
|
10
|
+
pubkeyHex: string;
|
|
11
|
+
npub: string;
|
|
12
|
+
};
|
|
13
|
+
/** Decrypt an ncryptsec with a password. Returns the derived pubkey (never the raw key). */
|
|
14
|
+
export declare function handleKeyDecrypt(ncryptsec: string, password: string): {
|
|
15
|
+
pubkeyHex: string;
|
|
16
|
+
npub: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NIP-49 ncryptsec — password-encrypted secret keys.
|
|
3
|
+
*
|
|
4
|
+
* Encrypt an nsec with a passphrase for safe storage.
|
|
5
|
+
* Decrypt an ncryptsec back to the raw key.
|
|
6
|
+
*/
|
|
7
|
+
import { encrypt, decrypt } from 'nostr-tools/nip49';
|
|
8
|
+
import { decode } from 'nostr-tools/nip19';
|
|
9
|
+
import { getPublicKey } from 'nostr-tools/pure';
|
|
10
|
+
import { npubEncode } from 'nostr-tools/nip19';
|
|
11
|
+
/** Encrypt a secret key (nsec or hex) with a password. Returns ncryptsec string. */
|
|
12
|
+
export function handleKeyEncrypt(secret, password) {
|
|
13
|
+
let bytes;
|
|
14
|
+
if (secret.startsWith('nsec1')) {
|
|
15
|
+
bytes = decode(secret).data;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
bytes = Buffer.from(secret, 'hex');
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const ncryptsec = encrypt(bytes, password);
|
|
22
|
+
const pubkeyHex = getPublicKey(bytes);
|
|
23
|
+
return { ncryptsec, pubkeyHex, npub: npubEncode(pubkeyHex) };
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
bytes.fill(0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Decrypt an ncryptsec with a password. Returns the derived pubkey (never the raw key). */
|
|
30
|
+
export function handleKeyDecrypt(ncryptsec, password) {
|
|
31
|
+
const bytes = decrypt(ncryptsec, password);
|
|
32
|
+
try {
|
|
33
|
+
const pubkeyHex = getPublicKey(bytes);
|
|
34
|
+
return { pubkeyHex, npub: npubEncode(pubkeyHex) };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
bytes.fill(0);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/util/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { hexId } from '../validation.js';
|
|
3
|
+
import { handleKeyEncrypt, handleKeyDecrypt } from './ncryptsec.js';
|
|
3
4
|
import { handleDecode, handleEncodeNpub, handleEncodeNote, handleEncodeNprofile, handleEncodeNevent, handleEncodeNaddr, handleVerify, handleEncrypt, handleDecrypt, handleCount, handleFetch, handleKeyPublic, handleEncodeNsec, handleFilter, handleNipList, handleNipShow, } from './handlers.js';
|
|
4
5
|
export function registerUtilTools(server, deps) {
|
|
5
6
|
server.registerTool('decode', {
|
|
@@ -167,4 +168,26 @@ export function registerUtilTools(server, deps) {
|
|
|
167
168
|
const nip = await handleNipShow(number);
|
|
168
169
|
return { content: [{ type: 'text', text: nip.content }] };
|
|
169
170
|
});
|
|
171
|
+
server.registerTool('key_encrypt', {
|
|
172
|
+
description: 'Encrypt a secret key with a password (NIP-49 ncryptsec). Returns the ncryptsec string and the derived pubkey. WARNING: secret key is transmitted through the MCP transport.',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
secret: z.string().describe('Secret key (nsec or hex)'),
|
|
175
|
+
password: z.string().describe('Password to encrypt with'),
|
|
176
|
+
},
|
|
177
|
+
annotations: { readOnlyHint: true },
|
|
178
|
+
}, async ({ secret, password }) => {
|
|
179
|
+
const result = handleKeyEncrypt(secret, password);
|
|
180
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
181
|
+
});
|
|
182
|
+
server.registerTool('key_decrypt', {
|
|
183
|
+
description: 'Decrypt an ncryptsec (NIP-49) with a password. Returns the derived pubkey for verification — never the raw key.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
ncryptsec: z.string().describe('ncryptsec string to decrypt'),
|
|
186
|
+
password: z.string().describe('Password used during encryption'),
|
|
187
|
+
},
|
|
188
|
+
annotations: { readOnlyHint: true },
|
|
189
|
+
}, async ({ ncryptsec, password }) => {
|
|
190
|
+
const result = handleKeyDecrypt(ncryptsec, password);
|
|
191
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
192
|
+
});
|
|
170
193
|
}
|
package/package.json
CHANGED