nostr-bray 1.30.0 → 1.32.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.
Files changed (173) hide show
  1. package/README.md +2 -1
  2. package/dist/admin/handlers.d.ts +30 -0
  3. package/dist/admin/handlers.js +71 -0
  4. package/dist/admin/index.d.ts +1 -0
  5. package/dist/admin/index.js +1 -0
  6. package/dist/bunker-helpers.d.ts +12 -0
  7. package/dist/bunker-helpers.js +36 -0
  8. package/dist/catalog.js +29 -18
  9. package/dist/cli/commands/admin.d.ts +2 -0
  10. package/dist/cli/commands/admin.js +29 -0
  11. package/dist/cli/commands/bunker.d.ts +15 -0
  12. package/dist/cli/commands/bunker.js +197 -0
  13. package/dist/cli/commands/event.d.ts +2 -0
  14. package/dist/cli/commands/event.js +74 -0
  15. package/dist/cli/commands/identity.d.ts +2 -0
  16. package/dist/cli/commands/identity.js +67 -0
  17. package/dist/cli/commands/musig2.d.ts +2 -0
  18. package/dist/cli/commands/musig2.js +63 -0
  19. package/dist/cli/commands/relay.d.ts +2 -0
  20. package/dist/cli/commands/relay.js +177 -0
  21. package/dist/cli/commands/safety.d.ts +2 -0
  22. package/dist/cli/commands/safety.js +14 -0
  23. package/dist/cli/commands/social.d.ts +2 -0
  24. package/dist/cli/commands/social.js +199 -0
  25. package/dist/cli/commands/sync.d.ts +2 -0
  26. package/dist/cli/commands/sync.js +31 -0
  27. package/dist/cli/commands/trust.d.ts +2 -0
  28. package/dist/cli/commands/trust.js +104 -0
  29. package/dist/cli/commands/util.d.ts +2 -0
  30. package/dist/cli/commands/util.js +82 -0
  31. package/dist/cli/commands/wallet.d.ts +17 -0
  32. package/dist/cli/commands/wallet.js +69 -0
  33. package/dist/cli/commands/zap.d.ts +6 -0
  34. package/dist/cli/commands/zap.js +38 -0
  35. package/dist/cli/dispatch.d.ts +15 -0
  36. package/dist/cli/dispatch.js +167 -0
  37. package/dist/cli/index.d.ts +2 -0
  38. package/dist/cli/index.js +440 -0
  39. package/dist/cli.js +2 -850
  40. package/dist/config.d.ts +1 -0
  41. package/dist/config.js +58 -2
  42. package/dist/context.d.ts +5 -0
  43. package/dist/context.js +17 -0
  44. package/dist/dispatch/capabilities.d.ts +2 -0
  45. package/dist/dispatch/handlers.d.ts +3 -0
  46. package/dist/dispatch/identities.d.ts +2 -0
  47. package/dist/dispatch/identities.js +13 -0
  48. package/dist/dispatch/index.d.ts +4 -0
  49. package/dist/dispatch/index.js +4 -0
  50. package/dist/dispatch/protocol.d.ts +11 -0
  51. package/dist/event/handlers.d.ts +51 -0
  52. package/dist/event/handlers.js +59 -0
  53. package/dist/event/index.d.ts +1 -0
  54. package/dist/event/index.js +1 -0
  55. package/dist/exports.d.ts +38 -0
  56. package/dist/exports.js +39 -0
  57. package/dist/format.d.ts +13 -0
  58. package/dist/format.js +63 -46
  59. package/dist/handler/handlers.d.ts +3 -0
  60. package/dist/handler/index.d.ts +1 -0
  61. package/dist/handler/index.js +1 -0
  62. package/dist/heartwood-context.d.ts +12 -0
  63. package/dist/heartwood-context.js +31 -2
  64. package/dist/help.js +8 -1
  65. package/dist/http-client.d.ts +36 -0
  66. package/dist/http-client.js +71 -0
  67. package/dist/identity/handlers.d.ts +92 -7
  68. package/dist/identity/handlers.js +92 -7
  69. package/dist/identity/index.d.ts +4 -0
  70. package/dist/identity/index.js +4 -0
  71. package/dist/identity/migration.d.ts +54 -3
  72. package/dist/identity/migration.js +54 -3
  73. package/dist/identity/shamir.d.ts +40 -2
  74. package/dist/identity/shamir.js +43 -3
  75. package/dist/identity/tools.js +4 -4
  76. package/dist/index.js +47 -3
  77. package/dist/marketplace/handlers.d.ts +11 -0
  78. package/dist/marketplace/index.d.ts +2 -0
  79. package/dist/marketplace/index.js +2 -0
  80. package/dist/marketplace/listings.d.ts +3 -0
  81. package/dist/moderation/handlers.d.ts +2 -0
  82. package/dist/moderation/index.d.ts +1 -0
  83. package/dist/moderation/index.js +1 -0
  84. package/dist/musig2/handlers.d.ts +78 -0
  85. package/dist/musig2/handlers.js +274 -0
  86. package/dist/musig2/index.d.ts +1 -0
  87. package/dist/musig2/index.js +1 -0
  88. package/dist/nip65.d.ts +5 -1
  89. package/dist/nip65.js +40 -4
  90. package/dist/privacy/handlers.d.ts +6 -0
  91. package/dist/privacy/index.d.ts +1 -0
  92. package/dist/privacy/index.js +1 -0
  93. package/dist/relay/curl.d.ts +32 -0
  94. package/dist/relay/curl.js +82 -0
  95. package/dist/relay/handlers.d.ts +121 -7
  96. package/dist/relay/handlers.js +144 -14
  97. package/dist/relay/index.d.ts +7 -0
  98. package/dist/relay/index.js +7 -0
  99. package/dist/relay/outbox-handlers.d.ts +57 -0
  100. package/dist/relay/outbox-handlers.js +118 -0
  101. package/dist/relay-pool.d.ts +23 -2
  102. package/dist/relay-pool.js +84 -32
  103. package/dist/resolve.js +21 -3
  104. package/dist/safety/handlers.d.ts +18 -2
  105. package/dist/safety/handlers.js +18 -2
  106. package/dist/safety/index.d.ts +2 -0
  107. package/dist/safety/index.js +2 -0
  108. package/dist/sdk.d.ts +315 -0
  109. package/dist/sdk.js +313 -0
  110. package/dist/serve.js +9 -0
  111. package/dist/signet/assessor.d.ts +1 -0
  112. package/dist/signet/handlers.d.ts +5 -0
  113. package/dist/signet/index.d.ts +2 -0
  114. package/dist/signet/index.js +2 -0
  115. package/dist/social/blossom.js +49 -3
  116. package/dist/social/dm.d.ts +46 -3
  117. package/dist/social/dm.js +80 -9
  118. package/dist/social/groups.d.ts +61 -0
  119. package/dist/social/groups.js +94 -0
  120. package/dist/social/handlers.d.ts +182 -13
  121. package/dist/social/handlers.js +213 -32
  122. package/dist/social/index.d.ts +13 -0
  123. package/dist/social/index.js +13 -0
  124. package/dist/social/nips.d.ts +30 -2
  125. package/dist/social/nips.js +32 -3
  126. package/dist/social/notifications.d.ts +29 -2
  127. package/dist/social/notifications.js +50 -8
  128. package/dist/social/tools.js +72 -4
  129. package/dist/state.d.ts +8 -1
  130. package/dist/state.js +22 -4
  131. package/dist/sync/handlers.d.ts +62 -0
  132. package/dist/sync/handlers.js +80 -0
  133. package/dist/sync/index.d.ts +1 -0
  134. package/dist/sync/index.js +1 -0
  135. package/dist/trust/handlers.d.ts +112 -7
  136. package/dist/trust/handlers.js +160 -14
  137. package/dist/trust/index.d.ts +5 -0
  138. package/dist/trust/index.js +5 -0
  139. package/dist/trust/ring.d.ts +26 -2
  140. package/dist/trust/ring.js +26 -2
  141. package/dist/trust/spoken.d.ts +37 -2
  142. package/dist/trust/spoken.js +37 -2
  143. package/dist/trust/tools.js +7 -5
  144. package/dist/types-public.d.ts +33 -0
  145. package/dist/types-public.js +11 -0
  146. package/dist/types.d.ts +1 -0
  147. package/dist/util/handlers.d.ts +163 -16
  148. package/dist/util/handlers.js +163 -15
  149. package/dist/util/index.d.ts +3 -0
  150. package/dist/util/index.js +3 -0
  151. package/dist/util/ncryptsec.d.ts +22 -2
  152. package/dist/util/ncryptsec.js +22 -2
  153. package/dist/util/tools.js +2 -2
  154. package/dist/validation.d.ts +74 -1
  155. package/dist/validation.js +212 -9
  156. package/dist/vault/handlers.d.ts +10 -0
  157. package/dist/vault/index.d.ts +2 -0
  158. package/dist/vault/index.js +2 -0
  159. package/dist/vault/resolver.d.ts +1 -0
  160. package/dist/widgets/dm-thread.html +26 -26
  161. package/dist/widgets/identity-picker.html +26 -26
  162. package/dist/widgets/social-feed.html +26 -26
  163. package/dist/workflow/handlers.d.ts +9 -0
  164. package/dist/workflow/handlers.js +19 -5
  165. package/dist/workflow/index.d.ts +1 -0
  166. package/dist/workflow/index.js +1 -0
  167. package/dist/zap/handlers.d.ts +161 -11
  168. package/dist/zap/handlers.js +161 -11
  169. package/dist/zap/index.d.ts +1 -0
  170. package/dist/zap/index.js +1 -0
  171. package/llms-full.txt +3 -3
  172. package/llms.txt +8 -2
  173. package/package.json +111 -26
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # nostr-bray
2
2
 
