nostr-bray 3.4.3 → 3.5.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/README.md +77 -10
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +28 -1
- package/dist/cli/commands/wallet.js +2 -1
- package/dist/cli/commands/zap.js +2 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/dispatch/tools.js +1 -1
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.js +71 -0
- package/dist/http-token.d.ts +8 -0
- package/dist/http-token.js +18 -0
- package/dist/identity/tools.d.ts +3 -0
- package/dist/index.js +13 -63
- package/dist/marketplace/handlers.d.ts +8 -3
- package/dist/marketplace/handlers.js +21 -4
- package/dist/marketplace/tools.js +38 -6
- package/dist/sdk.js +4 -1
- package/dist/tool-groups.d.ts +16 -0
- package/dist/tool-groups.js +52 -0
- package/dist/types.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +6 -0
- package/dist/wallet-service/grants.d.ts +24 -1
- package/dist/wallet-service/grants.js +172 -20
- package/dist/wallet-service/service.d.ts +9 -3
- package/dist/wallet-service/service.js +136 -36
- package/dist/wallet-service/tools.d.ts +10 -1
- package/dist/wallet-service/tools.js +101 -111
- package/dist/wallet-service/upstream.d.ts +2 -0
- package/dist/wallet-service/upstream.js +41 -5
- package/dist/zap/confirm.d.ts +16 -0
- package/dist/zap/confirm.js +58 -0
- package/dist/zap/handlers.d.ts +10 -1
- package/dist/zap/handlers.js +56 -6
- package/dist/zap/index.d.ts +1 -0
- package/dist/zap/index.js +1 -0
- package/dist/zap/payment-guard.d.ts +59 -0
- package/dist/zap/payment-guard.js +213 -0
- package/dist/zap/tools.js +29 -2
- package/llms-full.txt +13 -9
- package/llms.txt +4 -4
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nostr-bray
|
|
2
2
|
|
|
3
|
-
**Trust-aware Nostr MCP for AI and humans.**
|
|
3
|
+
**Trust-aware Nostr MCP for AI and humans.** 260 tools across 29 groups. Model-agnostic. Works with Claude, ChatGPT, Gemini, Cursor, Windsurf, or any MCP client.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/nostr-bray)
|
|
6
6
|
[](https://github.com/forgesworn/bray/actions/workflows/ci.yml)
|
|
@@ -166,12 +166,13 @@ nostr-bray implements or integrates the following NIPs:
|
|
|
166
166
|
| **NIP-44** | Encrypted payloads v2 |
|
|
167
167
|
| **NIP-45** | Event counts |
|
|
168
168
|
| **NIP-46** | Nostr Connect (bunker) |
|
|
169
|
+
| **NIP-47** | Nostr Wallet Connect: pay BOLT-11 invoices, balance, invoices, and scoped spending connections over one wallet. NIP-44 v2 only |
|
|
169
170
|
| **NIP-49** | Private key encryption (ncryptsec) |
|
|
170
171
|
| **NIP-50** | Search |
|
|
171
172
|
| **NIP-51** | Lists (mute, pin, follow sets, bookmarks) |
|
|
172
173
|
| **NIP-52** | Calendar events |
|
|
173
174
|
| **NIP-54** | Wiki pages |
|
|
174
|
-
| **NIP-57** |
|
|
175
|
+
| **NIP-57** | Zap receipts (kind 9735) are read. bray pays BOLT-11 invoices over NIP-47 but does not build kind 9734 zap requests or resolve LNURL, so it cannot yet zap a profile or note by itself |
|
|
175
176
|
| **NIP-58** | Badges |
|
|
176
177
|
| **NIP-65** | Relay list metadata |
|
|
177
178
|
| **NIP-72** | Communities |
|
|
@@ -214,6 +215,10 @@ Search order: `BRAY_CONFIG` env var > `$XDG_CONFIG_HOME/bray/config.json` > `~/.
|
|
|
214
215
|
| `NOSTR_NCRYPTSEC` | NIP-49 encrypted key |
|
|
215
216
|
| `NOSTR_NCRYPTSEC_PASSWORD` | Password for ncryptsec |
|
|
216
217
|
| `NWC_URI_FILE` | Path to a private `0600` file containing the NWC bearer URI |
|
|
218
|
+
| `BRAY_MAX_PAYMENT_MSAT` | bray's ceiling on any one payment, in msat (default `5000000`, 5,000 sats) |
|
|
219
|
+
| `BRAY_MAX_DAILY_MSAT` | bray's ceiling on spending in any rolling 24 hours, in msat (default `20000000`, 20,000 sats) |
|
|
220
|
+
| `BRAY_WALLET_SERVICE` | Set `1` to register `wallet-grant`, `wallet-refill` and `wallet-serve` |
|
|
221
|
+
| `BRAY_HOME` | Directory for bray's state files (default `$XDG_CONFIG_HOME/bray`, else `~/.config/bray`) |
|
|
217
222
|
| `NOSTR_RELAYS` | Comma-separated relay URLs |
|
|
218
223
|
| `NOSTR_FORBID_PUBKEY` | Comma-separated npubs or hex this process must never sign as. See below. |
|
|
219
224
|
| `TOR_PROXY` | SOCKS5h proxy for Tor |
|
|
@@ -224,7 +229,8 @@ Search order: `BRAY_CONFIG` env var > `$XDG_CONFIG_HOME/bray/config.json` > `~/.
|
|
|
224
229
|
All secret env vars are deleted from `process.env` before parsing can fail.
|
|
225
230
|
Raw `NWC_URI` is refused; use `NWC_URI_FILE` or `wallet connect <nwc-file>` so
|
|
226
231
|
the bearer credential never appears in a process environment, command argument
|
|
227
|
-
or MCP tool argument.
|
|
232
|
+
or MCP tool argument. The wallet must speak NIP-44 v2 encryption for NWC;
|
|
233
|
+
wallets that only offer NIP-04 are refused.
|
|
228
234
|
|
|
229
235
|
### Keys this process must never be
|
|
230
236
|
|
|
@@ -252,13 +258,45 @@ everything.
|
|
|
252
258
|
The active npub is printed on stderr on every start, so the answer to "which
|
|
253
259
|
key is this?" does not depend on asking the agent.
|
|
254
260
|
|
|
261
|
+
## Spending limits
|
|
262
|
+
|
|
263
|
+
**The hard limit is the wallet's own.** Point `NWC_URI_FILE` (or
|
|
264
|
+
`wallet connect <file>`) at a connection your wallet issued with a spending
|
|
265
|
+
budget. Whatever bray does, and whatever a model asks of it, the wallet will
|
|
266
|
+
not pay past that budget. Everything below is bray policing itself, which is
|
|
267
|
+
worth having but is not the same thing.
|
|
268
|
+
|
|
269
|
+
On top of that, every payment bray makes (`zap-send`, `marketplace-pay`, the
|
|
270
|
+
CLI's `zap-send` and `wallet pay`, the SDK, and payments made through the
|
|
271
|
+
scoped connections below) is checked before anything is sent:
|
|
272
|
+
|
|
273
|
+
- **Per payment:** at most `BRAY_MAX_PAYMENT_MSAT` (default 5,000 sats).
|
|
274
|
+
- **Per day:** at most `BRAY_MAX_DAILY_MSAT` in any rolling 24 hours (default
|
|
275
|
+
20,000 sats), counting a 1% (minimum 1 sat) fee reserve until the real fee
|
|
276
|
+
is known. Attempts are written to `payment-ledger.json` in the state
|
|
277
|
+
directory before the wallet is asked, so a restart does not hand the
|
|
278
|
+
allowance back.
|
|
279
|
+
- **Once per invoice:** an invoice is never paid twice, and an attempt whose
|
|
280
|
+
outcome is unknown is only retried after a lookup shows it failed.
|
|
281
|
+
|
|
282
|
+
`confirm: true` is set by the model, so it is not a human approval. When your
|
|
283
|
+
MCP client supports elicitation, `zap-send` and `marketplace-pay` also ask
|
|
284
|
+
you directly and pay only on an explicit yes; clients without elicitation
|
|
285
|
+
fall back to `confirm`, with the caps still applying. `zap-send` is not in
|
|
286
|
+
the default tool list either: it is reached through `search-actions`.
|
|
287
|
+
|
|
255
288
|
## Handing out a wallet connection
|
|
256
289
|
|
|
257
290
|
`zap-send` spends through the NWC URI you configured. That URI is an
|
|
258
291
|
unbounded capability over the wallet behind it: every method it supports, no
|
|
259
292
|
ceiling, until it is rotated. Handing one to an agent hands over everything.
|
|
260
293
|
|
|
261
|
-
`wallet-grant` issues connections over
|
|
294
|
+
`wallet-grant` issues scoped spending connections over that one wallet: one
|
|
295
|
+
wallet, one balance, and several narrower ways in. They are not separate
|
|
296
|
+
wallets. The tools that mint or widen spending authority (`wallet-grant`,
|
|
297
|
+
`wallet-refill`, `wallet-serve`) are registered only with
|
|
298
|
+
`BRAY_WALLET_SERVICE=1`; `wallet-grants` and `wallet-revoke` are always
|
|
299
|
+
available.
|
|
262
300
|
|
|
263
301
|
```
|
|
264
302
|
wallet-grant { name: "research-agent", methods: ["get_info","pay_invoice"],
|
|
@@ -269,11 +307,20 @@ wallet-refill { nameOrId: "research-agent", budgetMsat: 50000 }
|
|
|
269
307
|
wallet-serve { action: "start" }
|
|
270
308
|
```
|
|
271
309
|
|
|
310
|
+
`wallet-grant` writes the new connection's `nostr+walletconnect://` URI to a
|
|
311
|
+
`0600` file under `wallet-connections/` beside the grants file and returns
|
|
312
|
+
the path; the URI itself never appears in a tool result.
|
|
313
|
+
|
|
272
314
|
What a connection may do is an allowlist, and the default grants no spending
|
|
273
|
-
and does not disclose
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
and does not disclose anything: `get_info` and `make_invoice`. The others are
|
|
316
|
+
opt-in per connection, and **a connection that can spend must carry a
|
|
317
|
+
budget** - there is no unlimited grant to issue by accident. A connection
|
|
318
|
+
sees only itself:
|
|
319
|
+
|
|
320
|
+
- `get_balance` reports the connection's remaining budget, never the
|
|
321
|
+
wallet's balance.
|
|
322
|
+
- `lookup_invoice` answers only for invoices that connection issued or paid;
|
|
323
|
+
anything else is reported as not found.
|
|
277
324
|
|
|
278
325
|
The rules exist because a payment cannot be recalled:
|
|
279
326
|
|
|
@@ -286,8 +333,13 @@ The rules exist because a payment cannot be recalled:
|
|
|
286
333
|
`expiration` tag claims.
|
|
287
334
|
- Each connection has **its own service key**: two grants share nothing a
|
|
288
335
|
relay can correlate, and revoking one is deleting a key.
|
|
289
|
-
- The budget is charged what the **invoice** says, decoded here
|
|
290
|
-
|
|
336
|
+
- The budget is charged what the **invoice** says, decoded here, plus the
|
|
337
|
+
routing fee: a fee reserve is held until the wallet reports the real fee.
|
|
338
|
+
Invoices that state no amount are refused. A budget checked against a
|
|
339
|
+
figure the payer supplied is not a budget.
|
|
340
|
+
- A definite refusal from the wallet (insufficient balance, say) gives the
|
|
341
|
+
budget back, and the connection is not told why the wallet refused.
|
|
342
|
+
- bray's own per-payment and daily caps apply to these payments as well.
|
|
291
343
|
- A claimed payment whose preimage does not settle the invoice is refused
|
|
292
344
|
rather than passed on. Repeating an unproven claim would make this service
|
|
293
345
|
the one telling the lie.
|
|
@@ -298,6 +350,12 @@ as long as this process is alive. That is worth telling whoever you hand a URI
|
|
|
298
350
|
to: a connection that works while an MCP session is open is a different
|
|
299
351
|
promise from one that works overnight.
|
|
300
352
|
|
|
353
|
+
The file is the record, shared by every bray process on the machine. Only one
|
|
354
|
+
process may serve the grants at a time, the serving process re-reads a grant
|
|
355
|
+
before every request, and every change is a locked update of the file, so a
|
|
356
|
+
revocation or refill made from any process applies to the next request and
|
|
357
|
+
cannot be written away by another.
|
|
358
|
+
|
|
301
359
|
## CLI
|
|
302
360
|
|
|
303
361
|
```bash
|
|
@@ -335,6 +393,15 @@ See [llms.txt](./llms.txt) for a concise summary optimised for AI context window
|
|
|
335
393
|
| [dominion](https://github.com/forgesworn/dominion) | Epoch-based encrypted access control |
|
|
336
394
|
| [nostr-veil](https://github.com/forgesworn/nostr-veil) | Privacy-preserving Web of Trust |
|
|
337
395
|
|
|
396
|
+
## Support
|
|
397
|
+
|
|
398
|
+
For issues and feature requests, see [GitHub Issues](https://github.com/forgesworn/bray/issues).
|
|
399
|
+
|
|
400
|
+
If bray is useful to you, a tip is always welcome:
|
|
401
|
+
|
|
402
|
+
- Lightning: `profusemeat89@walletofsatoshi.com`
|
|
403
|
+
- Nostr zaps: `npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`
|
|
404
|
+
|
|
338
405
|
## Licence
|
|
339
406
|
|
|
340
407
|
MIT
|
package/dist/catalog.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ type ToolHandler = (args: any) => Promise<{
|
|
|
5
5
|
text: string;
|
|
6
6
|
}>;
|
|
7
7
|
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Tools registered directly on the MCP server, always in the client's tool
|
|
10
|
+
* list. Everything else is reached through search-actions + execute-action.
|
|
11
|
+
*
|
|
12
|
+
* Nothing that spends is promoted: a payment tool in the default list
|
|
13
|
+
* invites a model to reach for it. zap-send stays one search away.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROMOTED_TOOLS: ReadonlySet<string>;
|
|
8
16
|
/**
|
|
9
17
|
* Holds non-promoted tool definitions and exposes them via search-actions
|
|
10
18
|
* and execute-action meta-tools. This keeps the primary tool list lean
|
|
@@ -12,6 +20,8 @@ type ToolHandler = (args: any) => Promise<{
|
|
|
12
20
|
*/
|
|
13
21
|
export declare class ActionCatalog {
|
|
14
22
|
private readonly entries;
|
|
23
|
+
/** Promoted tools actually registered on the server, as counted by the proxy. */
|
|
24
|
+
promotedCount: number;
|
|
15
25
|
add(name: string, definition: any, handler: ToolHandler): void;
|
|
16
26
|
get size(): number;
|
|
17
27
|
search(intent: string): Array<{
|
|
@@ -31,7 +41,7 @@ export declare class ActionCatalog {
|
|
|
31
41
|
* Create a proxy around McpServer that intercepts registerTool calls.
|
|
32
42
|
* Promoted tools go to the real server; everything else goes to the catalog.
|
|
33
43
|
*/
|
|
34
|
-
export declare function createCatalogProxy(server: McpServer, catalog: ActionCatalog, promoted:
|
|
44
|
+
export declare function createCatalogProxy(server: McpServer, catalog: ActionCatalog, promoted: ReadonlySet<string>): McpServer;
|
|
35
45
|
/**
|
|
36
46
|
* Coerce execute-action's `params`/`parameters` argument into a plain object.
|
|
37
47
|
*
|
package/dist/catalog.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Tools registered directly on the MCP server, always in the client's tool
|
|
4
|
+
* list. Everything else is reached through search-actions + execute-action.
|
|
5
|
+
*
|
|
6
|
+
* Nothing that spends is promoted: a payment tool in the default list
|
|
7
|
+
* invites a model to reach for it. zap-send stays one search away.
|
|
8
|
+
*/
|
|
9
|
+
export const PROMOTED_TOOLS = new Set([
|
|
10
|
+
'whoami', 'social-post', 'social-reply', 'social-feed',
|
|
11
|
+
'dm-send', 'dm-read', 'zap-balance',
|
|
12
|
+
'identity-switch', 'relay-query',
|
|
13
|
+
'signet-badge', 'trust-score', 'vault-read',
|
|
14
|
+
'dispatch-send', 'dispatch-check', 'dispatch-reply',
|
|
15
|
+
'dispatch-ack', 'dispatch-status', 'dispatch-cancel',
|
|
16
|
+
'dispatch-refuse', 'dispatch-failure', 'dispatch-query',
|
|
17
|
+
'article-publish', 'article-read', 'article-list',
|
|
18
|
+
'search-notes', 'search-profiles', 'hashtag-feed',
|
|
19
|
+
'social-profile-get', 'dm-conversation', 'verify-person',
|
|
20
|
+
'dispatch-propose', 'dispatch-capability-publish', 'dispatch-capability-discover', 'dispatch-capability-read',
|
|
21
|
+
'badge-create', 'badge-award', 'badge-accept', 'badge-list',
|
|
22
|
+
'community-create', 'community-feed', 'community-post', 'community-approve', 'community-list',
|
|
23
|
+
'calendar-create', 'calendar-read', 'calendar-rsvp',
|
|
24
|
+
'listing-create', 'listing-read', 'listing-search', 'listing-close',
|
|
25
|
+
]);
|
|
2
26
|
/**
|
|
3
27
|
* Holds non-promoted tool definitions and exposes them via search-actions
|
|
4
28
|
* and execute-action meta-tools. This keeps the primary tool list lean
|
|
@@ -6,6 +30,8 @@ import { z } from 'zod';
|
|
|
6
30
|
*/
|
|
7
31
|
export class ActionCatalog {
|
|
8
32
|
entries = new Map();
|
|
33
|
+
/** Promoted tools actually registered on the server, as counted by the proxy. */
|
|
34
|
+
promotedCount = 0;
|
|
9
35
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
36
|
add(name, definition, handler) {
|
|
11
37
|
this.entries.set(name, {
|
|
@@ -88,7 +114,7 @@ export class ActionCatalog {
|
|
|
88
114
|
description: `Search ${catalog.size} additional actions by describing what you want to do. ` +
|
|
89
115
|
'Returns matching actions with names, descriptions, and parameter schemas. ' +
|
|
90
116
|
'Use this when the promoted tools (whoami, social-post, social-reply, social-feed, ' +
|
|
91
|
-
'dm-send, dm-read, zap-
|
|
117
|
+
'dm-send, dm-read, zap-balance, identity-switch, relay-query) ' +
|
|
92
118
|
'do not cover what you need.',
|
|
93
119
|
inputSchema: {
|
|
94
120
|
intent: z.string().describe('Describe what you want to do (e.g. "upload a file", "verify an attestation", ' +
|
|
@@ -171,6 +197,7 @@ export function createCatalogProxy(server, catalog, promoted) {
|
|
|
171
197
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
198
|
return (name, definition, handler) => {
|
|
173
199
|
if (promoted.has(name)) {
|
|
200
|
+
catalog.promotedCount++;
|
|
174
201
|
return target.registerTool(name, definition, handler);
|
|
175
202
|
}
|
|
176
203
|
catalog.add(name, definition, handler);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { handleZapSend, handleZapBalance, handleZapListTransactions, resolveNwcUri, loadWallets, saveWallets, parseNwcUri, normaliseNwcUriFile, } from '../../exports.js';
|
|
1
|
+
import { handleZapSend, handleZapBalance, handleZapListTransactions, resolveNwcUri, loadWallets, saveWallets, parseNwcUri, normaliseNwcUriFile, defaultPaymentGuard, } from '../../exports.js';
|
|
2
2
|
/**
|
|
3
3
|
* Dispatch `wallet *` subcommands (NIP-47 Nostr Wallet Connect).
|
|
4
4
|
*
|
|
@@ -50,6 +50,7 @@ export async function dispatch(cmd, _cmdArgs, h, ctx, pool, extras) {
|
|
|
50
50
|
const result = await handleZapSend(ctx, pool, {
|
|
51
51
|
invoice: req(1, 'wallet pay <bolt11>'),
|
|
52
52
|
nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri),
|
|
53
|
+
guard: defaultPaymentGuard(),
|
|
53
54
|
});
|
|
54
55
|
// A preimage can be an L402 bearer credential when the challenge macaroon
|
|
55
56
|
// is known. Report verified payment without printing the preimage.
|
package/dist/cli/commands/zap.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { handleZapSend, handleZapBalance, handleZapMakeInvoice, handleZapLookupInvoice, handleZapListTransactions, handleZapReceipts, handleZapDecode, resolveNwcUri } from '../../exports.js';
|
|
1
|
+
import { handleZapSend, handleZapBalance, handleZapMakeInvoice, handleZapLookupInvoice, handleZapListTransactions, handleZapReceipts, handleZapDecode, resolveNwcUri, defaultPaymentGuard } from '../../exports.js';
|
|
2
2
|
import * as fmt from '../../format.js';
|
|
3
3
|
export async function dispatch(cmd, cmdArgs, h, ctx, pool, extras) {
|
|
4
4
|
const { req, flag, out } = h;
|
|
5
5
|
const { globalNwcUri, walletsFile } = extras;
|
|
6
6
|
switch (cmd) {
|
|
7
7
|
case 'zap-send': {
|
|
8
|
-
const result = await handleZapSend(ctx, pool, { invoice: req(1, 'zap-send <bolt11>'), nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri) });
|
|
8
|
+
const result = await handleZapSend(ctx, pool, { invoice: req(1, 'zap-send <bolt11>'), nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri), guard: defaultPaymentGuard() });
|
|
9
9
|
out({ paid: true, verified: result.verified, paymentHash: result.paymentHash, feesPaidMsats: result.fees_paid });
|
|
10
10
|
break;
|
|
11
11
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ interface ConfigFile {
|
|
|
38
38
|
dispatchIdentities?: string;
|
|
39
39
|
/** Public keys this process must never sign as. See ContextOptions. */
|
|
40
40
|
forbidPubkeys?: string[];
|
|
41
|
+
/** Register the tools that mint spending connections (wallet-grant, wallet-refill, wallet-serve). */
|
|
42
|
+
walletService?: boolean;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Load config from a JSON file. Searches in order:
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { readNwcUriFile } from './zap/nwc-file.js';
|
|
5
|
+
import { paymentLimitsFromEnv } from './zap/payment-guard.js';
|
|
5
6
|
const NSEC_RE = /^nsec1[a-z0-9]{58}$/;
|
|
6
7
|
const HEX_RE = /^[0-9a-f]{64}$/;
|
|
7
8
|
const NCRYPTSEC_RE = /^ncryptsec1[a-z0-9]+$/;
|
|
@@ -225,6 +226,8 @@ export async function loadConfig() {
|
|
|
225
226
|
if (nwcFilePath) {
|
|
226
227
|
nwcUri = readNwcUriFile(nwcFilePath);
|
|
227
228
|
}
|
|
229
|
+
// A mistyped spending ceiling is a startup error, not a silent default.
|
|
230
|
+
paymentLimitsFromEnv();
|
|
228
231
|
// --- Relays ---
|
|
229
232
|
const relays = process.env.NOSTR_RELAYS
|
|
230
233
|
? parseRelays(process.env.NOSTR_RELAYS)
|
|
@@ -291,6 +294,11 @@ export async function loadConfig() {
|
|
|
291
294
|
]
|
|
292
295
|
.map((s) => s.trim())
|
|
293
296
|
.filter(Boolean);
|
|
297
|
+
// --- Wallet service ---
|
|
298
|
+
// Off unless asked for: wallet-grant, wallet-refill and wallet-serve hand
|
|
299
|
+
// out spending authority over the operator's wallet, which is not
|
|
300
|
+
// something a model should find in its tool list by default.
|
|
301
|
+
const walletService = process.env.BRAY_WALLET_SERVICE === '1' || file.walletService === true;
|
|
294
302
|
// --- Wallets file ---
|
|
295
303
|
const walletsFile = process.env.BRAY_WALLETS_FILE
|
|
296
304
|
?? file.walletsFile
|
|
@@ -318,5 +326,6 @@ export async function loadConfig() {
|
|
|
318
326
|
bindAddress,
|
|
319
327
|
dispatchIdentities,
|
|
320
328
|
forbidPubkeys,
|
|
329
|
+
walletService,
|
|
321
330
|
};
|
|
322
331
|
}
|
package/dist/dispatch/tools.js
CHANGED
|
@@ -262,7 +262,7 @@ export function registerDispatchTools(server, deps) {
|
|
|
262
262
|
server.registerTool('dispatch-capability-publish', {
|
|
263
263
|
description: 'Publish your agent\'s dispatch capabilities as a NIP-89 capability card (kind 31990). Lets other agents discover you on Nostr without needing a pre-shared identities file. The card advertises what task types you support, which repos you can work on, and your availability.',
|
|
264
264
|
inputSchema: {
|
|
265
|
-
name: z.string().describe('Agent name (e.g. "
|
|
265
|
+
name: z.string().describe('Agent name (e.g. "research-agent", "forge-worker")'),
|
|
266
266
|
description: z.string().describe('What this agent does (e.g. "Full-stack TypeScript agent for TROTT ecosystem")'),
|
|
267
267
|
task_types: z.array(z.string()).describe('Supported task types: "think", "build", or custom types'),
|
|
268
268
|
repos: z.array(z.string()).optional().describe('Repository names this agent can work on (e.g. ["toll-booth", "nsec-tree"])'),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
|
+
// A short-lived advisory lock between bray processes that share one state
|
|
3
|
+
// file. Held only for a read-modify-write, never across a network call, so
|
|
4
|
+
// waiting for it is a matter of milliseconds.
|
|
5
|
+
//
|
|
6
|
+
// The lock is a file created with O_EXCL. A holder that died leaves it
|
|
7
|
+
// behind; it is taken over once its process is gone or it is older than
|
|
8
|
+
// anything a read-modify-write could plausibly take.
|
|
9
|
+
const STALE_MS = 10_000;
|
|
10
|
+
const WAIT_MS = 5_000;
|
|
11
|
+
const SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
12
|
+
function holderIsGone(lockPath) {
|
|
13
|
+
try {
|
|
14
|
+
const age = Date.now() - statSync(lockPath).mtimeMs;
|
|
15
|
+
if (age > STALE_MS)
|
|
16
|
+
return true;
|
|
17
|
+
const pid = Number.parseInt(readFileSync(lockPath, 'utf8'), 10);
|
|
18
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
19
|
+
return false;
|
|
20
|
+
if (pid === process.pid)
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return err.code === 'ESRCH';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// vanished between the failed create and this look: try again
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Run `fn` while holding an exclusive lock at `${path}.lock`. */
|
|
36
|
+
export function withFileLock(path, fn) {
|
|
37
|
+
const lockPath = `${path}.lock`;
|
|
38
|
+
const deadline = Date.now() + WAIT_MS;
|
|
39
|
+
let descriptor;
|
|
40
|
+
while (descriptor === undefined) {
|
|
41
|
+
try {
|
|
42
|
+
descriptor = openSync(lockPath, 'wx', 0o600);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err.code !== 'EEXIST')
|
|
46
|
+
throw err;
|
|
47
|
+
if (holderIsGone(lockPath)) {
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(lockPath);
|
|
50
|
+
}
|
|
51
|
+
catch { /* another waiter took it over first */ }
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (Date.now() > deadline) {
|
|
55
|
+
throw new Error(`Timed out waiting for ${lockPath}; another bray process is holding it.`);
|
|
56
|
+
}
|
|
57
|
+
Atomics.wait(SLEEP, 0, 0, 10);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
writeSync(descriptor, String(process.pid));
|
|
62
|
+
return fn();
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
closeSync(descriptor);
|
|
66
|
+
try {
|
|
67
|
+
unlinkSync(lockPath);
|
|
68
|
+
}
|
|
69
|
+
catch { /* already gone */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bearer token the HTTP transport requires.
|
|
3
|
+
*
|
|
4
|
+
* A token the operator supplied in BRAY_HTTP_TOKEN is never echoed: they
|
|
5
|
+
* already have it, and stderr ends up in logs and process supervisors. A
|
|
6
|
+
* generated one is printed once, because nobody else can know it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveHttpToken(env?: NodeJS.ProcessEnv, log?: (message: string) => void): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* The bearer token the HTTP transport requires.
|
|
4
|
+
*
|
|
5
|
+
* A token the operator supplied in BRAY_HTTP_TOKEN is never echoed: they
|
|
6
|
+
* already have it, and stderr ends up in logs and process supervisors. A
|
|
7
|
+
* generated one is printed once, because nobody else can know it.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveHttpToken(env = process.env, log = (message) => console.error(message)) {
|
|
10
|
+
const supplied = env.BRAY_HTTP_TOKEN?.trim();
|
|
11
|
+
if (supplied) {
|
|
12
|
+
log('nostr-bray HTTP auth: using the token from BRAY_HTTP_TOKEN');
|
|
13
|
+
return supplied;
|
|
14
|
+
}
|
|
15
|
+
const generated = randomUUID();
|
|
16
|
+
log(`nostr-bray HTTP auth token: ${generated}`);
|
|
17
|
+
return generated;
|
|
18
|
+
}
|
package/dist/identity/tools.d.ts
CHANGED
|
@@ -3,12 +3,15 @@ import type { SigningContext } from '../signing-context.js';
|
|
|
3
3
|
import type { RelayPool } from '../relay-pool.js';
|
|
4
4
|
import type { Nip65Manager } from '../nip65.js';
|
|
5
5
|
import type { TrustContext } from '../trust-context.js';
|
|
6
|
+
import type { NwcTransport } from '@forgesworn/nwc-kit';
|
|
6
7
|
export interface ToolDeps {
|
|
7
8
|
ctx: SigningContext;
|
|
8
9
|
pool: RelayPool;
|
|
9
10
|
nip65: Nip65Manager;
|
|
10
11
|
nwcUri?: string;
|
|
11
12
|
walletsFile: string;
|
|
13
|
+
/** NWC relay transport override. Tests only; production uses the default relays. */
|
|
14
|
+
nwcTransport?: NwcTransport;
|
|
12
15
|
nip04Enabled?: boolean;
|
|
13
16
|
veilCacheTtl?: number;
|
|
14
17
|
veilCacheMax?: number;
|
package/dist/index.js
CHANGED
|
@@ -4,27 +4,11 @@ import { loadConfig } from './config.js';
|
|
|
4
4
|
import { IdentityContext } from './context.js';
|
|
5
5
|
import { RelayPool } from './relay-pool.js';
|
|
6
6
|
import { Nip65Manager } from './nip65.js';
|
|
7
|
-
import { registerIdentityTools } from './identity/tools.js';
|
|
8
|
-
import { registerSocialTools } from './social/tools.js';
|
|
9
|
-
import { registerTrustTools } from './trust/tools.js';
|
|
10
|
-
import { registerRelayTools } from './relay/tools.js';
|
|
11
|
-
import { registerRelayIntelligenceTools } from './relay/intelligence-tools.js';
|
|
12
|
-
import { registerZapTools } from './zap/tools.js';
|
|
13
|
-
import { registerWalletServiceTools } from './wallet-service/tools.js';
|
|
14
|
-
import { registerSafetyTools } from './safety/tools.js';
|
|
15
|
-
import { registerUtilTools } from './util/tools.js';
|
|
16
|
-
import { registerWorkflowTools } from './workflow/tools.js';
|
|
17
|
-
import { registerMarketplaceTools } from './marketplace/tools.js';
|
|
18
|
-
import { registerPrivacyTools } from './privacy/tools.js';
|
|
19
|
-
import { registerModerationTools } from './moderation/tools.js';
|
|
20
7
|
import { TrustContext } from './trust-context.js';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { registerDispatchTools } from './dispatch/tools.js';
|
|
24
|
-
import { registerHandlerTools } from './handler/tools.js';
|
|
25
|
-
import { registerSyncTools } from './sync/tools.js';
|
|
26
|
-
import { ActionCatalog, createCatalogProxy } from './catalog.js';
|
|
8
|
+
import { ActionCatalog, createCatalogProxy, PROMOTED_TOOLS } from './catalog.js';
|
|
9
|
+
import { registerAllTools } from './tool-groups.js';
|
|
27
10
|
import { configureHttpClient } from './http-client.js';
|
|
11
|
+
import { BRAY_VERSION } from './version.js';
|
|
28
12
|
const config = await loadConfig();
|
|
29
13
|
// Route every fetch() in this process through the SOCKS proxy when Tor is
|
|
30
14
|
// configured, before any HTTP-using code runs. Structural protection
|
|
@@ -135,58 +119,24 @@ config.nwcUri = undefined;
|
|
|
135
119
|
config.bunkerUri = undefined;
|
|
136
120
|
// NIP-65 relay list is loaded via loadIdentityRelays(): immediately for a local
|
|
137
121
|
// key, or after the bunker pubkey resolves (see establishBunkerInBackground).
|
|
138
|
-
const server = new McpServer({ name: 'nostr-bray', version:
|
|
122
|
+
const server = new McpServer({ name: 'nostr-bray', version: BRAY_VERSION }, {
|
|
139
123
|
instructions: 'Always check whoami before posting or signing. Use signet-badge to check trust before interacting with unfamiliar pubkeys. Use trust-score for the full three-dimensional view (verification + proximity + access). Use social-feed or social-notifications to get event IDs and author pubkeys before calling social-reply or social-react. DMs default to NIP-17 gift wrap (most private); only use NIP-04 if the recipient requires it. Respect vault tiers -- do not share decrypted content outside its intended audience. For less common actions, use search-actions to discover them, then execute-action to run them.',
|
|
140
124
|
});
|
|
141
125
|
// Promoted tools are registered directly with the server (always visible to Claude).
|
|
142
126
|
// Everything else goes to the catalog, discoverable via search-actions + execute-action.
|
|
143
|
-
const PROMOTED = new Set([
|
|
144
|
-
'whoami', 'social-post', 'social-reply', 'social-feed',
|
|
145
|
-
'dm-send', 'dm-read', 'zap-send', 'zap-balance',
|
|
146
|
-
'identity-switch', 'relay-query',
|
|
147
|
-
'signet-badge', 'trust-score', 'vault-read',
|
|
148
|
-
'dispatch-send', 'dispatch-check', 'dispatch-reply',
|
|
149
|
-
'dispatch-ack', 'dispatch-status', 'dispatch-cancel',
|
|
150
|
-
'dispatch-refuse', 'dispatch-failure', 'dispatch-query',
|
|
151
|
-
'article-publish', 'article-read', 'article-list',
|
|
152
|
-
'search-notes', 'search-profiles', 'hashtag-feed',
|
|
153
|
-
'social-profile-get', 'dm-conversation', 'verify-person',
|
|
154
|
-
'dispatch-propose', 'dispatch-capability-publish', 'dispatch-capability-discover', 'dispatch-capability-read',
|
|
155
|
-
'badge-create', 'badge-award', 'badge-accept', 'badge-list',
|
|
156
|
-
'community-create', 'community-feed', 'community-post', 'community-approve', 'community-list',
|
|
157
|
-
'calendar-create', 'calendar-read', 'calendar-rsvp',
|
|
158
|
-
'listing-create', 'listing-read', 'listing-search', 'listing-close',
|
|
159
|
-
]);
|
|
160
127
|
const catalog = new ActionCatalog();
|
|
161
|
-
const proxy = createCatalogProxy(server, catalog,
|
|
162
|
-
// Register all tools
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
registerTrustTools(proxy, deps);
|
|
166
|
-
registerRelayTools(proxy, deps);
|
|
167
|
-
registerRelayIntelligenceTools(proxy, deps);
|
|
168
|
-
registerZapTools(proxy, deps);
|
|
169
|
-
registerWalletServiceTools(proxy, deps);
|
|
170
|
-
registerSafetyTools(proxy, deps);
|
|
171
|
-
registerUtilTools(proxy, deps);
|
|
172
|
-
registerWorkflowTools(proxy, {
|
|
173
|
-
ctx: deps.ctx,
|
|
174
|
-
pool: deps.pool,
|
|
175
|
-
nip65: deps.nip65,
|
|
128
|
+
const proxy = createCatalogProxy(server, catalog, PROMOTED_TOOLS);
|
|
129
|
+
// Register all tools. The proxy routes promoted ones to the server and the
|
|
130
|
+
// rest to the catalog.
|
|
131
|
+
registerAllTools(proxy, deps, {
|
|
176
132
|
veilCacheTtl: config.veilCacheTtl,
|
|
177
133
|
veilCacheMax: config.veilCacheMax,
|
|
134
|
+
...(config.dispatchIdentities ? { dispatchIdentitiesPath: config.dispatchIdentities } : {}),
|
|
135
|
+
walletService: config.walletService,
|
|
178
136
|
});
|
|
179
|
-
registerMarketplaceTools(proxy, deps);
|
|
180
|
-
registerPrivacyTools(proxy, deps);
|
|
181
|
-
registerModerationTools(proxy, deps);
|
|
182
|
-
registerSignetTools(proxy, deps);
|
|
183
|
-
registerVaultTools(proxy, deps);
|
|
184
|
-
registerDispatchTools(proxy, { ...deps, dispatchIdentitiesPath: config.dispatchIdentities });
|
|
185
|
-
registerHandlerTools(proxy, deps);
|
|
186
|
-
registerSyncTools(proxy, deps);
|
|
187
137
|
// Add search-actions and execute-action meta-tools to the real server
|
|
188
138
|
catalog.registerMetaTools(server);
|
|
189
|
-
console.error(`nostr-bray: ${
|
|
139
|
+
console.error(`nostr-bray: ${catalog.promotedCount} promoted tools + ${catalog.size} cataloged (${catalog.promotedCount + catalog.size + 2} total)`);
|
|
190
140
|
if (config.transport === 'stdio') {
|
|
191
141
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
192
142
|
await server.connect(new StdioServerTransport());
|
|
@@ -197,8 +147,8 @@ else {
|
|
|
197
147
|
const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
198
148
|
const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
|
|
199
149
|
const { randomUUID, timingSafeEqual } = await import('node:crypto');
|
|
200
|
-
const
|
|
201
|
-
|
|
150
|
+
const { resolveHttpToken } = await import('./http-token.js');
|
|
151
|
+
const token = resolveHttpToken();
|
|
202
152
|
const expectedAuth = Buffer.from(`Bearer ${token}`);
|
|
203
153
|
// Sliding window rate limiter (per-IP).
|
|
204
154
|
// Cap the map size so a caller rotating source IPs cannot grow it without
|
|
@@ -133,11 +133,15 @@ export interface PayResult {
|
|
|
133
133
|
credentialId: string;
|
|
134
134
|
costSats?: number;
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
export declare function getCredential(id: string): {
|
|
136
|
+
interface StoredCredential {
|
|
138
137
|
macaroon: string;
|
|
139
138
|
preimage: string;
|
|
140
|
-
|
|
139
|
+
origin: string;
|
|
140
|
+
}
|
|
141
|
+
/** The origin a credential is bound to, normalised as `URL.origin`. */
|
|
142
|
+
export declare function credentialOrigin(url: string): string;
|
|
143
|
+
export declare function storeCredential(id: string, macaroon: string, preimage: string, url: string): void;
|
|
144
|
+
export declare function getCredential(id: string): StoredCredential | undefined;
|
|
141
145
|
export declare function clearCredentials(): void;
|
|
142
146
|
/** Build the L402 Authorization header from stored credentials. */
|
|
143
147
|
export declare function buildL402AuthHeader(credentialId: string): string | null;
|
|
@@ -194,3 +198,4 @@ export declare function handleMarketplaceRetire(ctx: SigningContext, pool: Relay
|
|
|
194
198
|
event: NostrEvent;
|
|
195
199
|
publish: PublishResult;
|
|
196
200
|
}>;
|
|
201
|
+
export {};
|
|
@@ -310,10 +310,23 @@ export function extractBolt11AmountSats(bolt11) {
|
|
|
310
310
|
}
|
|
311
311
|
return undefined;
|
|
312
312
|
}
|
|
313
|
-
// In-memory credential store for L402 tokens (per-session, not persisted)
|
|
314
313
|
const credentialStore = new Map();
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
/** The origin a credential is bound to, normalised as `URL.origin`. */
|
|
315
|
+
export function credentialOrigin(url) {
|
|
316
|
+
let parsed;
|
|
317
|
+
try {
|
|
318
|
+
parsed = new URL(url);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
throw new Error('The URL the L402 challenge came from is not a valid URL.');
|
|
322
|
+
}
|
|
323
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
324
|
+
throw new Error('L402 credentials can only be bound to an http(s) origin.');
|
|
325
|
+
}
|
|
326
|
+
return parsed.origin;
|
|
327
|
+
}
|
|
328
|
+
export function storeCredential(id, macaroon, preimage, url) {
|
|
329
|
+
credentialStore.set(id, { macaroon, preimage, origin: credentialOrigin(url) });
|
|
317
330
|
}
|
|
318
331
|
export function getCredential(id) {
|
|
319
332
|
return credentialStore.get(id);
|
|
@@ -331,10 +344,14 @@ export function buildL402AuthHeader(credentialId) {
|
|
|
331
344
|
/** Make an authenticated API call using L402 credentials. */
|
|
332
345
|
export async function handleMarketplaceCall(args) {
|
|
333
346
|
validatePublicUrl(args.url);
|
|
347
|
+
const credential = credentialStore.get(args.credentialId);
|
|
334
348
|
const authHeader = buildL402AuthHeader(args.credentialId);
|
|
335
|
-
if (!authHeader) {
|
|
349
|
+
if (!credential || !authHeader) {
|
|
336
350
|
throw new Error(`No credentials found for ID "${args.credentialId}". Use marketplace-pay first.`);
|
|
337
351
|
}
|
|
352
|
+
if (credentialOrigin(args.url) !== credential.origin) {
|
|
353
|
+
throw new Error(`That credential was paid for ${credential.origin} and is only sent there; refusing to send it to ${credentialOrigin(args.url)}.`);
|
|
354
|
+
}
|
|
338
355
|
const headers = {
|
|
339
356
|
...args.headers,
|
|
340
357
|
Authorization: authHeader,
|