nostr-bray 2.4.0 → 3.1.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 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` immediately after parsing.
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
 
@@ -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-url> Store NWC URI for the active identity
11
- * wallet disconnect Remove the stored NWC URI for the active identity
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-url> Store NWC URI for the active identity
7
- * wallet disconnect Remove the stored NWC URI for the active identity
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 uri = req(1, 'wallet connect <nwc-url>');
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] = uri;
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-url>` or set NWC_URI.' });
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
- out(await handleZapSend(ctx, pool, {
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;
@@ -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
- out(await handleZapSend(ctx, pool, { invoice: req(1, 'zap-send <bolt11>'), nwcUri: resolveNwcUri(ctx, walletsFile, globalNwcUri) }));
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-url> Store NWC URI for the active identity
186
- wallet disconnect Remove stored NWC URI for the active identity
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
- NWC_URI / NWC_URI_FILE Nostr Wallet Connect URI
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 = process.env.NOSTR_SECRET_KEY_FILE ?? file.secretKeyFile;
91
- const keyEnvVar = process.env.NOSTR_SECRET_KEY;
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
- || process.env.NOSTR_NCRYPTSEC_FILE
100
- || process.env.NOSTR_NCRYPTSEC
124
+ || secretEnvironment.ncryptsecFile
125
+ || secretEnvironment.ncryptsec
101
126
  || file.ncryptsecFile);
102
127
  let bunkerUri;
103
- if (process.env.BUNKER_URI_FILE) {
104
- bunkerUri = readSecretFile(process.env.BUNKER_URI_FILE);
128
+ if (secretEnvironment.bunkerUriFile) {
129
+ bunkerUri = readSecretFile(secretEnvironment.bunkerUriFile);
105
130
  }
106
- else if (process.env.BUNKER_URI) {
107
- bunkerUri = process.env.BUNKER_URI;
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 = process.env.NOSTR_NCRYPTSEC_FILE
122
- ? readSecretFile(process.env.NOSTR_NCRYPTSEC_FILE)
123
- : process.env.NOSTR_NCRYPTSEC
124
- ? process.env.NOSTR_NCRYPTSEC
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 = process.env.NOSTR_NCRYPTSEC_PASSWORD_FILE ?? file.ncryptsecPasswordFile;
157
+ const ncryptsecPasswordFile = secretEnvironment.ncryptsecPasswordFile ?? file.ncryptsecPasswordFile;
133
158
  let ncryptsecPasswordBuf = null;
134
159
  let ncryptsecPassword;
135
- if (process.env.NOSTR_NCRYPTSEC_PASSWORD) {
136
- ncryptsecPassword = process.env.NOSTR_NCRYPTSEC_PASSWORD;
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 nwcFilePath = process.env.NWC_URI_FILE ?? file.nwcUriFile;
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 = readSecretFile(nwcFilePath);
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/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-url>', description: 'Store a nostr+walletconnect:// URI for the active identity. Each identity can have its own Lightning wallet.', examples: ['nostr-bray wallet connect "nostr+walletconnect://pubkey?relay=wss%3A%2F%2Frelay.example&secret=deadbeef"'], notes: 'URI is stored per-identity in the wallets file (0600). Use `wallet status` to confirm.' },
61
- 'wallet-disconnect': { usage: 'wallet disconnect', description: 'Remove the stored NWC URI for the active identity. Falls back to NWC_URI env var if set.', examples: ['nostr-bray wallet disconnect'] },
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 NWC_URI.', examples: ['nostr-bray wallet pay lnbc10u1...'], notes: 'SPENDS REAL SATS. Decodes the invoice and sends a pay_invoice NIP-47 request.' },
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 NWC_URI configured.', examples: ['nostr-bray zap-send lnbc10u1...'] },
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
- // Store credentials keyed by event ID (opaque to caller)
170
- const credentialId = payResult.event.id;
171
- // In a real L402 flow the preimage comes from the payment result.
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 / NWC_URI etc.
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 / NWC_URI etc.
346
+ * Reads NOSTR_SECRET_KEY / BUNKER_URI / NOSTR_RELAYS / NWC_URI_FILE etc.
347
347
  */
348
348
  export function defaultBray() {
349
349
  if (!_default) {
@@ -1,34 +1,33 @@
1
- import type { Event as NostrEvent } from 'nostr-tools';
1
+ import type { GetBalanceResult, NwcRequestOptions, NwcTransaction, NwcTransport, PayInvoiceResult } from '@forgesworn/nwc-kit';
2
2
  import type { SigningContext } from '../signing-context.js';
3
3
  import type { RelayPool } from '../relay-pool.js';
4
- import type { PublishResult } from '../types.js';
5
4
  /**
6
5
  * Load the wallets map from the JSON file. Returns empty map if file missing.
7
6
  *
8
7
  * @param walletsFile - Absolute path to the JSON wallets store.
9
- * @returns A map of public key hex strings to NWC URIs.
8
+ * @returns A map of public key hex strings to absolute NWC secret-file paths.
10
9
  *
11
10
  * @example
12
11
  * const wallets = loadWallets('/home/user/.bray/wallets.json')
13
- * // { 'abc123...': 'nostr+walletconnect://...' }
12
+ * // { 'abc123...': '/run/secrets/alice-nwc' }
14
13
  */
15
14
  export declare function loadWallets(walletsFile: string): Record<string, string>;
16
15
  /**
17
16
  * Save the wallets map to the JSON file. Creates parent dirs and sets 0600.
18
17
  *
19
18
  * @param walletsFile - Absolute path to the JSON wallets store.
20
- * @param wallets - Map of public key hex strings to NWC URIs to persist.
19
+ * @param wallets - Map of public key hex strings to absolute NWC secret-file paths.
21
20
  * @returns void
22
21
  *
23
22
  * @example
24
23
  * saveWallets('/home/user/.bray/wallets.json', {
25
- * 'abc123...': 'nostr+walletconnect://pubkey?relay=wss://relay.example&secret=deadbeef',
24
+ * 'abc123...': '/run/secrets/alice-nwc',
26
25
  * })
27
26
  */
28
27
  export declare function saveWallets(walletsFile: string, wallets: Record<string, string>): void;
29
28
  /**
30
29
  * Resolve the NWC URI for the active identity.
31
- * 1. Per-identity wallet from wallets file
30
+ * 1. Per-identity NWC secret file referenced by the wallet registry
32
31
  * 2. Global NWC URI (fallback)
33
32
  * 3. undefined (no wallet configured)
34
33
  *
@@ -37,27 +36,28 @@ export declare function saveWallets(walletsFile: string, wallets: Record<string,
37
36
  * @returns The resolved NWC URI, or `undefined` if no wallet is configured.
38
37
  *
39
38
  * @example
40
- * const uri = resolveNwcUri(ctx, '/home/user/.bray/wallets.json', process.env.NWC_URI)
39
+ * const uri = resolveNwcUri(ctx, '/home/user/.bray/wallets.json', configuredNwcUri)
41
40
  * if (!uri) throw new Error('No wallet configured')
42
41
  */
43
42
  export declare function resolveNwcUri(ctx: SigningContext, walletsFile: string, globalNwcUri?: string): string | undefined;
44
43
  export interface NwcConnection {
45
44
  pubkey: string;
46
45
  relay: string;
47
- secret: string;
46
+ relays: readonly string[];
47
+ lud16?: string;
48
48
  }
49
49
  /**
50
50
  * Parse a nostr+walletconnect:// URI.
51
51
  *
52
52
  * @param uri - A `nostr+walletconnect://` URI as specified in NIP-47.
53
- * @returns A parsed {@link NwcConnection} containing the wallet pubkey, relay URL, and connection secret.
54
- * @throws {Error} If `pubkey`, `relay`, or `secret` are missing from the URI.
53
+ * @returns Public connection metadata. The connection secret is never returned.
54
+ * @throws {Error} If the URI is malformed or insecure.
55
55
  *
56
56
  * @example
57
57
  * const conn = parseNwcUri(
58
58
  * 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
59
59
  * )
60
- * // { pubkey: 'abc123', relay: 'wss://relay.example', secret: 'deadbeef' }
60
+ * // { pubkey: 'abc123', relay: 'wss://relay.example/', relays: ['wss://relay.example/'] }
61
61
  */
62
62
  export declare function parseNwcUri(uri: string): NwcConnection;
63
63
  export interface ZapReceipt {
@@ -87,9 +87,8 @@ export declare function handleZapReceipts(ctx: SigningContext, pool: RelayPool,
87
87
  * Decode basic bolt11 invoice fields.
88
88
  *
89
89
  * @param bolt11 - A BOLT-11 Lightning invoice string (e.g. `lnbc...`).
90
- * @returns An object with the decoded fields. `amountMsats` is present when the
91
- * amount is encoded in the invoice prefix; `description` and `expiry` are not
92
- * yet decoded and will be absent in the current implementation.
90
+ * @returns The bounded amount, description and expiry fields committed by the
91
+ * invoice. `amountMsats` is absent for an amountless invoice.
93
92
  *
94
93
  * @example
95
94
  * const { amountMsats } = handleZapDecode('lnbc1000n1...')
@@ -100,142 +99,47 @@ export declare function handleZapDecode(bolt11: string): {
100
99
  description?: string;
101
100
  expiry?: number;
102
101
  };
103
- /**
104
- * Pay a Lightning invoice via NWC.
105
- *
106
- * @param args - Payment arguments.
107
- * @param args.invoice - BOLT-11 Lightning invoice to pay.
108
- * @param args.nwcUri - NWC URI identifying the wallet to use.
109
- * @returns The signed NIP-47 request event and relay publish result.
110
- * @throws {Error} If `nwcUri` is not provided.
111
- *
112
- * @example
113
- * const { event, publish } = await handleZapSend(ctx, pool, {
114
- * invoice: 'lnbc500n1...',
115
- * nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
116
- * })
117
- */
118
- export declare function handleZapSend(ctx: SigningContext, pool: RelayPool, args: {
119
- invoice: string;
120
- nwcUri?: string;
121
- }): Promise<{
122
- event: NostrEvent;
123
- publish: PublishResult;
124
- }>;
125
- /**
126
- * Request wallet balance via NWC.
127
- *
128
- * @param args - Balance request arguments.
129
- * @param args.nwcUri - NWC URI identifying the wallet to query.
130
- * @returns The signed NIP-47 `get_balance` request event and relay publish result.
131
- * @throws {Error} If `nwcUri` is not provided.
132
- *
133
- * @example
134
- * const { event, publish } = await handleZapBalance(ctx, pool, {
135
- * nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
136
- * })
137
- */
138
- export declare function handleZapBalance(ctx: SigningContext, pool: RelayPool, args: {
102
+ interface NwcOperationOptions extends NwcRequestOptions {
103
+ /** Injectable for deterministic tests and specialised relay transports. */
104
+ transport?: NwcTransport;
105
+ }
106
+ interface NwcArgs extends NwcOperationOptions {
139
107
  nwcUri?: string;
140
- }): Promise<{
141
- event: NostrEvent;
142
- publish: PublishResult;
143
- }>;
108
+ }
109
+ export interface ZapPaymentResult extends PayInvoiceResult {
110
+ paymentHash: string;
111
+ verified: true;
112
+ }
144
113
  /**
145
- * Generate a Lightning invoice via NWC.
146
- *
147
- * @param args - Invoice creation arguments.
148
- * @param args.amountMsats - Invoice amount in millisatoshis.
149
- * @param args.description - Optional human-readable memo attached to the invoice.
150
- * @param args.nwcUri - NWC URI identifying the wallet to use.
151
- * @returns The signed NIP-47 `make_invoice` request event and relay publish result.
152
- * @throws {Error} If `nwcUri` is not provided.
153
- *
154
- * @example
155
- * const { event, publish } = await handleZapMakeInvoice(ctx, pool, {
156
- * amountMsats: 21_000,
157
- * description: 'Coffee ☕',
158
- * nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
159
- * })
114
+ * The wallet may have submitted a payment but Bray cannot prove settlement.
115
+ * Callers must reconcile `paymentHash` before attempting the invoice again.
160
116
  */
161
- export declare function handleZapMakeInvoice(ctx: SigningContext, pool: RelayPool, args: {
117
+ export declare class ZapPaymentOutcomeUnknownError extends Error {
118
+ readonly paymentHash: string;
119
+ readonly code = "PAYMENT_OUTCOME_UNKNOWN";
120
+ constructor(paymentHash: string);
121
+ }
122
+ /** Pay a BOLT-11 invoice and return only after the wallet proves settlement. */
123
+ export declare function handleZapSend(_ctx: SigningContext, _pool: RelayPool, args: {
124
+ invoice: string;
125
+ } & NwcArgs): Promise<ZapPaymentResult>;
126
+ /** Return the wallet's confirmed balance in millisatoshis. */
127
+ export declare function handleZapBalance(_ctx: SigningContext, _pool: RelayPool, args: NwcArgs): Promise<GetBalanceResult>;
128
+ /** Generate a Lightning invoice through the configured wallet. */
129
+ export declare function handleZapMakeInvoice(_ctx: SigningContext, _pool: RelayPool, args: {
162
130
  amountMsats: number;
163
131
  description?: string;
164
- nwcUri?: string;
165
- }): Promise<{
166
- event: NostrEvent;
167
- publish: PublishResult;
168
- }>;
169
- /**
170
- * Look up an invoice via NWC.
171
- *
172
- * @param args - Lookup arguments. Provide at least one of `paymentHash` or `invoice`.
173
- * @param args.paymentHash - Hex-encoded payment hash of the invoice to look up.
174
- * @param args.invoice - BOLT-11 invoice string to look up.
175
- * @param args.nwcUri - NWC URI identifying the wallet to query.
176
- * @returns The signed NIP-47 `lookup_invoice` request event and relay publish result.
177
- * @throws {Error} If `nwcUri` is not provided.
178
- *
179
- * @example
180
- * const { event, publish } = await handleZapLookupInvoice(ctx, pool, {
181
- * paymentHash: 'a1b2c3...',
182
- * nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
183
- * })
184
- */
185
- export declare function handleZapLookupInvoice(ctx: SigningContext, pool: RelayPool, args: {
132
+ } & NwcArgs): Promise<NwcTransaction>;
133
+ /** Look up an invoice and return the wallet's authenticated result. */
134
+ export declare function handleZapLookupInvoice(_ctx: SigningContext, _pool: RelayPool, args: {
186
135
  paymentHash?: string;
187
136
  invoice?: string;
188
- nwcUri?: string;
189
- }): Promise<{
190
- event: NostrEvent;
191
- publish: PublishResult;
192
- }>;
193
- /**
194
- * List recent transactions via NWC.
195
- *
196
- * @param args - Pagination arguments.
197
- * @param args.limit - Maximum number of transactions to return (default 10).
198
- * @param args.offset - Number of transactions to skip for pagination (default 0).
199
- * @param args.nwcUri - NWC URI identifying the wallet to query.
200
- * @returns The signed NIP-47 `list_transactions` request event and relay publish result.
201
- * @throws {Error} If `nwcUri` is not provided.
202
- *
203
- * @example
204
- * const { event, publish } = await handleZapListTransactions(ctx, pool, {
205
- * limit: 5,
206
- * offset: 0,
207
- * nwcUri: 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
208
- * })
209
- */
210
- export declare function handleZapListTransactions(ctx: SigningContext, pool: RelayPool, args: {
137
+ } & NwcArgs): Promise<NwcTransaction>;
138
+ /** List recent transactions through the isolated NIP-47 extension 05 client. */
139
+ export declare function handleZapListTransactions(_ctx: SigningContext, _pool: RelayPool, args: {
211
140
  limit?: number;
212
141
  offset?: number;
213
- nwcUri?: string;
214
- }): Promise<{
215
- event: NostrEvent;
216
- publish: PublishResult;
142
+ } & NwcArgs): Promise<{
143
+ transactions: NwcTransaction[];
217
144
  }>;
218
- /**
219
- * Parse a NWC wallet response event.
220
- *
221
- * @param nwcUri - The NWC URI whose secret is used to decrypt the response.
222
- * @param event - A kind 23195 NIP-47 response event from the wallet service.
223
- * @returns The decrypted response payload containing `result_type`, an optional
224
- * `result` object, and an optional `error` with `code` and `message`.
225
- *
226
- * @example
227
- * const response = handleZapParseResponse(
228
- * 'nostr+walletconnect://abc123?relay=wss%3A%2F%2Frelay.example&secret=deadbeef',
229
- * walletResponseEvent,
230
- * )
231
- * if (response.error) console.error(response.error.message)
232
- * else console.log(response.result)
233
- */
234
- export declare function handleZapParseResponse(nwcUri: string, event: NostrEvent): {
235
- result_type: string;
236
- result?: Record<string, unknown>;
237
- error?: {
238
- code: string;
239
- message: string;
240
- };
241
- };
145
+ export {};