3
- **Trust-aware Nostr MCP for AI and humans.** 235 tools across 27 groups. Model-agnostic. Works with Claude, ChatGPT, Gemini, Cursor, Windsurf, or any MCP client.
3
+ **Trust-aware Nostr MCP for AI and humans.** 238 tools across 27 groups. Model-agnostic. Works with Claude, ChatGPT, Gemini, Cursor, Windsurf, or any MCP client.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/nostr-bray)](https://www.npmjs.com/package/nostr-bray)
6
6
  [![CI](https://github.com/forgesworn/bray/actions/workflows/ci.yml/badge.svg)](https://github.com/forgesworn/bray/actions/workflows/ci.yml)
7
7
  [![licence](https://img.shields.io/npm/l/nostr-bray)](./LICENSE)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-ESM-blue)](./tsconfig.json)
9
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/TheCryptoDonkey?logo=githubsponsors&color=ea4aaa&label=Sponsor)](https://github.com/sponsors/TheCryptoDonkey)
9
10
 
10
11
  ## Quick Start
11
12
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Admin — NIP-86 relay management API.
3
+ *
4
+ * NIP-86 defines an HTTP management API for relay operators. Requests are POST to the
5
+ * relay's HTTP URL with a NIP-98 `Authorization: Nostr <base64(event)>` header.
6
+ *
7
+ * Reference: https://github.com/nostr-protocol/nips/blob/master/86.md
8
+ */
9
+ import type { SigningContext } from '../signing-context.js';
10
+ export type AdminMethod = 'allowpubkey' | 'banpubkey' | 'listallowedpubkeys' | 'listbannedpubkeys' | 'allowkind' | 'bankind' | 'listallowedkinds' | 'listbannedkinds' | 'blockip' | 'unblockip' | 'listblockedips';
11
+ export interface AdminCallOptions {
12
+ /** Relay HTTP URL (e.g. https://relay.example.com) */
13
+ relay: string;
14
+ /** NIP-86 method name */
15
+ method: AdminMethod;
16
+ /** Method parameters (pubkey hex, kind number, IP address, etc.) */
17
+ params?: Array<string | number>;
18
+ }
19
+ export interface AdminResult {
20
+ relay: string;
21
+ method: AdminMethod;
22
+ result: unknown;
23
+ }
24
+ /**
25
+ * Call a NIP-86 relay management method.
26
+ *
27
+ * @param ctx - Signing context used to produce the NIP-98 auth event
28
+ * @param opts - Target relay URL, method name, and optional parameters
29
+ */
30
+ export declare function handleAdminCall(ctx: SigningContext, opts: AdminCallOptions): Promise<AdminResult>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Admin — NIP-86 relay management API.
3
+ *
4
+ * NIP-86 defines an HTTP management API for relay operators. Requests are POST to the
5
+ * relay's HTTP URL with a NIP-98 `Authorization: Nostr <base64(event)>` header.
6
+ *
7
+ * Reference: https://github.com/nostr-protocol/nips/blob/master/86.md
8
+ */
9
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
10
+ /**
11
+ * Build a NIP-98 HTTP Auth header value for the given URL and HTTP method.
12
+ *
13
+ * Produces a signed kind 27235 event and returns the
14
+ * `Nostr <base64url(JSON)>` string ready for the Authorization header.
15
+ */
16
+ async function buildNip98Header(ctx, url, httpMethod, payloadHash) {
17
+ const sign = ctx.getSigningFunction();
18
+ const tags = [
19
+ ['u', url],
20
+ ['method', httpMethod.toUpperCase()],
21
+ ];
22
+ if (payloadHash)
23
+ tags.push(['payload', payloadHash]);
24
+ const event = await sign({
25
+ kind: 27235,
26
+ created_at: Math.floor(Date.now() / 1000),
27
+ tags,
28
+ content: '',
29
+ });
30
+ const encoded = Buffer.from(JSON.stringify(event), 'utf-8').toString('base64');
31
+ return `Nostr ${encoded}`;
32
+ }
33
+ // ─── Handler ──────────────────────────────────────────────────────────────────
34
+ /**
35
+ * Call a NIP-86 relay management method.
36
+ *
37
+ * @param ctx - Signing context used to produce the NIP-98 auth event
38
+ * @param opts - Target relay URL, method name, and optional parameters
39
+ */
40
+ export async function handleAdminCall(ctx, opts) {
41
+ // NIP-86 always POSTs to the relay root URL
42
+ const url = opts.relay.replace(/\/$/, '');
43
+ const body = JSON.stringify({
44
+ method: opts.method,
45
+ params: opts.params ?? [],
46
+ });
47
+ // Hash the request body for the NIP-98 payload tag (SHA-256 hex)
48
+ const { createHash } = await import('node:crypto');
49
+ const payloadHash = createHash('sha256').update(body, 'utf-8').digest('hex');
50
+ const authHeader = await buildNip98Header(ctx, url, 'POST', payloadHash);
51
+ const response = await fetch(url, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Content-Type': 'application/nostr+json+rpc',
55
+ 'Authorization': authHeader,
56
+ },
57
+ body,
58
+ });
59
+ if (!response.ok) {
60
+ const text = await response.text().catch(() => '');
61
+ throw new Error(`Admin call failed: HTTP ${response.status} — ${text}`);
62
+ }
63
+ const json = await response.json();
64
+ if (json.error)
65
+ throw new Error(`Relay error: ${json.error}`);
66
+ return {
67
+ relay: opts.relay,
68
+ method: opts.method,
69
+ result: json.result,
70
+ };
71
+ }
@@ -0,0 +1 @@
1
+ export * from './handlers.js';
@@ -0,0 +1 @@
1
+ export * from './handlers.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Read the BIP-39 mnemonic from an nsec-tree-cli profile
3
+ * (`~/.nsec-tree/profiles/<name>.json`). Lets `bunker --profile <name>` use the
4
+ * master directly, so the operator never types or stores the 12 words separately.
5
+ */
6
+ export declare function readNsecTreeProfileMnemonic(name: string, baseDir?: string): string;
7
+ /**
8
+ * Get — creating + persisting on first use — a stable bunker connection key for
9
+ * a label, so the bunker:// URI stays the same across restarts without the
10
+ * operator managing key files. Stored in bray's state dir.
11
+ */
12
+ export declare function getOrCreateBunkerKey(label: string, stateDir?: string): string;
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { generateSecretKey } from 'nostr-tools/pure';
5
+ import { bytesToHex } from 'nostr-tools/utils';
6
+ import { readStateFile, writeStateFile } from './state.js';
7
+ const NSEC_TREE_PROFILES_DIR = join(homedir(), '.nsec-tree', 'profiles');
8
+ const BUNKER_KEYS_FILE = 'bunker-keys.json';
9
+ /**
10
+ * Read the BIP-39 mnemonic from an nsec-tree-cli profile
11
+ * (`~/.nsec-tree/profiles/<name>.json`). Lets `bunker --profile <name>` use the
12
+ * master directly, so the operator never types or stores the 12 words separately.
13
+ */
14
+ export function readNsecTreeProfileMnemonic(name, baseDir = NSEC_TREE_PROFILES_DIR) {
15
+ const raw = readFileSync(join(baseDir, `${name}.json`), 'utf8');
16
+ const mnemonic = JSON.parse(raw).root?.mnemonic;
17
+ if (typeof mnemonic !== 'string' || mnemonic.trim() === '') {
18
+ throw new Error(`nsec-tree profile "${name}" is not mnemonic-backed (no recoverable 12 words)`);
19
+ }
20
+ return mnemonic.trim();
21
+ }
22
+ /**
23
+ * Get — creating + persisting on first use — a stable bunker connection key for
24
+ * a label, so the bunker:// URI stays the same across restarts without the
25
+ * operator managing key files. Stored in bray's state dir.
26
+ */
27
+ export function getOrCreateBunkerKey(label, stateDir) {
28
+ const keys = readStateFile(BUNKER_KEYS_FILE, stateDir);
29
+ const existing = keys[label];
30
+ if (typeof existing === 'string' && /^[0-9a-f]{64}$/i.test(existing))
31
+ return existing;
32
+ const hex = bytesToHex(generateSecretKey());
33
+ keys[label] = hex;
34
+ writeStateFile(BUNKER_KEYS_FILE, keys, stateDir);
35
+ return hex;
36
+ }
package/dist/catalog.js CHANGED
@@ -51,25 +51,36 @@ export class ActionCatalog {
51
51
  }],
52
52
  };
