nostr-bray 3.9.0 → 3.10.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.js +12 -0
- package/dist/cli/commands/bunker.js +94 -42
- package/dist/cli/index.js +1 -1
- package/dist/help.js +1 -1
- package/dist/veil/scoring.js +8 -4
- package/llms-full.txt +1 -1
- package/package.json +2 -2
package/dist/bunker.js
CHANGED
|
@@ -145,8 +145,20 @@ export function startBunker(opts) {
|
|
|
145
145
|
log(`Client no longer in approved-clients.json, so refused: ${clientPk.slice(0, 12)}...`);
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
|
+
// `bunker authorize` adds to the approvals file while the daemon runs, so a
|
|
149
|
+
// client not yet approved in memory is looked up there before the gates.
|
|
150
|
+
function approvedOnDisk(clientPk) {
|
|
151
|
+
const current = readStateFileStrict(APPROVALS_FILE, opts.stateDir);
|
|
152
|
+
return current !== null && Array.isArray(current[bunkerPk]) && current[bunkerPk].includes(clientPk);
|
|
153
|
+
}
|
|
148
154
|
async function handleRequest(event) {
|
|
149
155
|
const clientPk = event.pubkey;
|
|
156
|
+
if (!openMode && !authorizedKeys.has(clientPk) && approvedOnDisk(clientPk)) {
|
|
157
|
+
authorizedKeys.add(clientPk);
|
|
158
|
+
if (!bunkerApprovals.includes(clientPk))
|
|
159
|
+
bunkerApprovals.push(clientPk);
|
|
160
|
+
log(`Client found in approved-clients.json (bunker authorize): ${clientPk.slice(0, 12)}...`);
|
|
161
|
+
}
|
|
150
162
|
if (!openMode && authorizedKeys.has(clientPk) && !stillApproved(clientPk)) {
|
|
151
163
|
// A revoked client has left authorizedKeys and meets the gates below as
|
|
152
164
|
// a stranger. One still there could not be checked (unreadable file).
|
|
@@ -20,6 +20,21 @@ export async function applyBunkerPersona(ctx, args) {
|
|
|
20
20
|
}
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
+
/** A pubkey given as 64-character hex or npub, as lowercase hex; undefined if neither. */
|
|
24
|
+
async function parsePubkeyArg(given) {
|
|
25
|
+
if (!given)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (/^[0-9a-f]{64}$/i.test(given))
|
|
28
|
+
return given.toLowerCase();
|
|
29
|
+
const { decode } = await import('nostr-tools/nip19');
|
|
30
|
+
try {
|
|
31
|
+
const decoded = decode(given);
|
|
32
|
+
return decoded.type === 'npub' ? decoded.data : undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
23
38
|
export async function dispatch(args) {
|
|
24
39
|
// Accept `bunker daemon` as alias for `bunker` (start daemon)
|
|
25
40
|
if (args[1] === 'daemon')
|
|
@@ -200,41 +215,92 @@ export async function dispatch(args) {
|
|
|
200
215
|
process.exit(1);
|
|
201
216
|
}
|
|
202
217
|
}
|
|
203
|
-
// ── bunker authorize <app-pubkey>
|
|
204
|
-
// Pre-
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
218
|
+
// ── bunker authorize <app-pubkey|npub> [which daemon] ──────────────────────
|
|
219
|
+
// Pre-approve an app's pubkey on the local bunker daemon, so it connects
|
|
220
|
+
// without the one-time secret. The approval is keyed by the daemon's own
|
|
221
|
+
// bunker key, found the way the daemon finds it: --bunker-key-file,
|
|
222
|
+
// --bunker-key, or the key bray keeps for --persona, --profile or the
|
|
223
|
+
// default identity. --daemon-pubkey names the daemon's pubkey directly. A running daemon picks it up from the
|
|
224
|
+
// app's next request. No network connection is made.
|
|
208
225
|
if (args[1] === 'authorize') {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
226
|
+
const usage = 'usage: bray bunker authorize <64-char-hex-pubkey|npub> [--persona <name> | --profile <name> | --bunker-key-file <file> | --bunker-key <hex> | --daemon-pubkey <hex|npub>]';
|
|
227
|
+
const clientPk = await parsePubkeyArg(args[2]);
|
|
228
|
+
const opt = (name) => {
|
|
229
|
+
const i = args.indexOf(name);
|
|
230
|
+
return i >= 0 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : undefined;
|
|
231
|
+
};
|
|
232
|
+
if (!clientPk) {
|
|
233
|
+
console.error(usage);
|
|
212
234
|
process.exit(1);
|
|
213
235
|
}
|
|
214
|
-
const {
|
|
215
|
-
const {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
236
|
+
const { getPublicKey } = await import('nostr-tools/pure');
|
|
237
|
+
const { readStateFile, readStateFileStrict, writeStateFile } = await import('../../state.js');
|
|
238
|
+
let bunkerPk;
|
|
239
|
+
let which;
|
|
240
|
+
if (args.includes('--daemon-pubkey')) {
|
|
241
|
+
bunkerPk = await parsePubkeyArg(opt('--daemon-pubkey'));
|
|
242
|
+
which = 'the --daemon-pubkey daemon';
|
|
243
|
+
if (!bunkerPk) {
|
|
244
|
+
console.error(usage);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (args.includes('--bunker-key-file')) {
|
|
249
|
+
const file = opt('--bunker-key-file');
|
|
250
|
+
const { readFileSync } = await import('node:fs');
|
|
251
|
+
let hex = '';
|
|
252
|
+
try {
|
|
253
|
+
hex = file ? readFileSync(file, 'utf-8').trim() : '';
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
hex = '';
|
|
257
|
+
}
|
|
258
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
259
|
+
console.error('bunker authorize: --bunker-key-file must name a file holding a 64-character hex key');
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
bunkerPk = getPublicKey(Buffer.from(hex, 'hex'));
|
|
263
|
+
which = 'the key in --bunker-key-file';
|
|
264
|
+
}
|
|
265
|
+
else if (args.includes('--bunker-key')) {
|
|
266
|
+
const hex = opt('--bunker-key') ?? '';
|
|
267
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
268
|
+
console.error('bunker authorize: --bunker-key must be a 64-character hex key');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
bunkerPk = getPublicKey(Buffer.from(hex, 'hex'));
|
|
272
|
+
which = 'the --bunker-key daemon';
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const persona = opt('--persona');
|
|
276
|
+
const profile = opt('--profile');
|
|
277
|
+
const label = persona ?? profile ?? 'default';
|
|
278
|
+
const startHint = persona ? ` --persona ${persona}` : profile ? ` --profile ${profile}` : '';
|
|
279
|
+
const keys = readStateFile('bunker-keys.json');
|
|
280
|
+
const hex = keys[label];
|
|
281
|
+
if (typeof hex !== 'string' || !/^[0-9a-f]{64}$/i.test(hex)) {
|
|
282
|
+
console.error(`bunker authorize: no bunker daemon key for "${label}" yet. Start the daemon once (bray bunker${startHint}), or pass --daemon-pubkey <the daemon's pubkey>.`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
bunkerPk = getPublicKey(Buffer.from(hex, 'hex'));
|
|
286
|
+
which = label === 'default' ? 'the default daemon' : `the daemon for "${label}"`;
|
|
224
287
|
}
|
|
225
|
-
const { pubkey: bunkerPk } = parseBunkerUri(bunkerUriStr);
|
|
226
288
|
const APPROVALS_FILE = 'approved-clients.json';
|
|
227
|
-
const current =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
289
|
+
const current = readStateFileStrict(APPROVALS_FILE);
|
|
290
|
+
if (current === null) {
|
|
291
|
+
console.error('bunker authorize: approved-clients.json in the bray state directory is unreadable; fix it or move it aside, then retry');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const entries = Array.isArray(current[bunkerPk]) ? current[bunkerPk] : [];
|
|
295
|
+
const { npubEncode } = await import('nostr-tools/nip19');
|
|
296
|
+
if (entries.includes(clientPk)) {
|
|
297
|
+
console.error(`bunker authorize: ${clientPk} is already authorised for ${which}`);
|
|
232
298
|
process.exit(0);
|
|
233
299
|
}
|
|
234
|
-
entries
|
|
235
|
-
current[bunkerPk] = entries;
|
|
300
|
+
current[bunkerPk] = [...entries, clientPk];
|
|
236
301
|
writeStateFile(APPROVALS_FILE, current);
|
|
237
|
-
console.
|
|
302
|
+
console.error(`Authorised for ${which}. A running daemon accepts this app from its next request.`);
|
|
303
|
+
console.log(JSON.stringify({ authorised: clientPk, bunker: npubEncode(bunkerPk), total: current[bunkerPk].length }, null, 2));
|
|
238
304
|
process.exit(0);
|
|
239
305
|
}
|
|
240
306
|
// ── bunker revoke <app-pubkey|npub> ─────────────────────────────────────────
|
|
@@ -248,21 +314,7 @@ export async function dispatch(args) {
|
|
|
248
314
|
console.error(usage);
|
|
249
315
|
process.exit(1);
|
|
250
316
|
}
|
|
251
|
-
|
|
252
|
-
if (/^[0-9a-f]{64}$/i.test(given)) {
|
|
253
|
-
clientPk = given.toLowerCase();
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
const { decode } = await import('nostr-tools/nip19');
|
|
257
|
-
try {
|
|
258
|
-
const decoded = decode(given);
|
|
259
|
-
if (decoded.type === 'npub')
|
|
260
|
-
clientPk = decoded.data;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
clientPk = undefined;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
317
|
+
const clientPk = await parsePubkeyArg(given);
|
|
266
318
|
if (!clientPk) {
|
|
267
319
|
console.error(usage);
|
|
268
320
|
process.exit(1);
|
package/dist/cli/index.js
CHANGED
|
@@ -250,7 +250,7 @@ Modes:
|
|
|
250
250
|
(--auth requires NIP-42; --eager-auth also challenges on connect)
|
|
251
251
|
bunker connect <bunker://…> Save remote bunker URI for future commands
|
|
252
252
|
bunker pair [--relay <url>] [--expect <npub>] Pair with a signer app over nostrconnect:// and save it
|
|
253
|
-
bunker authorize <hex-pubkey>
|
|
253
|
+
bunker authorize <hex-pubkey|npub> [--persona N | --profile N | --bunker-key-file F | --daemon-pubkey P] Pre-approve an app on the local daemon, even while it runs
|
|
254
254
|
bunker revoke <hex-pubkey|npub> Withdraw an app's approval, even from a running daemon
|
|
255
255
|
bunker status Show saved bunker connection state
|
|
256
256
|
bunker sign <file|-> One-shot: sign an event template via the bunker (or the saved signer)
|
package/dist/help.js
CHANGED
|
@@ -93,7 +93,7 @@ export const COMMAND_HELP = {
|
|
|
93
93
|
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'] },
|
|
94
94
|
'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.' },
|
|
95
95
|
'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'] },
|
|
96
|
-
bunker: { usage: 'bunker [--profile <name>] [--persona <name>] [--bunker-key-file <f>] [--authorized-keys pk1,pk2] [--open] [--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. --profile <name> loads the master from an nsec-tree-cli profile (~/.nsec-tree/profiles/<name>.json); --persona <name> signs as a derived sub-identity (the same key as nsec-tree-cli "export nsec <name>"). The bunker:// key stays stable across restarts, since a per-identity connection key is persisted automatically. The printed link carries a one-time connect secret: a new client must connect with it, after which the daemon prints the next link, so one link cannot be shared between two apps. An unused secret survives restarts. Clients approved earlier reconnect without it and are listed at start; `bunker revoke <pubkey>` withdraws one
|
|
96
|
+
bunker: { usage: 'bunker [--profile <name>] [--persona <name>] [--bunker-key-file <f>] [--authorized-keys pk1,pk2] [--open] [--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. --profile <name> loads the master from an nsec-tree-cli profile (~/.nsec-tree/profiles/<name>.json); --persona <name> signs as a derived sub-identity (the same key as nsec-tree-cli "export nsec <name>"). The bunker:// key stays stable across restarts, since a per-identity connection key is persisted automatically. The printed link carries a one-time connect secret: a new client must connect with it, after which the daemon prints the next link, so one link cannot be shared between two apps. An unused secret survives restarts. Clients approved earlier reconnect without it and are listed at start; `bunker authorize <pubkey>` pre-approves an app without the secret (pass the same --persona, --profile, --bunker-key-file or --bunker-key as the daemon, or --daemon-pubkey) and `bunker revoke <pubkey>` withdraws one; a running daemon applies either from the next request that app sends (--open serves any client regardless).', examples: ['nostr-bray bunker', 'nostr-bray bunker --profile master --persona magazine', 'nostr-bray bunker --authorized-keys abc123...,def456...', 'nostr-bray bunker --quiet'], notes: 'With --authorized-keys, only those clients, plus any saved as approved (earlier connections or bunker authorize), are accepted and no secret is used. --open serves any client that connects, with no secret; anyone who learns the link can then sign as you. Clients that connect while it runs open are not saved, so a later daemon without --open still asks them for the secret.' },
|
|
97
97
|
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'] },
|
|
98
98
|
};
|
|
99
99
|
/** Get formatted help for a single command */
|
package/dist/veil/scoring.js
CHANGED
|
@@ -38,14 +38,18 @@ export class VeilScoring {
|
|
|
38
38
|
let ringEndorsements = 0;
|
|
39
39
|
// 4. Parse assertions and count endorsements
|
|
40
40
|
for (const event of events) {
|
|
41
|
+
// The relay may return anything; count only kind 30382 assertions about
|
|
42
|
+
// this pubkey, so a valid proof about someone else adds nothing.
|
|
43
|
+
if (event.kind !== 30382)
|
|
44
|
+
continue;
|
|
41
45
|
const parsed = parseAssertion(event);
|
|
42
|
-
if (parsed.subject
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
if (parsed.subject !== pubkey)
|
|
47
|
+
continue;
|
|
48
|
+
endorsements++;
|
|
45
49
|
// 5. Check for veil-sig tags and verify ring proofs
|
|
46
50
|
const hasVeilSig = event.tags.some((t) => t[0] === 'veil-sig');
|
|
47
51
|
if (hasVeilSig) {
|
|
48
|
-
const verification = verifyProof(event);
|
|
52
|
+
const verification = verifyProof(event, { expectedKind: 30382 });
|
|
49
53
|
if (verification.valid) {
|
|
50
54
|
ringEndorsements += verification.distinctSigners;
|
|
51
55
|
if (!flags.includes('ring proof verified')) {
|
package/llms-full.txt
CHANGED
|
@@ -39,7 +39,7 @@ Single-process MCP server (stdio or HTTP transport) with 261 tools in 29 groups.
|
|
|
39
39
|
*Authentication tiers (use exactly one):
|
|
40
40
|
1. **Bunker** (NIP-46) -- most secure, keys never leave the signer
|
|
41
41
|
For a signer app (Amber, nsec.app), `npx nostr-bray bunker pair --relay <wss://…>` prints a nostrconnect:// URI to paste or scan into the app. Once the app answers, bray saves the signer (a secret-free bunker:// URI marked paired, plus the client key the app approved) in its state directory, and the MCP server and CLI sign through it with no BUNKER_URI. Options: `--relay` (repeatable; defaults to NOSTR_RELAYS, then the config file relays), `--timeout <seconds>` (default 300), `--expect <npub>` (refuse any other account), `--yes` (skip the y/N prompt). In a terminal bray shows the account the signer offers and asks y/N before saving; outside one, `--expect` or `--yes` is required. The URI is a one-time credential: whoever answers it first becomes the signer. With TOR_PROXY set, pairing goes through the proxy and only .onion relays are accepted unless ALLOW_CLEARNET_WITH_TOR=1. `bunker status` reports `paired`; if the app forgets bray, start-up fails with a hint to run `bunker pair` again. An explicit BUNKER_URI or key takes precedence; `bunker connect` replaces the saved signer. A paired signer is checked with get_public_key on start, not sent connect again.
|
|
42
|
-
bray can also be the signer: `npx nostr-bray bunker` runs a NIP-46 daemon holding your key and prints a bunker:// link with a one-time connect secret. A new client must connect with that link; the daemon then prints the next link, so one link serves one app. An unused secret survives restarts. Clients approved earlier reconnect without the secret and are listed at start; `npx nostr-bray bunker revoke <pubkey|npub>` withdraws one, and a running daemon
|
|
42
|
+
bray can also be the signer: `npx nostr-bray bunker` runs a NIP-46 daemon holding your key and prints a bunker:// link with a one-time connect secret. A new client must connect with that link; the daemon then prints the next link, so one link serves one app. An unused secret survives restarts. Clients approved earlier reconnect without the secret and are listed at start; `npx nostr-bray bunker authorize <pubkey|npub> [--persona <name>]` pre-approves an app for that daemon without the secret, `npx nostr-bray bunker revoke <pubkey|npub>` withdraws one, and a running daemon applies either from that app's next request. `--authorized-keys pk1,pk2` accepts only those clients; `--open` serves any client that connects, so anyone who learns the link can sign as you; clients served while open are not saved as approved.
|
|
43
43
|
2. **ncryptsec** (NIP-49) -- password-encrypted key, decrypted at startup
|
|
44
44
|
3. **File** -- secret read from disk, deleted from process.env after parsing
|
|
45
45
|
4. **Env var** -- quickest start, lowest security
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nostr-bray",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"mcpName": "dev.forgesworn/bray",
|
|
5
5
|
"description": "Trust-aware Nostr MCP server, CLI and TypeScript SDK for AI agents and humans. Three dimensions of trust -- verification, proximity, and access -- woven into every interaction.",
|
|
6
6
|
"type": "module",
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
"farrier-kit": "1.1.3",
|
|
181
181
|
"nostr-attestations": "^3.0.1",
|
|
182
182
|
"nostr-tools": "2.24.1",
|
|
183
|
-
"nostr-veil": "^0.
|
|
183
|
+
"nostr-veil": "^0.32.0",
|
|
184
184
|
"nsec-tree": "^1.4.2",
|
|
185
185
|
"signet-protocol": "^1.1.0",
|
|
186
186
|
"socks-proxy-agent": "^10.0.0",
|