nostr-bray 2.3.0 → 3.0.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 +5 -1
- package/dist/bunker.js +25 -3
- package/dist/cli/commands/identity.js +17 -3
- package/dist/cli/commands/wallet.d.ts +2 -2
- package/dist/cli/commands/wallet.js +13 -11
- package/dist/cli/commands/zap.js +4 -2
- package/dist/cli/index.js +3 -3
- package/dist/config.js +46 -31
- package/dist/context.d.ts +2 -0
- package/dist/context.js +4 -0
- package/dist/help.js +4 -4
- package/dist/marketplace/tools.js +9 -6
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +1 -1
- package/dist/social/blossom.d.ts +1 -0
- package/dist/social/blossom.js +31 -1
- package/dist/zap/handlers.d.ts +48 -144
- package/dist/zap/handlers.js +177 -223
- package/dist/zap/index.d.ts +1 -0
- package/dist/zap/index.js +1 -0
- package/dist/zap/nwc-file.d.ts +5 -0
- package/dist/zap/nwc-file.js +65 -0
- package/dist/zap/tools.js +20 -15
- package/llms-full.txt +7 -6
- package/llms.txt +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -212,13 +212,17 @@ Search order: `BRAY_CONFIG` env var > `$XDG_CONFIG_HOME/bray/config.json` > `~/.
|
|
|
212
212
|
| `NOSTR_SECRET_KEY_FILE` | Path to secret key file |
|
|
213
213
|
| `NOSTR_NCRYPTSEC` | NIP-49 encrypted key |
|
|
214
214
|
| `NOSTR_NCRYPTSEC_PASSWORD` | Password for ncryptsec |
|
|
215
|
+
| `NWC_URI_FILE` | Path to a private `0600` file containing the NWC bearer URI |
|
|
215
216
|
| `NOSTR_RELAYS` | Comma-separated relay URLs |
|
|
216
217
|
| `TOR_PROXY` | SOCKS5h proxy for Tor |
|
|
217
218
|
| `NIP04_ENABLED` | Set `1` to enable legacy NIP-04 DMs |
|
|
218
219
|
| `TRANSPORT` | `stdio` (default) or `http` |
|
|
219
220
|
| `PORT` | HTTP port (default 3000) |
|
|
220
221
|
|
|
221
|
-
All secret env vars are deleted from `process.env`
|
|
222
|
+
All secret env vars are deleted from `process.env` before parsing can fail.
|
|
223
|
+
Raw `NWC_URI` is refused; use `NWC_URI_FILE` or `wallet connect <nwc-file>` so
|
|
224
|
+
the bearer credential never appears in a process environment, command argument
|
|
225
|
+
or MCP tool argument.
|
|
222
226
|
|
|
223
227
|
## CLI
|
|
224
228
|
|
package/dist/bunker.js
CHANGED
|
@@ -86,11 +86,33 @@ export function startBunker(opts) {
|
|
|
86
86
|
case 'get_public_key':
|
|
87
87
|
result = ctx.activePublicKeyHex;
|
|
88
88
|
break;
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// `sign_event_compact` is the Heartwood dialect, and Bray speaks it so a
|
|
90
|
+
// client does not have to keep two code paths or waste a round trip
|
|
91
|
+
// discovering it is unsupported.
|
|
92
|
+
//
|
|
93
|
+
// Two differences from NIP-46, both about not moving the event around as
|
|
94
|
+
// a string. params[0] may be the event OBJECT rather than a stringified
|
|
95
|
+
// event, which spares a signer an unescape pass costing twice the content
|
|
96
|
+
// in one allocation; and the compact reply returns only what the client
|
|
97
|
+
// cannot work out itself. On a hardware signer those two copies are the
|
|
98
|
+
// difference between a 12 KB and an 18 KB ceiling. Bray has no such
|
|
99
|
+
// limit, but matching the dialect keeps clients simple.
|
|
100
|
+
case 'sign_event':
|
|
101
|
+
case 'sign_event_compact': {
|
|
102
|
+
// Declared string[], but that was always a claim about the client
|
|
103
|
+
// rather than a guarantee, and here it is genuinely either.
|
|
104
|
+
const raw = request.params[0];
|
|
105
|
+
const template = (typeof raw === 'string' ? JSON.parse(raw) : raw);
|
|
91
106
|
const sign = ctx.getSigningFunction();
|
|
92
107
|
const signed = await sign(template);
|
|
93
|
-
result =
|
|
108
|
+
result = request.method === 'sign_event_compact'
|
|
109
|
+
? JSON.stringify({
|
|
110
|
+
id: signed.id,
|
|
111
|
+
pubkey: signed.pubkey,
|
|
112
|
+
created_at: signed.created_at,
|
|
113
|
+
sig: signed.sig,
|
|
114
|
+
})
|
|
115
|
+
: JSON.stringify(signed);
|
|
94
116
|
break;
|
|
95
117
|
}
|
|
96
118
|
case 'nip04_encrypt': {
|
|
@@ -18,11 +18,25 @@ export async function dispatch(cmd, cmdArgs, h, ctx, pool) {
|
|
|
18
18
|
case 'persona':
|
|
19
19
|
out(await ctx.derivePersona(req(1, 'persona <name> [index]'), parseInt(cmdArgs[2] ?? '0', 10)));
|
|
20
20
|
break;
|
|
21
|
-
case 'switch':
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
case 'switch': {
|
|
22
|
+
const target = req(1, 'switch <target> [index]');
|
|
23
|
+
await ctx.switch(target, cmdArgs[2] ? parseInt(cmdArgs[2], 10) : undefined);
|
|
24
|
+
// Name the derivation path, not just the npub. A bare name means the
|
|
25
|
+
// persona path, so `switch work` and `derive work` are different keys;
|
|
26
|
+
// without the path on screen the difference is invisible until something
|
|
27
|
+
// has been signed by the wrong identity.
|
|
28
|
+
console.error(`Now signing as ${ctx.activeNpub} (${ctx.activePurpose})`);
|
|
29
|
+
if (target !== 'master' && !target.includes(':') && ctx.activePurpose === `persona/${target}`) {
|
|
30
|
+
console.error(`Note: "${target}" resolved to the persona path. The raw purpose ` +
|
|
31
|
+
`"${target}" is a different key — see \`derive ${target}\`.`);
|
|
32
|
+
}
|
|
33
|
+
// The CLI is one process per command, so this applies to the rest of this
|
|
34
|
+
// invocation only. Say so rather than implying a saved default.
|
|
35
|
+
console.error('This switch applies to this command only; use --key or --bunker to sign as');
|
|
36
|
+
console.error('another identity in a later command.');
|
|
24
37
|
console.log(ctx.activeNpub);
|
|
25
38
|
break;
|
|
39
|
+
}
|
|
26
40
|
case 'prove':
|
|
27
41
|
out(await handleIdentityProve(ctx, { mode: (cmdArgs[1] === 'full' ? 'full' : 'blind') }));
|
|
28
42
|
break;
|
|
@@ -7,8 +7,8 @@ export interface WalletExtras {
|
|
|
7
7
|
* Dispatch `wallet *` subcommands (NIP-47 Nostr Wallet Connect).
|
|
8
8
|
*
|
|
9
9
|
* Commands:
|
|
10
|
-
* wallet connect <nwc-
|
|
11
|
-
* wallet disconnect Remove the stored NWC
|
|
10
|
+
* wallet connect <nwc-file> Store an NWC secret-file reference for the active identity
|
|
11
|
+
* wallet disconnect Remove the stored NWC file reference for the active identity
|
|
12
12
|
* wallet status Show configured wallet pubkey and relay
|
|
13
13
|
* wallet pay <bolt11> Pay a Lightning invoice via NWC
|
|
14
14
|
* wallet balance Request wallet balance via NWC
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { handleZapSend, handleZapBalance, handleZapListTransactions, resolveNwcUri, loadWallets, saveWallets, parseNwcUri, } from '../../exports.js';
|
|
1
|
+
import { handleZapSend, handleZapBalance, handleZapListTransactions, resolveNwcUri, loadWallets, saveWallets, parseNwcUri, normaliseNwcUriFile, } from '../../exports.js';
|
|
2
2
|
/**
|
|
3
3
|
* Dispatch `wallet *` subcommands (NIP-47 Nostr Wallet Connect).
|
|
4
4
|
*
|
|
5
5
|
* Commands:
|
|
6
|
-
* wallet connect <nwc-
|
|
7
|
-
* wallet disconnect Remove the stored NWC
|
|
6
|
+
* wallet connect <nwc-file> Store an NWC secret-file reference for the active identity
|
|
7
|
+
* wallet disconnect Remove the stored NWC file reference for the active identity
|
|
8
8
|
* wallet status Show configured wallet pubkey and relay
|
|
9
9
|
* wallet pay <bolt11> Pay a Lightning invoice via NWC
|
|
10
10
|
* wallet balance Request wallet balance via NWC
|
|
@@ -15,13 +15,11 @@ export async function dispatch(cmd, _cmdArgs, h, ctx, pool, extras) {
|
|
|
15
15
|
const { globalNwcUri, walletsFile } = extras;
|
|
16
16
|
switch (cmd) {
|
|
17
17
|
case 'wallet-connect': {
|
|
18
|
-
const
|
|
19
|
-
// Validate URI before storing
|
|
20
|
-
parseNwcUri(uri);
|
|
18
|
+
const secretFile = normaliseNwcUriFile(req(1, 'wallet connect <nwc-file>'));
|
|
21
19
|
const pubkey = ctx.activePublicKeyHex;
|
|
22
20
|
const npub = ctx.activeNpub;
|
|
23
21
|
const wallets = loadWallets(walletsFile);
|
|
24
|
-
wallets[pubkey] =
|
|
22
|
+
wallets[pubkey] = secretFile;
|
|
25
23
|
saveWallets(walletsFile, wallets);
|
|
26
24
|
out({ ok: true, identity: npub, message: `Wallet configured for ${npub}` });
|
|
27
25
|
break;
|
|
@@ -40,7 +38,7 @@ export async function dispatch(cmd, _cmdArgs, h, ctx, pool, extras) {
|
|
|
40
38
|
const npub = ctx.activeNpub;
|
|
41
39
|
const uri = resolveNwcUri(ctx, walletsFile, globalNwcUri);
|
|
42
40
|
if (!uri) {
|
|
43
|
-
out({ ok: false, identity: npub, configured: false, message: 'No wallet configured. Use `wallet connect <nwc-
|
|
41
|
+
out({ ok: false, identity: npub, configured: false, message: 'No wallet configured. Use `wallet connect <nwc-file>` or set NWC_URI_FILE.' });
|
|
44
42
|
break;
|
|
45
43
|
}
|
|
46
44
|
const conn = parseNwcUri(uri);
|
|
@@ -48,12 +46,16 @@ export async function dispatch(cmd, _cmdArgs, h, ctx, pool, extras) {
|
|
|
48
46
|
out({ ok: true, identity: npub, configured: true, walletPubkey: conn.pubkey, relay: conn.relay });
|
|
49
47
|
break;
|
|
50
48
|
}
|
|
51
|
-
case 'wallet-pay':
|
|
52
|
-
|
|
49
|
+
case 'wallet-pay': {
|
|
50
|
+
const result = await handleZapSend(ctx, pool, {
|
|
53
51
|
invoice: req(1, 'wallet pay <bolt11>'),
|
|
54
52
|
nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri),
|
|
55
|
-
})
|
|
53
|
+
});
|
|
54
|
+
// A preimage can be an L402 bearer credential when the challenge macaroon
|
|
55
|
+
// is known. Report verified payment without printing the preimage.
|
|
56
|
+
out({ paid: true, verified: result.verified, paymentHash: result.paymentHash, feesPaidMsats: result.fees_paid });
|
|
56
57
|
break;
|
|
58
|
+
}
|
|
57
59
|
case 'wallet-balance':
|
|
58
60
|
out(await handleZapBalance(ctx, pool, { nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri) }));
|
|
59
61
|
break;
|
package/dist/cli/commands/zap.js
CHANGED
|
@@ -4,9 +4,11 @@ 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
|
-
case 'zap-send':
|
|
8
|
-
|
|
7
|
+
case 'zap-send': {
|
|
8
|
+
const result = await handleZapSend(ctx, pool, { invoice: req(1, 'zap-send <bolt11>'), nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri) });
|
|
9
|
+
out({ paid: true, verified: result.verified, paymentHash: result.paymentHash, feesPaidMsats: result.fees_paid });
|
|
9
10
|
break;
|
|
11
|
+
}
|
|
10
12
|
case 'zap-balance':
|
|
11
13
|
out(await handleZapBalance(ctx, pool, { nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri) }));
|
|
12
14
|
break;
|
package/dist/cli/index.js
CHANGED
|
@@ -182,8 +182,8 @@ Admin (NIP-86 relay management):
|
|
|
182
182
|
listdisallowedkinds; they were never NIP-86 method names)
|
|
183
183
|
|
|
184
184
|
Wallet (NIP-47 Nostr Wallet Connect):
|
|
185
|
-
wallet connect <nwc-
|
|
186
|
-
wallet disconnect Remove stored NWC
|
|
185
|
+
wallet connect <nwc-file> Store a private NWC secret-file reference
|
|
186
|
+
wallet disconnect Remove stored NWC file reference
|
|
187
187
|
wallet status Show configured wallet pubkey and relay
|
|
188
188
|
wallet pay <bolt11> Pay a Lightning invoice via NWC
|
|
189
189
|
wallet balance Request wallet balance via NWC
|
|
@@ -261,7 +261,7 @@ Environment:
|
|
|
261
261
|
BUNKER_URI / BUNKER_URI_FILE bunker:// URI (use INSTEAD of secret key)
|
|
262
262
|
NOSTR_RELAYS Comma-separated relay URLs
|
|
263
263
|
NOSTR_AUTH NIP-42 policy: off (default), on-demand, eager
|
|
264
|
-
|
|
264
|
+
NWC_URI_FILE Private 0600 file containing an NWC URI
|
|
265
265
|
TOR_PROXY SOCKS5h proxy URL
|
|
266
266
|
NOSTR_BRAY_OUTPUT Default output: "human" (default) or "json"
|
|
267
267
|
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { readNwcUriFile } from './zap/nwc-file.js';
|
|
4
5
|
const NSEC_RE = /^nsec1[a-z0-9]{58}$/;
|
|
5
6
|
const HEX_RE = /^[0-9a-f]{64}$/;
|
|
6
7
|
const NCRYPTSEC_RE = /^ncryptsec1[a-z0-9]+$/;
|
|
@@ -85,10 +86,34 @@ export function loadConfigFile() {
|
|
|
85
86
|
/** Load configuration from config file, environment variables, and secret files.
|
|
86
87
|
* Priority: env vars > config file > defaults */
|
|
87
88
|
export async function loadConfig() {
|
|
89
|
+
// Capture then remove bearer credentials before any parsing or validation can
|
|
90
|
+
// throw. This keeps failure paths from leaving secrets in process.env.
|
|
91
|
+
const secretEnvironment = {
|
|
92
|
+
secretKey: process.env.NOSTR_SECRET_KEY,
|
|
93
|
+
secretKeyFile: process.env.NOSTR_SECRET_KEY_FILE,
|
|
94
|
+
bunkerUri: process.env.BUNKER_URI,
|
|
95
|
+
bunkerUriFile: process.env.BUNKER_URI_FILE,
|
|
96
|
+
ncryptsec: process.env.NOSTR_NCRYPTSEC,
|
|
97
|
+
ncryptsecFile: process.env.NOSTR_NCRYPTSEC_FILE,
|
|
98
|
+
ncryptsecPassword: process.env.NOSTR_NCRYPTSEC_PASSWORD,
|
|
99
|
+
ncryptsecPasswordFile: process.env.NOSTR_NCRYPTSEC_PASSWORD_FILE,
|
|
100
|
+
nwcUri: process.env.NWC_URI,
|
|
101
|
+
nwcUriFile: process.env.NWC_URI_FILE,
|
|
102
|
+
};
|
|
103
|
+
delete process.env.NOSTR_SECRET_KEY;
|
|
104
|
+
delete process.env.NOSTR_SECRET_KEY_FILE;
|
|
105
|
+
delete process.env.NWC_URI;
|
|
106
|
+
delete process.env.NWC_URI_FILE;
|
|
107
|
+
delete process.env.BUNKER_URI;
|
|
108
|
+
delete process.env.BUNKER_URI_FILE;
|
|
109
|
+
delete process.env.NOSTR_NCRYPTSEC;
|
|
110
|
+
delete process.env.NOSTR_NCRYPTSEC_FILE;
|
|
111
|
+
delete process.env.NOSTR_NCRYPTSEC_PASSWORD;
|
|
112
|
+
delete process.env.NOSTR_NCRYPTSEC_PASSWORD_FILE;
|
|
88
113
|
const file = loadConfigFile();
|
|
89
114
|
// --- Secret key ---
|
|
90
|
-
const keyFilePath =
|
|
91
|
-
const keyEnvVar =
|
|
115
|
+
const keyFilePath = secretEnvironment.secretKeyFile ?? file.secretKeyFile;
|
|
116
|
+
const keyEnvVar = secretEnvironment.secretKey;
|
|
92
117
|
let secretKey;
|
|
93
118
|
// A local key source is an explicit choice to sign locally. A bunker URI
|
|
94
119
|
// saved by `bunker connect` is only a fallback and must not override that
|
|
@@ -96,15 +121,15 @@ export async function loadConfig() {
|
|
|
96
121
|
// it is present. Explicit bunker configuration still takes precedence.
|
|
97
122
|
const hasLocalKeySource = !!(keyFilePath
|
|
98
123
|
|| keyEnvVar
|
|
99
|
-
||
|
|
100
|
-
||
|
|
124
|
+
|| secretEnvironment.ncryptsecFile
|
|
125
|
+
|| secretEnvironment.ncryptsec
|
|
101
126
|
|| file.ncryptsecFile);
|
|
102
127
|
let bunkerUri;
|
|
103
|
-
if (
|
|
104
|
-
bunkerUri = readSecretFile(
|
|
128
|
+
if (secretEnvironment.bunkerUriFile) {
|
|
129
|
+
bunkerUri = readSecretFile(secretEnvironment.bunkerUriFile);
|
|
105
130
|
}
|
|
106
|
-
else if (
|
|
107
|
-
bunkerUri =
|
|
131
|
+
else if (secretEnvironment.bunkerUri) {
|
|
132
|
+
bunkerUri = secretEnvironment.bunkerUri;
|
|
108
133
|
}
|
|
109
134
|
else if (file.bunkerUriFile) {
|
|
110
135
|
bunkerUri = readSecretFile(file.bunkerUriFile);
|
|
@@ -118,10 +143,10 @@ export async function loadConfig() {
|
|
|
118
143
|
bunkerUri = saved.uri;
|
|
119
144
|
}
|
|
120
145
|
// --- NIP-49 ncryptsec (password-encrypted key) ---
|
|
121
|
-
const ncryptsec =
|
|
122
|
-
? readSecretFile(
|
|
123
|
-
:
|
|
124
|
-
?
|
|
146
|
+
const ncryptsec = secretEnvironment.ncryptsecFile
|
|
147
|
+
? readSecretFile(secretEnvironment.ncryptsecFile)
|
|
148
|
+
: secretEnvironment.ncryptsec
|
|
149
|
+
? secretEnvironment.ncryptsec
|
|
125
150
|
: file.ncryptsecFile
|
|
126
151
|
? readSecretFile(file.ncryptsecFile)
|
|
127
152
|
: undefined;
|
|
@@ -129,11 +154,11 @@ export async function loadConfig() {
|
|
|
129
154
|
// Prefer file-based sources so the password can be delivered via systemd
|
|
130
155
|
// credentials, docker secrets, or similar secure channels and never appears
|
|
131
156
|
// in the process environment or config JSON.
|
|
132
|
-
const ncryptsecPasswordFile =
|
|
157
|
+
const ncryptsecPasswordFile = secretEnvironment.ncryptsecPasswordFile ?? file.ncryptsecPasswordFile;
|
|
133
158
|
let ncryptsecPasswordBuf = null;
|
|
134
159
|
let ncryptsecPassword;
|
|
135
|
-
if (
|
|
136
|
-
ncryptsecPassword =
|
|
160
|
+
if (secretEnvironment.ncryptsecPassword) {
|
|
161
|
+
ncryptsecPassword = secretEnvironment.ncryptsecPassword;
|
|
137
162
|
}
|
|
138
163
|
else if (ncryptsecPasswordFile) {
|
|
139
164
|
// Read as Buffer so we can zeroise the at-rest byte representation even
|
|
@@ -191,13 +216,14 @@ export async function loadConfig() {
|
|
|
191
216
|
}
|
|
192
217
|
const secretFormat = secretKey ? detectKeyFormat(secretKey) : 'nsec';
|
|
193
218
|
// --- NWC URI ---
|
|
194
|
-
const
|
|
219
|
+
const rawNwcUri = secretEnvironment.nwcUri;
|
|
220
|
+
const nwcFilePath = secretEnvironment.nwcUriFile ?? file.nwcUriFile;
|
|
221
|
+
if (rawNwcUri) {
|
|
222
|
+
throw new Error('NWC_URI is disabled because bearer wallet credentials must not be stored in environment variables; use NWC_URI_FILE');
|
|
223
|
+
}
|
|
195
224
|
let nwcUri;
|
|
196
225
|
if (nwcFilePath) {
|
|
197
|
-
nwcUri =
|
|
198
|
-
}
|
|
199
|
-
else if (process.env.NWC_URI) {
|
|
200
|
-
nwcUri = process.env.NWC_URI;
|
|
226
|
+
nwcUri = readNwcUriFile(nwcFilePath);
|
|
201
227
|
}
|
|
202
228
|
// --- Relays ---
|
|
203
229
|
const relays = process.env.NOSTR_RELAYS
|
|
@@ -261,17 +287,6 @@ export async function loadConfig() {
|
|
|
261
287
|
const walletsFile = process.env.BRAY_WALLETS_FILE
|
|
262
288
|
?? file.walletsFile
|
|
263
289
|
?? (process.env.HOME ? `${process.env.HOME}/.nostr/bray-wallets.json` : '');
|
|
264
|
-
// --- Clean up secrets from process.env ---
|
|
265
|
-
delete process.env.NOSTR_SECRET_KEY;
|
|
266
|
-
delete process.env.NOSTR_SECRET_KEY_FILE;
|
|
267
|
-
delete process.env.NWC_URI;
|
|
268
|
-
delete process.env.NWC_URI_FILE;
|
|
269
|
-
delete process.env.BUNKER_URI;
|
|
270
|
-
delete process.env.BUNKER_URI_FILE;
|
|
271
|
-
delete process.env.NOSTR_NCRYPTSEC;
|
|
272
|
-
delete process.env.NOSTR_NCRYPTSEC_FILE;
|
|
273
|
-
delete process.env.NOSTR_NCRYPTSEC_PASSWORD;
|
|
274
|
-
delete process.env.NOSTR_NCRYPTSEC_PASSWORD_FILE;
|
|
275
290
|
return {
|
|
276
291
|
secretKey,
|
|
277
292
|
secretFormat,
|
package/dist/context.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare class IdentityContext implements ExtendedSigningContext {
|
|
|
13
13
|
constructor(secretKey: string, format: 'nsec' | 'hex' | 'mnemonic', opts?: ContextOptions);
|
|
14
14
|
/** Current active identity's npub (bech32) */
|
|
15
15
|
get activeNpub(): string;
|
|
16
|
+
/** Derivation path of the current active identity, for reporting to a human */
|
|
17
|
+
get activePurpose(): string;
|
|
16
18
|
/** Current active identity's hex public key — use this in relay filters */
|
|
17
19
|
get activePublicKeyHex(): string;
|
|
18
20
|
/** Derive a child identity by purpose and index */
|
package/dist/context.js
CHANGED
|
@@ -55,6 +55,10 @@ export class IdentityContext {
|
|
|
55
55
|
get activeNpub() {
|
|
56
56
|
return this.activeEntry.identity.npub;
|
|
57
57
|
}
|
|
58
|
+
/** Derivation path of the current active identity, for reporting to a human */
|
|
59
|
+
get activePurpose() {
|
|
60
|
+
return this.activeEntry.purpose;
|
|
61
|
+
}
|
|
58
62
|
/** Current active identity's hex public key — use this in relay filters */
|
|
59
63
|
get activePublicKeyHex() {
|
|
60
64
|
return Buffer.from(this.activeEntry.identity.publicKey).toString('hex');
|
package/dist/help.js
CHANGED
|
@@ -57,14 +57,14 @@ export const COMMAND_HELP = {
|
|
|
57
57
|
'relay-add': { usage: 'relay-add <url> [read|write]', description: 'Add a relay to the active identity\'s relay set (in-memory only — does not publish kind 10002).', examples: ['nostr-bray relay-add wss://new-relay.com', 'nostr-bray relay-add wss://read-only.com read'] },
|
|
58
58
|
'relay-info': { usage: 'relay-info <wss://url>', description: 'Fetch the NIP-11 relay information document (name, description, supported NIPs, limits).', examples: ['nostr-bray relay-info wss://relay.damus.io'] },
|
|
59
59
|
// Wallet (NIP-47 Nostr Wallet Connect)
|
|
60
|
-
'wallet-connect': { usage: 'wallet connect <nwc-
|
|
61
|
-
'wallet-disconnect': { usage: 'wallet disconnect', description: 'Remove the stored NWC
|
|
60
|
+
'wallet-connect': { usage: 'wallet connect <nwc-file>', description: 'Store a reference to a private local file containing the NWC URI for the active identity.', examples: ['nostr-bray wallet connect /run/secrets/alice-nwc'], notes: 'The file must be a regular 0600 file. The bearer URI is never passed on the command line or stored in the wallet registry.' },
|
|
61
|
+
'wallet-disconnect': { usage: 'wallet disconnect', description: 'Remove the stored NWC file reference for the active identity. Falls back to NWC_URI_FILE if configured.', examples: ['nostr-bray wallet disconnect'] },
|
|
62
62
|
'wallet-status': { usage: 'wallet status', description: 'Show the wallet pubkey and relay configured for the active identity. The connection secret is never shown.', examples: ['nostr-bray wallet status --json'] },
|
|
63
|
-
'wallet-pay': { usage: 'wallet pay <bolt11>', description: 'Pay a Lightning invoice via Nostr Wallet Connect (NIP-47). Requires a wallet configured via `wallet connect` or
|
|
63
|
+
'wallet-pay': { usage: 'wallet pay <bolt11>', description: 'Pay a Lightning invoice via Nostr Wallet Connect (NIP-47). Requires a wallet configured via `wallet connect` or NWC_URI_FILE.', examples: ['nostr-bray wallet pay lnbc10u1...'], notes: 'SPENDS REAL SATS. Waits for an authenticated wallet result and verifies the returned preimage without printing it.' },
|
|
64
64
|
'wallet-balance': { usage: 'wallet balance', description: 'Request wallet balance via Nostr Wallet Connect (NIP-47).', examples: ['nostr-bray wallet balance', 'nostr-bray wallet balance --json'] },
|
|
65
65
|
'wallet-history': { usage: 'wallet history [--limit N]', description: 'List recent Lightning transactions via Nostr Wallet Connect (NIP-47). Default limit: 10.', examples: ['nostr-bray wallet history', 'nostr-bray wallet history --limit 25 --json'] },
|
|
66
66
|
// Zap
|
|
67
|
-
'zap-send': { usage: 'zap-send <bolt11>', description: 'Pay a Lightning invoice via Nostr Wallet Connect (NIP-47). Requires
|
|
67
|
+
'zap-send': { usage: 'zap-send <bolt11>', description: 'Pay a Lightning invoice via Nostr Wallet Connect (NIP-47). Requires NWC_URI_FILE or a persona wallet file.', examples: ['nostr-bray zap-send lnbc10u1...'] },
|
|
68
68
|
'zap-balance': { usage: 'zap-balance', description: 'Request wallet balance via NWC.', examples: ['nostr-bray zap-balance'] },
|
|
69
69
|
'zap-invoice': { usage: 'zap-invoice <msats> [description]', description: 'Generate a Lightning invoice via NWC to receive payments.', examples: ['nostr-bray zap-invoice 100000 "Coffee payment"'] },
|
|
70
70
|
'zap-lookup': { usage: 'zap-lookup <payment-hash>', description: 'Look up a Lightning invoice payment status via NWC.', examples: ['nostr-bray zap-lookup abc123...'] },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { hexId, relayUrl } from '../validation.js';
|
|
3
4
|
import { resolveRecipient } from '../resolve.js';
|
|
4
5
|
import { handleMarketplaceDiscover, handleMarketplaceInspect, handleMarketplaceSearch, handleMarketplaceReputation, handleMarketplaceCompare, handleMarketplaceProbe, handleMarketplaceCall, handleMarketplaceAnnounce, handleMarketplaceUpdate, handleMarketplaceRetire, storeCredential, clearCredentials, extractBolt11AmountSats, } from './handlers.js';
|
|
@@ -146,6 +147,10 @@ export function registerMarketplaceTools(server, deps) {
|
|
|
146
147
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
147
148
|
}, async ({ macaroon, invoice, confirm }) => {
|
|
148
149
|
const decoded = handleZapDecode(invoice);
|
|
150
|
+
if (decoded.expiry === undefined)
|
|
151
|
+
throw new Error('Invalid BOLT-11 invoice');
|
|
152
|
+
if (decoded.amountMsats === undefined)
|
|
153
|
+
throw new Error('Amountless L402 invoices are not supported');
|
|
149
154
|
const costSats = extractBolt11AmountSats(invoice);
|
|
150
155
|
if (!confirm) {
|
|
151
156
|
return {
|
|
@@ -166,17 +171,15 @@ export function registerMarketplaceTools(server, deps) {
|
|
|
166
171
|
invoice,
|
|
167
172
|
nwcUri: resolveNwcUri(deps.ctx, deps.walletsFile, deps.nwcUri),
|
|
168
173
|
});
|
|
169
|
-
//
|
|
170
|
-
const credentialId =
|
|
171
|
-
|
|
172
|
-
// NWC pay_invoice returns asynchronously — use the payment hash as placeholder
|
|
173
|
-
// until the wallet confirms. For now we store with the event id as preimage proxy.
|
|
174
|
-
storeCredential(credentialId, macaroon, credentialId);
|
|
174
|
+
// Keep the bearer credential in process and return only an opaque handle.
|
|
175
|
+
const credentialId = randomUUID();
|
|
176
|
+
storeCredential(credentialId, macaroon, payResult.preimage);
|
|
175
177
|
return {
|
|
176
178
|
content: [{
|
|
177
179
|
type: 'text',
|
|
178
180
|
text: JSON.stringify({
|
|
179
181
|
paid: true,
|
|
182
|
+
verified: payResult.verified,
|
|
180
183
|
credentialId,
|
|
181
184
|
costSats,
|
|
182
185
|
amountMsats: decoded.amountMsats,
|
package/dist/sdk.d.ts
CHANGED
|
@@ -363,6 +363,6 @@ export interface BrayClient {
|
|
|
363
363
|
export declare function createBray(config: BrayClientConfig): Promise<BrayClient>;
|
|
364
364
|
/**
|
|
365
365
|
* Get or create a singleton BrayClient built from environment variables.
|
|
366
|
-
* Reads NOSTR_SECRET_KEY / BUNKER_URI / NOSTR_RELAYS /
|
|
366
|
+
* Reads NOSTR_SECRET_KEY / BUNKER_URI / NOSTR_RELAYS / NWC_URI_FILE etc.
|
|
367
367
|
*/
|
|
368
368
|
export declare function defaultBray(): Promise<BrayClient>;
|
package/dist/sdk.js
CHANGED
|
@@ -343,7 +343,7 @@ export async function createBray(config) {
|
|
|
343
343
|
let _default = null;
|
|
344
344
|
/**
|
|
345
345
|
* Get or create a singleton BrayClient built from environment variables.
|
|
346
|
-
* Reads NOSTR_SECRET_KEY / BUNKER_URI / NOSTR_RELAYS /
|
|
346
|
+
* Reads NOSTR_SECRET_KEY / BUNKER_URI / NOSTR_RELAYS / NWC_URI_FILE etc.
|
|
347
347
|
*/
|
|
348
348
|
export function defaultBray() {
|
|
349
349
|
if (!_default) {
|
package/dist/social/blossom.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface BlobDescriptor {
|
|
|
8
8
|
type?: string;
|
|
9
9
|
uploaded?: number;
|
|
10
10
|
}
|
|
11
|
+
export declare function detectContentType(body: Uint8Array): string | undefined;
|
|
11
12
|
/** Upload a file to a blossom media server */
|
|
12
13
|
export declare function handleBlossomUpload(ctx: SigningContext, args: {
|
|
13
14
|
server: string;
|
package/dist/social/blossom.js
CHANGED
|
@@ -5,6 +5,36 @@ import { validatePublicUrl } from '../validation.js';
|
|
|
5
5
|
// fetch. Keeps an attacker-chosen source from amplifying nostr-bray into a
|
|
6
6
|
// megabyte-scale Blossom relay.
|
|
7
7
|
const MAX_UPLOAD = 100 * 1024 * 1024;
|
|
8
|
+
// Blossom servers sniff the body and reject a mismatched Content-Type, so
|
|
9
|
+
// `application/octet-stream` fails for any format they recognise. Detect from
|
|
10
|
+
// the leading bytes rather than the file name: the bytes are what the server
|
|
11
|
+
// checks, and an uploader should not be able to mislabel a blob by renaming it.
|
|
12
|
+
const MAGIC = [
|
|
13
|
+
{ type: 'image/png', test: (b) => b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47 },
|
|
14
|
+
{ type: 'image/jpeg', test: (b) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
|
|
15
|
+
{ type: 'image/gif', test: (b) => b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 },
|
|
16
|
+
{
|
|
17
|
+
type: 'image/webp',
|
|
18
|
+
test: (b) => b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
|
|
19
|
+
b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50,
|
|
20
|
+
},
|
|
21
|
+
{ type: 'application/pdf', test: (b) => b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46 },
|
|
22
|
+
{ type: 'image/svg+xml', test: (b) => Buffer.from(b.subarray(0, 256)).toString('utf8').includes('<svg') },
|
|
23
|
+
];
|
|
24
|
+
export function detectContentType(body) {
|
|
25
|
+
if (body.byteLength < 4)
|
|
26
|
+
return undefined;
|
|
27
|
+
for (const { type, test } of MAGIC) {
|
|
28
|
+
try {
|
|
29
|
+
if (test(body))
|
|
30
|
+
return type;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// a short or odd buffer simply does not match this signature
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
8
38
|
/** Upload a file to a blossom media server */
|
|
9
39
|
export async function handleBlossomUpload(ctx, args) {
|
|
10
40
|
validatePublicUrl(args.server);
|
|
@@ -44,7 +74,7 @@ export async function handleBlossomUpload(ctx, args) {
|
|
|
44
74
|
method: 'PUT',
|
|
45
75
|
headers: {
|
|
46
76
|
Authorization: authHeader,
|
|
47
|
-
'Content-Type': args.contentType ?? 'application/octet-stream',
|
|
77
|
+
'Content-Type': args.contentType ?? detectContentType(body) ?? 'application/octet-stream',
|
|
48
78
|
},
|
|
49
79
|
body: Buffer.from(body),
|
|
50
80
|
signal: AbortSignal.timeout(30_000),
|