53
53
  }
54
- if (entry.inputSchema && Object.keys(entry.inputSchema).length > 0) {
55
- const schema = z.object(entry.inputSchema);
56
- const result = schema.safeParse(params);
57
- if (!result.success) {
58
- return {
59
- content: [{
60
- type: 'text',
61
- text: JSON.stringify({
62
- error: 'Invalid parameters',
63
- action: name,
64
- issues: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`),
65
- expected: extractParams(entry.inputSchema),
66
- }, null, 2),
67
- }],
68
- };
69
- }
70
- return entry.handler(result.data);
54
+ // A registered catalog entry must carry an inputSchema, even if empty.
55
+ // Fail-closed when undefined so that a registration bug cannot silently
56
+ // forward unvalidated arguments to a handler.
57
+ if (!entry.inputSchema) {
58
+ return {
59
+ content: [{
60
+ type: 'text',
61
+ text: JSON.stringify({
62
+ error: 'Action has no declared input schema',
63
+ action: name,
64
+ }, null, 2),
65
+ }],
66
+ };
67
+ }
68
+ const schema = z.object(entry.inputSchema);
69
+ const result = schema.safeParse(params ?? {});
70
+ if (!result.success) {
71
+ return {
72
+ content: [{
73
+ type: 'text',
74
+ text: JSON.stringify({
75
+ error: 'Invalid parameters',
76
+ action: name,
77
+ issues: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`),
78
+ expected: extractParams(entry.inputSchema),
79
+ }, null, 2),
80
+ }],
81
+ };
71
82
  }
