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.
- package/README.md +2 -1
- package/dist/admin/handlers.d.ts +30 -0
- package/dist/admin/handlers.js +71 -0
- package/dist/admin/index.d.ts +1 -0
- package/dist/admin/index.js +1 -0
- package/dist/bunker-helpers.d.ts +12 -0
- package/dist/bunker-helpers.js +36 -0
- package/dist/catalog.js +29 -18
- package/dist/cli/commands/admin.d.ts +2 -0
- package/dist/cli/commands/admin.js +29 -0
- package/dist/cli/commands/bunker.d.ts +15 -0
- package/dist/cli/commands/bunker.js +197 -0
- package/dist/cli/commands/event.d.ts +2 -0
- package/dist/cli/commands/event.js +74 -0
- package/dist/cli/commands/identity.d.ts +2 -0
- package/dist/cli/commands/identity.js +67 -0
- package/dist/cli/commands/musig2.d.ts +2 -0
- package/dist/cli/commands/musig2.js +63 -0
- package/dist/cli/commands/relay.d.ts +2 -0
- package/dist/cli/commands/relay.js +177 -0
- package/dist/cli/commands/safety.d.ts +2 -0
- package/dist/cli/commands/safety.js +14 -0
- package/dist/cli/commands/social.d.ts +2 -0
- package/dist/cli/commands/social.js +199 -0
- package/dist/cli/commands/sync.d.ts +2 -0
- package/dist/cli/commands/sync.js +31 -0
- package/dist/cli/commands/trust.d.ts +2 -0
- package/dist/cli/commands/trust.js +104 -0
- package/dist/cli/commands/util.d.ts +2 -0
- package/dist/cli/commands/util.js +82 -0
- package/dist/cli/commands/wallet.d.ts +17 -0
- package/dist/cli/commands/wallet.js +69 -0
- package/dist/cli/commands/zap.d.ts +6 -0
- package/dist/cli/commands/zap.js +38 -0
- package/dist/cli/dispatch.d.ts +15 -0
- package/dist/cli/dispatch.js +167 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +440 -0
- package/dist/cli.js +2 -850
- package/dist/config.d.ts +1 -0
- package/dist/config.js +58 -2
- package/dist/context.d.ts +5 -0
- package/dist/context.js +17 -0
- package/dist/dispatch/capabilities.d.ts +2 -0
- package/dist/dispatch/handlers.d.ts +3 -0
- package/dist/dispatch/identities.d.ts +2 -0
- package/dist/dispatch/identities.js +13 -0
- package/dist/dispatch/index.d.ts +4 -0
- package/dist/dispatch/index.js +4 -0
- package/dist/dispatch/protocol.d.ts +11 -0
- package/dist/event/handlers.d.ts +51 -0
- package/dist/event/handlers.js +59 -0
- package/dist/event/index.d.ts +1 -0
- package/dist/event/index.js +1 -0
- package/dist/exports.d.ts +38 -0
- package/dist/exports.js +39 -0
- package/dist/format.d.ts +13 -0
- package/dist/format.js +63 -46
- package/dist/handler/handlers.d.ts +3 -0
- package/dist/handler/index.d.ts +1 -0
- package/dist/handler/index.js +1 -0
- package/dist/heartwood-context.d.ts +12 -0
- package/dist/heartwood-context.js +31 -2
- package/dist/help.js +8 -1
- package/dist/http-client.d.ts +36 -0
- package/dist/http-client.js +71 -0
- package/dist/identity/handlers.d.ts +92 -7
- package/dist/identity/handlers.js +92 -7
- package/dist/identity/index.d.ts +4 -0
- package/dist/identity/index.js +4 -0
- package/dist/identity/migration.d.ts +54 -3
- package/dist/identity/migration.js +54 -3
- package/dist/identity/shamir.d.ts +40 -2
- package/dist/identity/shamir.js +43 -3
- package/dist/identity/tools.js +4 -4
- package/dist/index.js +47 -3
- package/dist/marketplace/handlers.d.ts +11 -0
- package/dist/marketplace/index.d.ts +2 -0
- package/dist/marketplace/index.js +2 -0
- package/dist/marketplace/listings.d.ts +3 -0
- package/dist/moderation/handlers.d.ts +2 -0
- package/dist/moderation/index.d.ts +1 -0
- package/dist/moderation/index.js +1 -0
- package/dist/musig2/handlers.d.ts +78 -0
- package/dist/musig2/handlers.js +274 -0
- package/dist/musig2/index.d.ts +1 -0
- package/dist/musig2/index.js +1 -0
- package/dist/nip65.d.ts +5 -1
- package/dist/nip65.js +40 -4
- package/dist/privacy/handlers.d.ts +6 -0
- package/dist/privacy/index.d.ts +1 -0
- package/dist/privacy/index.js +1 -0
- package/dist/relay/curl.d.ts +32 -0
- package/dist/relay/curl.js +82 -0
- package/dist/relay/handlers.d.ts +121 -7
- package/dist/relay/handlers.js +144 -14
- package/dist/relay/index.d.ts +7 -0
- package/dist/relay/index.js +7 -0
- package/dist/relay/outbox-handlers.d.ts +57 -0
- package/dist/relay/outbox-handlers.js +118 -0
- package/dist/relay-pool.d.ts +23 -2
- package/dist/relay-pool.js +84 -32
- package/dist/resolve.js +21 -3
- package/dist/safety/handlers.d.ts +18 -2
- package/dist/safety/handlers.js +18 -2
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/sdk.d.ts +315 -0
- package/dist/sdk.js +313 -0
- package/dist/serve.js +9 -0
- package/dist/signet/assessor.d.ts +1 -0
- package/dist/signet/handlers.d.ts +5 -0
- package/dist/signet/index.d.ts +2 -0
- package/dist/signet/index.js +2 -0
- package/dist/social/blossom.js +49 -3
- package/dist/social/dm.d.ts +46 -3
- package/dist/social/dm.js +80 -9
- package/dist/social/groups.d.ts +61 -0
- package/dist/social/groups.js +94 -0
- package/dist/social/handlers.d.ts +182 -13
- package/dist/social/handlers.js +213 -32
- package/dist/social/index.d.ts +13 -0
- package/dist/social/index.js +13 -0
- package/dist/social/nips.d.ts +30 -2
- package/dist/social/nips.js +32 -3
- package/dist/social/notifications.d.ts +29 -2
- package/dist/social/notifications.js +50 -8
- package/dist/social/tools.js +72 -4
- package/dist/state.d.ts +8 -1
- package/dist/state.js +22 -4
- package/dist/sync/handlers.d.ts +62 -0
- package/dist/sync/handlers.js +80 -0
- package/dist/sync/index.d.ts +1 -0
- package/dist/sync/index.js +1 -0
- package/dist/trust/handlers.d.ts +112 -7
- package/dist/trust/handlers.js +160 -14
- package/dist/trust/index.d.ts +5 -0
- package/dist/trust/index.js +5 -0
- package/dist/trust/ring.d.ts +26 -2
- package/dist/trust/ring.js +26 -2
- package/dist/trust/spoken.d.ts +37 -2
- package/dist/trust/spoken.js +37 -2
- package/dist/trust/tools.js +7 -5
- package/dist/types-public.d.ts +33 -0
- package/dist/types-public.js +11 -0
- package/dist/types.d.ts +1 -0
- package/dist/util/handlers.d.ts +163 -16
- package/dist/util/handlers.js +163 -15
- package/dist/util/index.d.ts +3 -0
- package/dist/util/index.js +3 -0
- package/dist/util/ncryptsec.d.ts +22 -2
- package/dist/util/ncryptsec.js +22 -2
- package/dist/util/tools.js +2 -2
- package/dist/validation.d.ts +74 -1
- package/dist/validation.js +212 -9
- package/dist/vault/handlers.d.ts +10 -0
- package/dist/vault/index.d.ts +2 -0
- package/dist/vault/index.js +2 -0
- package/dist/vault/resolver.d.ts +1 -0
- package/dist/widgets/dm-thread.html +26 -26
- package/dist/widgets/identity-picker.html +26 -26
- package/dist/widgets/social-feed.html +26 -26
- package/dist/workflow/handlers.d.ts +9 -0
- package/dist/workflow/handlers.js +19 -5
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +1 -0
- package/dist/zap/handlers.d.ts +161 -11
- package/dist/zap/handlers.js +161 -11
- package/dist/zap/index.d.ts +1 -0
- package/dist/zap/index.js +1 -0
- package/llms-full.txt +3 -3
- package/llms.txt +8 -2
- 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.**
|
|
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
|
[](https://www.npmjs.com/package/nostr-bray)
|
|
6
6
|
[](https://github.com/forgesworn/bray/actions/workflows/ci.yml)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](./tsconfig.json)
|
|
9
|
+
[](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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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(
|
|
83
|
+
return entry.handler(result.data);
|
|
73
84
|
}
|
|
74
85
|
registerMetaTools(server) {
|
|
75
86
|
const catalog = this;
|
|
@@ -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,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,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
|
+
}
|