72
- return entry.handler(params);
83
+ return entry.handler(result.data);
73
84
  }
74
85
  registerMetaTools(server) {
75
86
  const catalog = this;
@@ -0,0 +1,2 @@
1
+ import type { Helpers } from '../dispatch.js';
2
+ export declare function dispatch(cmd: string, cmdArgs: string[], h: Helpers, ctx: any): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { handleAdminCall } from '../../exports.js';
2
+ const ADMIN_METHODS = new Set([
3
+ 'allowpubkey', 'banpubkey', 'listallowedpubkeys', 'listbannedpubkeys',
4
+ 'allowkind', 'bankind', 'listallowedkinds', 'listbannedkinds',
5
+ 'blockip', 'unblockip', 'listblockedips',
6
+ ]);
7
+ export async function dispatch(cmd, cmdArgs, h, ctx) {
8
+ // cmd is always 'admin'; subcommand is cmdArgs[1] (already sliced by normaliser)
9
+ // After COMPOUND_COMMANDS normalisation: cmd = 'admin-<subcommand>'
10
+ const { req, out } = h;
11
+ // Extract the method from the compound command name
12
+ const method = cmd.replace(/^admin-/, '');
13
+ if (!ADMIN_METHODS.has(method)) {
14
+ throw new Error(`Unknown admin subcommand: ${method}. Valid: ${[...ADMIN_METHODS].join(', ')}`);
15
+ }
16
+ const relay = req(1, `admin ${method} <relay-url> [param...]`);
17
+ // Remaining positional args after relay URL are method params
18
+ const params = cmdArgs.slice(2).filter(a => !a.startsWith('--'));
19
+ // allowkind / bankind take integer params
20
+ const coercedParams = params.map(p => {
21
+ const n = Number(p);
22
+ return isNaN(n) ? p : n;
23
+ });
24
+ out(await handleAdminCall(ctx, {
25
+ relay,
26
+ method,
27
+ params: coercedParams.length ? coercedParams : undefined,
28
+ }), d => `${d.method} on ${d.relay}: ${JSON.stringify(d.result)}`);
29
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Bunker command module.
3
+ * bunker sign, bunker connect, bunker authorize, bunker status, and bunker daemon
4
+ * are all dispatched BEFORE ctx/pool setup in index.ts, so this module does its
5
+ * own config/context construction.
6
+ */
7
+ /**
8
+ * Apply `--persona <name>` to a bunker context: switch signing to that named
9
+ * sub-identity (the same key `nsec-tree-cli export nsec <name>` yields). With no
10
+ * `--persona`, the bunker signs as the configured master identity, unchanged.
11
+ */
12
+ export declare function applyBunkerPersona(ctx: {
13
+ use(purpose: string, index?: number): Promise<void>;
14
+ }, args: string[]): Promise<string | undefined>;
15
+ export declare function dispatch(args: string[]): Promise<void>;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Bunker command module.
3
+ * bunker sign, bunker connect, bunker authorize, bunker status, and bunker daemon
4
+ * are all dispatched BEFORE ctx/pool setup in index.ts, so this module does its
5
+ * own config/context construction.
6
+ */
7
+ /** Filename used to persist the active bunker URI inside the bray state dir. */
8
+ const BUNKER_URI_FILE = 'bunker-uri';
9
+ /**
10
+ * Apply `--persona <name>` to a bunker context: switch signing to that named
11
+ * sub-identity (the same key `nsec-tree-cli export nsec <name>` yields). With no
12
+ * `--persona`, the bunker signs as the configured master identity, unchanged.
13
+ */
14
+ export async function applyBunkerPersona(ctx, args) {
15
+ const i = args.indexOf('--persona');
16
+ const value = i >= 0 ? args[i + 1] : undefined;
17
+ if (value && !value.startsWith('--')) {
18
+ await ctx.use(value);
19
+ return value;
20
+ }
21
+ return undefined;
22
+ }
23
+ export async function dispatch(args) {
24
+ // Accept `bunker daemon` as alias for `bunker` (start daemon)
25
+ if (args[1] === 'daemon')
26
+ args.splice(1, 1);
27
+ // ── bunker connect <bunker-url> ─────────────────────────────────────────────
28
+ // Connect to a remote NIP-46 bunker or Heartwood device, verify the
29
+ // connection, then persist the URI so subsequent bray commands use it
30
+ // automatically without needing BUNKER_URI in the environment.
31
+ if (args[1] === 'connect') {
32
+ const uri = args[2];
33
+ if (!uri) {
34
+ console.error('usage: bray bunker connect <bunker://…>');
35
+ process.exit(1);
36
+ }
37
+ const { BunkerContext } = await import('../../bunker-context.js');
38
+ const { HeartwoodContext } = await import('../../heartwood-context.js');
39
+ const { writeStateFile } = await import('../../state.js');
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ let client;
42
+ try {
43
+ client = await BunkerContext.connect(uri);
44
+ await client.resolvePublicKey();
45
+ }
46
+ catch (err) {
47
+ console.error(`bunker connect: failed to connect — ${err.message}`);
48
+ process.exit(1);
49
+ }
50
+ const hw = await HeartwoodContext.probe(client);
51
+ const isHeartwood = hw !== null;
52
+ // Persist URI in state dir so loadConfig() picks it up on next invocation.
53
+ // Written with 0600 permissions by writeStateFile.
54
+ writeStateFile(BUNKER_URI_FILE, { uri });
55
+ const npub = client.activeNpub;
56
+ client.destroy();
57
+ console.log(JSON.stringify({
58
+ connected: true,
59
+ npub,
60
+ heartwood: isHeartwood,
61
+ saved: true,
62
+ }, null, 2));
63
+ process.exit(0);
64
+ }
65
+ // ── bunker authorize <app-pubkey> ───────────────────────────────────────────
66
+ // Pre-authorise an app's hex pubkey so it can use the bunker daemon without
67
+ // going through the interactive connect flow. Reads the saved bunker URI for
68
+ // the bunker transport pubkey (used as the key in approved-clients.json) then
69
+ // writes the new entry — no network connection required.
70
+ if (args[1] === 'authorize') {
71
+ const appPubkey = args[2];
72
+ if (!appPubkey || !/^[0-9a-f]{64}$/i.test(appPubkey)) {
73
+ console.error('usage: bray bunker authorize <64-char-hex-pubkey>');
74
+ process.exit(1);
75
+ }
76
+ const { readStateFile, writeStateFile } = await import('../../state.js');
77
+ const { parseBunkerUri } = await import('../../bunker-context.js');
78
+ // The bunker transport pubkey keys the approvals file. It is the pubkey
79
+ // encoded in the saved bunker URI (bunker://<pubkey>?…), OR falls back to
80
+ // the BUNKER_URI env var so the command also works without a saved URI.
81
+ const saved = readStateFile(BUNKER_URI_FILE);
82
+ const bunkerUriStr = saved.uri ?? process.env.BUNKER_URI;
83
+ if (!bunkerUriStr) {
84
+ console.error('bunker authorize: no saved bunker URI — run `bunker connect` first or set BUNKER_URI');
85
+ process.exit(1);
86
+ }
87
+ const { pubkey: bunkerPk } = parseBunkerUri(bunkerUriStr);
88
+ const APPROVALS_FILE = 'approved-clients.json';
89
+ const current = readStateFile(APPROVALS_FILE);
90
+ const entries = current[bunkerPk] ?? [];
91
+ const normalised = appPubkey.toLowerCase();
92
+ if (entries.includes(normalised)) {
93
+ console.error(`bunker authorize: ${normalised} is already authorised`);
94
+ process.exit(0);
95
+ }
96
+ entries.push(normalised);
97
+ current[bunkerPk] = entries;
98
+ writeStateFile(APPROVALS_FILE, current);
99
+ console.log(JSON.stringify({ authorised: normalised, total: entries.length }, null, 2));
100
+ process.exit(0);
101
+ }
102
+ // ── bunker status ───────────────────────────────────────────────────────────
103
+ // Report the saved bunker URI and its parsed details. Does NOT attempt a
104
+ // live connection (use `bunker connect` to verify reachability).
105
+ if (args[1] === 'status') {
106
+ const { readStateFile } = await import('../../state.js');
107
+ const saved = readStateFile(BUNKER_URI_FILE);
108
+ if (!saved.uri) {
109
+ console.log(JSON.stringify({ connected: false }));
110
+ process.exit(0);
111
+ }
112
+ const { parseBunkerUri } = await import('../../bunker-context.js');
113
+ const { npubEncode } = await import('nostr-tools/nip19');
114
+ const cfg = parseBunkerUri(saved.uri);
115
+ const npub = npubEncode(cfg.pubkey);
116
+ console.log(JSON.stringify({
117
+ connected: true,
118
+ npub,
119
+ relays: cfg.relays,
120
+ }, null, 2));
121
+ process.exit(0);
122
+ }
123
+ // One-shot signing: bray bunker sign <file> [--bunker <url>]
124
+ if (args[1] === 'sign') {
125
+ const { readFileSync } = await import('node:fs');
126
+ const { BunkerContext } = await import('../../bunker-context.js');
127
+ const bunkerUri = process.env.BUNKER_URI;
128
+ if (!bunkerUri) {
129
+ console.error('bunker sign: missing bunker URI — use --bunker <url> or set BUNKER_URI');
130
+ process.exit(1);
131
+ }
132
+ const filePath = args[2] && args[2] !== '-' ? args[2] : undefined;
133
+ const raw = filePath ? readFileSync(filePath, 'utf-8') : readFileSync(0, 'utf-8');
134
+ const template = JSON.parse(raw);
135
+ const client = await BunkerContext.connect(bunkerUri);
136
+ await client.resolvePublicKey();
137
+ const sign = client.getSigningFunction();
138
+ const signed = await sign({
139
+ kind: template.kind ?? 1,
140
+ created_at: template.created_at ?? Math.floor(Date.now() / 1000),
141
+ tags: template.tags ?? [],
142
+ content: template.content ?? '',
143
+ });
144
+ console.log(JSON.stringify(signed, null, 2));
145
+ client.destroy();
146
+ process.exit(0);
147
+ }
148
+ // Bunker daemon
149
+ const { startBunker } = await import('../../bunker.js');
150
+ const { readNsecTreeProfileMnemonic, getOrCreateBunkerKey } = await import('../../bunker-helpers.js');
151
+ // --profile <name>: load the master straight from an nsec-tree-cli profile
152
+ // (~/.nsec-tree/profiles/<name>.json). loadConfig scrubs NOSTR_SECRET_KEY after.
153
+ const profileIdx = args.indexOf('--profile');
154
+ const profileName = profileIdx >= 0 && args[profileIdx + 1] && !args[profileIdx + 1].startsWith('--')
155
+ ? args[profileIdx + 1]
156
+ : undefined;
157
+ if (profileName)
158
+ process.env.NOSTR_SECRET_KEY = readNsecTreeProfileMnemonic(profileName);
159
+ const config = await (await import('../../config.js')).loadConfig();
160
+ const { IdentityContext: IC } = await import('../../context.js');
161
+ const bCtx = new IC(config.secretKey, config.secretFormat);
162
+ config.secretKey = '';
163
+ const persona = await applyBunkerPersona(bCtx, args);
164
+ const authorizedKeys = args.includes('--authorized-keys')
165
+ ? args[args.indexOf('--authorized-keys') + 1].split(',')
166
+ : undefined;
167
+ let bunkerKeyHex;
168
+ if (args.includes('--bunker-key-file')) {
169
+ const { readFileSync } = await import('node:fs');
170
+ bunkerKeyHex = readFileSync(args[args.indexOf('--bunker-key-file') + 1], 'utf-8').trim();
171
+ }
172
+ else if (args.includes('--bunker-key')) {
173
+ bunkerKeyHex = args[args.indexOf('--bunker-key') + 1];
174
+ }
175
+ else {
176
+ // No explicit key: persist a STABLE one per identity so the bunker:// URI
177
+ // survives restarts without the operator managing key files.
178
+ bunkerKeyHex = getOrCreateBunkerKey(persona ?? profileName ?? 'default');
179
+ }
180
+ const relays = config.relays.length > 0
181
+ ? config.relays
182
+ : ['wss://relay.damus.io', 'wss://nos.lol'];
183
+ const bunker = startBunker({
184
+ ctx: bCtx,
185
+ relays,
186
+ authorizedKeys,
187
+ bunkerKeyHex,
188
+ quiet: args.includes('--quiet'),
189
+ });
190
+ console.error(`nostr-bray bunker running`);
191
+ console.error(`URI: ${bunker.url}`);
192
+ console.error(`Signing as: ${bCtx.activeNpub}`);
193
+ console.error('Press Ctrl+C to stop');
194
+ process.on('SIGINT', () => { bunker.close(); bCtx.destroy(); process.exit(0); });
195
+ process.on('SIGTERM', () => { bunker.close(); bCtx.destroy(); process.exit(0); });
196
+ await new Promise(() => { });
197
+ }
@@ -0,0 +1,2 @@
1
+ import type { Helpers } from '../dispatch.js';
2
+ export declare function dispatch(cmd: string, _cmdArgs: string[], h: Helpers, ctx: any, pool: any): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import { handlePublishEvent, handlePublishRaw } from '../../exports.js';
2
+ export async function dispatch(cmd, _cmdArgs, h, ctx, pool) {
3
+ const { flag, flags, hasFlag, out } = h;
4
+ switch (cmd) {
5
+ case 'event': {
6
+ const kind = parseInt(flag('kind') ?? '', 10);
7
+ if (!kind || isNaN(kind))
8
+ throw new Error('Usage: event --kind <N> [--tag k=v] [--content s] [--relay url] [--no-publish]');
9
+ const tagValues = flags('tag').map(t => {
10
+ const eq = t.indexOf('=');
11
+ if (eq === -1)
12
+ return [t];
13
+ return [t.slice(0, eq), t.slice(eq + 1)];
14
+ });
15
+ const content = flag('content') ?? '';
16
+ const relayOverrides = flags('relay');
17
+ if (hasFlag('no-publish')) {
18
+ const sign = ctx.getSigningFunction();
19
+ const event = await sign({
20
+ kind,
21
+ created_at: Math.floor(Date.now() / 1000),
22
+ tags: tagValues,
23
+ content,
24
+ });
25
+ console.log(JSON.stringify(event, null, 2));
26
+ break;
27
+ }
28
+ out(await handlePublishEvent(ctx, pool, {
29
+ kind,
30
+ content,
31
+ tags: tagValues,
32
+ relays: relayOverrides.length ? relayOverrides : undefined,
33
+ }));
34
+ break;
35
+ }
36
+ case 'publish-raw': {
37
+ const { readFileSync } = await import('node:fs');
38
+ const raw = hasFlag('file')
39
+ ? readFileSync(flag('file'), 'utf-8')
40
+ : readFileSync(0, 'utf-8');
41
+ const inputEvent = JSON.parse(raw);
42
+ const relayOverrides = flags('relay');
43
+ const timeoutMs = flag('timeout') ? parseInt(flag('timeout'), 10) : undefined;
44
+ const quorum = flag('quorum') ? parseInt(flag('quorum'), 10) : undefined;
45
+ const report = hasFlag('report');
46
+ const result = await handlePublishRaw(ctx, pool, {
47
+ event: inputEvent,
48
+ noSign: hasFlag('no-sign'),
49
+ relays: relayOverrides.length ? relayOverrides : undefined,
50
+ timeoutMs,
51
+ quorum,
52
+ });
53
+ if (report) {
54
+ const { accepted, rejected, errors } = result.publish;
55
+ const rows = [
56
+ ...accepted.map(u => ` ✓ ${u}`),
57
+ ...rejected.map(u => {
58
+ const err = errors.find(e => e.startsWith(u));
59
+ return ` ✗ ${u}${err ? ` (${err.slice(u.length + 2)})` : ''}`;
60
+ }),
61
+ ];
62
+ const status = result.publish.success ? 'OK' : 'FAILED';
63
+ console.log(`publish-raw ${status}: ${accepted.length}/${accepted.length + rejected.length} relays accepted`);
64
+ console.log(rows.join('\n'));
65
+ }
66
+ else {
67
+ out(result);
68
+ }
69
+ break;
70
+ }
71
+ default:
72
+ throw new Error(`Unknown command: ${cmd}. Run --help for usage.`);
73
+ }
74
+ }
@@ -0,0 +1,2 @@
1
+ import type { Helpers } from '../dispatch.js';
2
+ export declare function dispatch(cmd: string, cmdArgs: string[], h: Helpers, ctx: any, pool: any): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import { handleIdentityList, handleIdentityProve, handleIdentityCreate, handleBackupShamir, handleRestoreShamir, handleIdentityBackup, handleIdentityRestore, handleIdentityMigrate, handleTrustProofPublish, } from '../../exports.js';
2
+ import * as fmt from '../../format.js';
3
+ export async function dispatch(cmd, cmdArgs, h, ctx, pool) {
4
+ const { req, flag, hasFlag, out } = h;
5
+ switch (cmd) {
6
+ case 'whoami':
7
+ console.log(ctx.activeNpub);
8
+ break;
9
+ case 'create':
10
+ out(handleIdentityCreate());
11
+ break;
12
+ case 'list':
13
+ out(await handleIdentityList(ctx), fmt.formatIdentityList);
14
+ break;
15
+ case 'derive':
16
+ out(await ctx.derive(req(1, 'derive <purpose> [index]'), parseInt(cmdArgs[2] ?? '0', 10)));
17
+ break;
18
+ case 'persona':
19
+ out(await ctx.derivePersona(req(1, 'persona <name> [index]'), parseInt(cmdArgs[2] ?? '0', 10)));
20
+ break;
21
+ case 'switch':
22
+ await ctx.switch(req(1, 'switch <target> [index]'), cmdArgs[2] ? parseInt(cmdArgs[2], 10) : undefined);
23
+ console.error(`Now signing as ${ctx.activeNpub}`);
24
+ console.log(ctx.activeNpub);
25
+ break;
26
+ case 'prove':
27
+ out(await handleIdentityProve(ctx, { mode: (cmdArgs[1] === 'full' ? 'full' : 'blind') }));
28
+ break;
29
+ case 'proof-publish': {
30
+ const r = await handleTrustProofPublish(ctx, pool, {
31
+ mode: (cmdArgs[1] === 'full' ? 'full' : 'blind'),
32
+ confirm: hasFlag('confirm'),
33
+ });
34
+ out(r);
35
+ break;
36
+ }
37
+ case 'backup':
38
+ out(handleBackupShamir({
39
+ secret: new Uint8Array(ctx.activePrivateKey),
40
+ threshold: parseInt(cmdArgs[2] ?? '3', 10),
41
+ shares: parseInt(cmdArgs[3] ?? '5', 10),
42
+ outputDir: req(1, 'backup <dir> [threshold] [shares]'),
43
+ }));
44
+ break;
45
+ case 'restore': {
46
+ const tFlag = flag('t', '3');
47
+ const files = cmdArgs.slice(1).filter(a => a !== '--t' && a !== `-t` && a !== tFlag);
48
+ out({ masterNpub: handleRestoreShamir({ files, threshold: parseInt(tFlag, 10) }) });
49
+ break;
50
+ }
51
+ case 'identity-backup':
52
+ out(await handleIdentityBackup(pool, req(1, 'identity-backup <pubkey-hex>'), ctx.activeNpub));
53
+ break;
54
+ case 'identity-restore':
55
+ out(await handleIdentityRestore(ctx, pool, await handleIdentityBackup(pool, req(1, 'identity-restore <pubkey-hex>'), ctx.activeNpub)));
56
+ break;
57
+ case 'migrate':
58
+ out(await handleIdentityMigrate(ctx, pool, {
59
+ oldPubkeyHex: req(1, 'migrate <old-hex> <old-npub>'),
60
+ oldNpub: req(2, 'migrate <old-hex> <old-npub>'),
61
+ confirm: hasFlag('confirm'),
62
+ }));
63
+ break;
64
+ default:
65
+ throw new Error(`Unknown command: ${cmd}. Run --help for usage.`);
66
+ }
67
+ }
@@ -0,0 +1,2 @@
1
+ import type { Helpers } from '../dispatch.js';
2
+ export declare function dispatch(cmd: string, _cmdArgs: string[], h: Helpers): Promise<void>;