nostr-bray 0.0.0-development

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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/cli.d.ts +20 -0
  4. package/dist/cli.js +229 -0
  5. package/dist/config.d.ts +5 -0
  6. package/dist/config.js +87 -0
  7. package/dist/context.d.ts +35 -0
  8. package/dist/context.js +184 -0
  9. package/dist/identity/handlers.d.ts +31 -0
  10. package/dist/identity/handlers.js +32 -0
  11. package/dist/identity/migration.d.ts +37 -0
  12. package/dist/identity/migration.js +84 -0
  13. package/dist/identity/shamir.d.ts +18 -0
  14. package/dist/identity/shamir.js +46 -0
  15. package/dist/identity/tools.d.ts +11 -0
  16. package/dist/identity/tools.js +168 -0
  17. package/dist/index.d.ts +10 -0
  18. package/dist/index.js +125 -0
  19. package/dist/nip65.d.ts +15 -0
  20. package/dist/nip65.js +71 -0
  21. package/dist/relay/handlers.d.ts +36 -0
  22. package/dist/relay/handlers.js +96 -0
  23. package/dist/relay/tools.d.ts +3 -0
  24. package/dist/relay/tools.js +60 -0
  25. package/dist/relay-pool.d.ts +42 -0
  26. package/dist/relay-pool.js +150 -0
  27. package/dist/safety/handlers.d.ts +15 -0
  28. package/dist/safety/handlers.js +12 -0
  29. package/dist/safety/tools.d.ts +3 -0
  30. package/dist/safety/tools.js +29 -0
  31. package/dist/social/dm.d.ts +28 -0
  32. package/dist/social/dm.js +90 -0
  33. package/dist/social/handlers.d.ts +42 -0
  34. package/dist/social/handlers.js +107 -0
  35. package/dist/social/notifications.d.ts +32 -0
  36. package/dist/social/notifications.js +74 -0
  37. package/dist/social/tools.d.ts +3 -0
  38. package/dist/social/tools.js +168 -0
  39. package/dist/trust/handlers.d.ts +65 -0
  40. package/dist/trust/handlers.js +133 -0
  41. package/dist/trust/ring.d.ts +19 -0
  42. package/dist/trust/ring.js +54 -0
  43. package/dist/trust/spoken.d.ts +33 -0
  44. package/dist/trust/spoken.js +14 -0
  45. package/dist/trust/tools.d.ts +3 -0
  46. package/dist/trust/tools.js +182 -0
  47. package/dist/types.d.ts +41 -0
  48. package/dist/types.js +1 -0
  49. package/dist/validation.d.ts +5 -0
  50. package/dist/validation.js +5 -0
  51. package/dist/zap/handlers.d.ts +80 -0
  52. package/dist/zap/handlers.js +165 -0
  53. package/dist/zap/tools.d.ts +3 -0
  54. package/dist/zap/tools.js +98 -0
  55. package/llms-full.txt +281 -0
  56. package/llms.txt +73 -0
  57. package/package.json +96 -0
@@ -0,0 +1,184 @@
1
+ import { fromNsec, fromMnemonic, derive, zeroise } from 'nsec-tree';
2
+ import { derivePersona } from 'nsec-tree/persona';
3
+ import { createBlindProof, createFullProof } from 'nsec-tree/proof';
4
+ import { finalizeEvent } from 'nostr-tools/pure';
5
+ export class IdentityContext {
6
+ root;
7
+ cache = new Map();
8
+ masterEntry;
9
+ activeEntry;
10
+ maxCacheSize;
11
+ constructor(secretKey, format, opts) {
12
+ this.maxCacheSize = opts?.maxCache ?? 5;
13
+ if (format === 'mnemonic') {
14
+ this.root = fromMnemonic(secretKey);
15
+ }
16
+ else if (format === 'hex') {
17
+ const bytes = Buffer.from(secretKey, 'hex');
18
+ this.root = fromNsec(bytes);
19
+ }
20
+ else {
21
+ this.root = fromNsec(secretKey);
22
+ }
23
+ // Derive master identity — kept separate from LRU cache
24
+ const masterIdentity = derive(this.root, 'master', 0);
25
+ this.masterEntry = {
26
+ identity: masterIdentity,
27
+ purpose: 'master',
28
+ index: 0,
29
+ lastUsed: Date.now(),
30
+ };
31
+ this.activeEntry = this.masterEntry;
32
+ }
33
+ /** Current active identity's npub */
34
+ get activeNpub() {
35
+ return this.activeEntry.identity.npub;
36
+ }
37
+ /** Derive a child identity by purpose and index */
38
+ derive(purpose, index) {
39
+ // Return from cache if already derived
40
+ for (const [npub, entry] of this.cache) {
41
+ if (entry.purpose === purpose && entry.index === index && !entry.personaName) {
42
+ entry.lastUsed = Date.now();
43
+ return { npub, purpose, index };
44
+ }
45
+ }
46
+ const identity = derive(this.root, purpose, index);
47
+ const entry = {
48
+ identity,
49
+ purpose,
50
+ index,
51
+ lastUsed: Date.now(),
52
+ };
53
+ this.putCache(identity.npub, entry);
54
+ return { npub: identity.npub, purpose, index };
55
+ }
56
+ /** Derive a named persona */
57
+ derivePersona(name, index) {
58
+ // Return from cache if already derived
59
+ for (const [npub, entry] of this.cache) {
60
+ if (entry.personaName === name && entry.index === index) {
61
+ entry.lastUsed = Date.now();
62
+ return { npub, purpose: entry.purpose, index: entry.index, personaName: name };
63
+ }
64
+ }
65
+ const persona = derivePersona(this.root, name, index);
66
+ const entry = {
67
+ identity: persona.identity,
68
+ purpose: persona.identity.purpose,
69
+ index: persona.index,
70
+ personaName: name,
71
+ lastUsed: Date.now(),
72
+ };
73
+ this.putCache(persona.identity.npub, entry);
74
+ return {
75
+ npub: persona.identity.npub,
76
+ purpose: persona.identity.purpose,
77
+ index: persona.index,
78
+ personaName: name,
79
+ };
80
+ }
81
+ /** Switch active identity by purpose+index, persona name, or "master" */
82
+ switch(purposeOrName, index) {
83
+ if (purposeOrName === 'master') {
84
+ this.activeEntry = this.masterEntry;
85
+ this.masterEntry.lastUsed = Date.now();
86
+ return;
87
+ }
88
+ // Check cache for matching purpose or persona name
89
+ for (const [, entry] of this.cache) {
90
+ const matchesPurpose = entry.purpose === purposeOrName && (index === undefined || entry.index === index);
91
+ const matchesPersona = entry.personaName === purposeOrName && (index === undefined || entry.index === index);
92
+ if (matchesPurpose || matchesPersona) {
93
+ entry.lastUsed = Date.now();
94
+ this.activeEntry = entry;
95
+ return;
96
+ }
97
+ }
98
+ // Not in cache — derive on the fly
99
+ const identity = derive(this.root, purposeOrName, index ?? 0);
100
+ const entry = {
101
+ identity,
102
+ purpose: purposeOrName,
103
+ index: index ?? 0,
104
+ lastUsed: Date.now(),
105
+ };
106
+ this.putCache(identity.npub, entry);
107
+ this.activeEntry = entry;
108
+ }
109
+ /** Get a signing function bound to the current active identity */
110
+ getSigningFunction() {
111
+ return async (template) => {
112
+ return finalizeEvent(template, this.activeEntry.identity.privateKey);
113
+ };
114
+ }
115
+ /** List all known identities — returns public info only, never private keys */
116
+ listIdentities() {
117
+ const result = [
118
+ { npub: this.masterEntry.identity.npub, purpose: 'master', index: 0 },
119
+ ];
120
+ for (const [, entry] of this.cache) {
121
+ result.push({
122
+ npub: entry.identity.npub,
123
+ purpose: entry.purpose,
124
+ index: entry.index,
125
+ personaName: entry.personaName,
126
+ });
127
+ }
128
+ return result;
129
+ }
130
+ /** Create a linkage proof for the active identity */
131
+ prove(mode = 'blind') {
132
+ const child = this.activeEntry.identity;
133
+ if (mode === 'full') {
134
+ return createFullProof(this.root, child);
135
+ }
136
+ return createBlindProof(this.root, child);
137
+ }
138
+ /** Get the active identity's private key for NIP-17/44/04 crypto operations */
139
+ get activePrivateKey() {
140
+ return this.activeEntry.identity.privateKey;
141
+ }
142
+ /** Test helper — returns reference to private key bytes for verifying zeroise */
143
+ _getPrivateKeyRefForTesting(npub) {
144
+ if (this.masterEntry.identity.npub === npub)
145
+ return this.masterEntry.identity.privateKey;
146
+ const entry = this.cache.get(npub);
147
+ return entry?.identity.privateKey;
148
+ }
149
+ /** Destroy all identities and the tree root */
150
+ destroy() {
151
+ zeroise(this.masterEntry.identity);
152
+ for (const [, entry] of this.cache) {
153
+ zeroise(entry.identity);
154
+ }
155
+ this.cache.clear();
156
+ this.root.destroy();
157
+ }
158
+ /** Insert into LRU cache, evicting oldest if at capacity */
159
+ putCache(npub, entry) {
160
+ if (this.cache.has(npub)) {
161
+ this.cache.set(npub, entry);
162
+ return;
163
+ }
164
+ if (this.cache.size >= this.maxCacheSize) {
165
+ let oldestNpub;
166
+ let oldestTime = Infinity;
167
+ for (const [n, e] of this.cache) {
168
+ // Never evict the active identity
169
+ if (n === this.activeEntry.identity.npub)
170
+ continue;
171
+ if (e.lastUsed < oldestTime) {
172
+ oldestTime = e.lastUsed;
173
+ oldestNpub = n;
174
+ }
175
+ }
176
+ if (oldestNpub) {
177
+ const evicted = this.cache.get(oldestNpub);
178
+ zeroise(evicted.identity);
179
+ this.cache.delete(oldestNpub);
180
+ }
181
+ }
182
+ this.cache.set(npub, entry);
183
+ }
184
+ }
@@ -0,0 +1,31 @@
1
+ import type { LinkageProof } from 'nsec-tree';
2
+ import type { IdentityContext } from '../context.js';
3
+ import type { PublicIdentity } from '../types.js';
4
+ /** Generate a fresh identity — returns mnemonic + master npub, no raw private keys */
5
+ export declare function handleIdentityCreate(): {
6
+ npub: string;
7
+ mnemonic: string;
8
+ };
9
+ /** Derive a child identity by purpose and index */
10
+ export declare function handleIdentityDerive(ctx: IdentityContext, args: {
11
+ purpose: string;
12
+ index: number;
13
+ }): PublicIdentity;
14
+ /** Derive a named persona */
15
+ export declare function handleIdentityDerivePersona(ctx: IdentityContext, args: {
16
+ name: string;
17
+ index: number;
18
+ }): PublicIdentity;
19
+ /** Switch active identity */
20
+ export declare function handleIdentitySwitch(ctx: IdentityContext, args: {
21
+ target: string;
22
+ index?: number;
23
+ }): {
24
+ npub: string;
25
+ };
26
+ /** List all known identities — returns public info only */
27
+ export declare function handleIdentityList(ctx: IdentityContext): PublicIdentity[];
28
+ /** Create a linkage proof for the active identity. Defaults to blind (no purpose/index). */
29
+ export declare function handleIdentityProve(ctx: IdentityContext, args: {
30
+ mode?: 'blind' | 'full';
31
+ }): LinkageProof;
@@ -0,0 +1,32 @@
1
+ import { generateMnemonic } from '@scure/bip39';
2
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
3
+ import { fromMnemonic } from 'nsec-tree';
4
+ /** Generate a fresh identity — returns mnemonic + master npub, no raw private keys */
5
+ export function handleIdentityCreate() {
6
+ const mnemonic = generateMnemonic(wordlist, 256); // 24 words
7
+ const root = fromMnemonic(mnemonic);
8
+ const npub = root.masterPubkey;
9
+ root.destroy();
10
+ return { npub, mnemonic };
11
+ }
12
+ /** Derive a child identity by purpose and index */
13
+ export function handleIdentityDerive(ctx, args) {
14
+ return ctx.derive(args.purpose, args.index);
15
+ }
16
+ /** Derive a named persona */
17
+ export function handleIdentityDerivePersona(ctx, args) {
18
+ return ctx.derivePersona(args.name, args.index);
19
+ }
20
+ /** Switch active identity */
21
+ export function handleIdentitySwitch(ctx, args) {
22
+ ctx.switch(args.target, args.index);
23
+ return { npub: ctx.activeNpub };
24
+ }
25
+ /** List all known identities — returns public info only */
26
+ export function handleIdentityList(ctx) {
27
+ return ctx.listIdentities();
28
+ }
29
+ /** Create a linkage proof for the active identity. Defaults to blind (no purpose/index). */
30
+ export function handleIdentityProve(ctx, args) {
31
+ return ctx.prove(args.mode ?? 'blind');
32
+ }
@@ -0,0 +1,37 @@
1
+ import type { Event as NostrEvent } from 'nostr-tools';
2
+ import type { IdentityContext } from '../context.js';
3
+ import type { RelayPool } from '../relay-pool.js';
4
+ export interface BackupBundle {
5
+ pubkeyHex: string;
6
+ events: NostrEvent[];
7
+ }
8
+ export interface RestoreResult {
9
+ restored: Array<{
10
+ kind: number;
11
+ id: string;
12
+ }>;
13
+ skipped: Array<{
14
+ kind: number;
15
+ reason: string;
16
+ }>;
17
+ }
18
+ export interface MigrateSummary {
19
+ profileFields: string[];
20
+ contactCount: number;
21
+ relayCount: number;
22
+ attestationCount: number;
23
+ }
24
+ export interface MigrateResult {
25
+ status: 'preview' | 'migrated';
26
+ summary: MigrateSummary;
27
+ }
28
+ /** Fetch all relevant events for a pubkey (profile, contacts, relay list, attestations) */
29
+ export declare function handleIdentityBackup(pool: RelayPool, pubkeyHex: string, npub: string): Promise<BackupBundle>;
30
+ /** Re-sign migratable events under the active identity. Skips attestations (trust chain protection). */
31
+ export declare function handleIdentityRestore(ctx: IdentityContext, pool: RelayPool, backup: BackupBundle): Promise<RestoreResult>;
32
+ /** Migrate identity: preview first, then execute when confirmed */
33
+ export declare function handleIdentityMigrate(ctx: IdentityContext, pool: RelayPool, args: {
34
+ oldPubkeyHex: string;
35
+ oldNpub: string;
36
+ confirm: boolean;
37
+ }): Promise<MigrateResult>;
@@ -0,0 +1,84 @@
1
+ import { verifyEvent } from 'nostr-tools/pure';
2
+ /** Kinds to backup and the subset that can be re-signed */
3
+ const BACKUP_KINDS = [0, 3, 10002, 31000];
4
+ const RESIGNABLE_KINDS = new Set([0, 3, 10002]);
5
+ /** Fetch all relevant events for a pubkey (profile, contacts, relay list, attestations) */
6
+ export async function handleIdentityBackup(pool, pubkeyHex, npub) {
7
+ const events = await pool.query(npub, {
8
+ kinds: BACKUP_KINDS,
9
+ authors: [pubkeyHex],
10
+ });
11
+ return { pubkeyHex, events };
12
+ }
13
+ /** Re-sign migratable events under the active identity. Skips attestations (trust chain protection). */
14
+ export async function handleIdentityRestore(ctx, pool, backup) {
15
+ const sign = ctx.getSigningFunction();
16
+ const restored = [];
17
+ const skipped = [];
18
+ for (const event of backup.events) {
19
+ // Verify signature and author before re-signing
20
+ if (!verifyEvent(event) || event.pubkey !== backup.pubkeyHex) {
21
+ skipped.push({
22
+ kind: event.kind,
23
+ reason: `Event ${event.id} failed signature verification or author mismatch`,
24
+ });
25
+ continue;
26
+ }
27
+ if (!RESIGNABLE_KINDS.has(event.kind)) {
28
+ skipped.push({
29
+ kind: event.kind,
30
+ reason: `Kind ${event.kind} attestation — cannot re-sign (trust chain protection)`,
31
+ });
32
+ continue;
33
+ }
34
+ // Re-sign the event content under the new identity
35
+ const template = {
36
+ kind: event.kind,
37
+ created_at: Math.floor(Date.now() / 1000),
38
+ tags: event.tags,
39
+ content: event.content,
40
+ };
41
+ const signed = await sign(template);
42
+ restored.push({ kind: event.kind, id: signed.id });
43
+ }
44
+ return { restored, skipped };
45
+ }
46
+ /** Migrate identity: preview first, then execute when confirmed */
47
+ export async function handleIdentityMigrate(ctx, pool, args) {
48
+ const backup = await handleIdentityBackup(pool, args.oldPubkeyHex, args.oldNpub);
49
+ // Build summary
50
+ const profileEvent = backup.events.find(e => e.kind === 0);
51
+ let profileFields = [];
52
+ if (profileEvent) {
53
+ try {
54
+ const parsed = JSON.parse(profileEvent.content);
55
+ profileFields = Object.keys(parsed);
56
+ }
57
+ catch { /* empty profile */ }
58
+ }
59
+ const contactEvent = backup.events.find(e => e.kind === 3);
60
+ const contactCount = contactEvent?.tags.filter(t => t[0] === 'p').length ?? 0;
61
+ const relayEvent = backup.events.find(e => e.kind === 10002);
62
+ const relayCount = relayEvent?.tags.filter(t => t[0] === 'r').length ?? 0;
63
+ const attestationCount = backup.events.filter(e => e.kind === 31000).length;
64
+ const summary = { profileFields, contactCount, relayCount, attestationCount };
65
+ if (!args.confirm) {
66
+ return { status: 'preview', summary };
67
+ }
68
+ // Execute migration: re-sign migratable events
69
+ await handleIdentityRestore(ctx, pool, backup);
70
+ // Publish linkage proof connecting old → new
71
+ const proof = ctx.prove('full');
72
+ const sign = ctx.getSigningFunction();
73
+ const proofEvent = await sign({
74
+ kind: 30078,
75
+ created_at: Math.floor(Date.now() / 1000),
76
+ tags: [
77
+ ['d', `migration:${args.oldPubkeyHex}`],
78
+ ['p', args.oldPubkeyHex],
79
+ ],
80
+ content: JSON.stringify(proof),
81
+ });
82
+ await pool.publish(args.oldNpub, proofEvent);
83
+ return { status: 'migrated', summary };
84
+ }
@@ -0,0 +1,18 @@
1
+ export interface BackupArgs {
2
+ secret: Uint8Array;
3
+ threshold: number;
4
+ shares: number;
5
+ outputDir: string;
6
+ }
7
+ export interface RestoreArgs {
8
+ files: string[];
9
+ threshold: number;
10
+ }
11
+ /** Split a secret into Shamir shards and write to files. Returns file paths only, never shard content. */
12
+ export declare function handleBackupShamir(args: BackupArgs): {
13
+ files: string[];
14
+ threshold: number;
15
+ shares: number;
16
+ };
17
+ /** Restore a secret from shard files. Returns the reconstructed secret bytes. */
18
+ export declare function handleRestoreShamir(args: RestoreArgs): Uint8Array;
@@ -0,0 +1,46 @@
1
+ import { writeFileSync, readFileSync, statSync, renameSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { splitSecret, reconstructSecret, shareToWords, wordsToShare, } from '@forgesworn/shamir-words';
4
+ /** Split a secret into Shamir shards and write to files. Returns file paths only, never shard content. */
5
+ export function handleBackupShamir(args) {
6
+ // Validate output directory exists
7
+ try {
8
+ const stat = statSync(args.outputDir);
9
+ if (!stat.isDirectory())
10
+ throw new Error(`Not a directory: ${args.outputDir}`);
11
+ }
12
+ catch (err) {
13
+ if (err.code === 'ENOENT')
14
+ throw new Error(`Output directory does not exist: ${args.outputDir}`);
15
+ throw err;
16
+ }
17
+ const shamirShares = splitSecret(args.secret, args.threshold, args.shares);
18
+ const files = [];
19
+ // Write to temp files first, then rename atomically
20
+ const tempPaths = [];
21
+ for (let i = 0; i < shamirShares.length; i++) {
22
+ const words = shareToWords(shamirShares[i]);
23
+ const filePath = join(args.outputDir, `shard-${i + 1}.bray`);
24
+ const tempPath = `${filePath}.tmp`;
25
+ writeFileSync(tempPath, words.join(' '), { encoding: 'utf-8', mode: 0o600 });
26
+ tempPaths.push(tempPath);
27
+ files.push(filePath);
28
+ }
29
+ // Atomic rename — all or nothing
30
+ for (let i = 0; i < tempPaths.length; i++) {
31
+ renameSync(tempPaths[i], files[i]);
32
+ }
33
+ return { files, threshold: args.threshold, shares: args.shares };
34
+ }
35
+ /** Restore a secret from shard files. Returns the reconstructed secret bytes. */
36
+ export function handleRestoreShamir(args) {
37
+ if (args.files.length < args.threshold) {
38
+ throw new Error(`Insufficient shards: have ${args.files.length}, need ${args.threshold}`);
39
+ }
40
+ const shares = args.files.map(filePath => {
41
+ const content = readFileSync(filePath, 'utf-8').trim();
42
+ const words = content.split(' ');
43
+ return wordsToShare(words);
44
+ });
45
+ return reconstructSecret(shares, args.threshold);
46
+ }
@@ -0,0 +1,11 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { IdentityContext } from '../context.js';
3
+ import type { RelayPool } from '../relay-pool.js';
4
+ import type { Nip65Manager } from '../nip65.js';
5
+ export interface ToolDeps {
6
+ ctx: IdentityContext;
7
+ pool: RelayPool;
8
+ nip65: Nip65Manager;
9
+ nwcUri?: string;
10
+ }
11
+ export declare function registerIdentityTools(server: McpServer, deps: ToolDeps): void;
@@ -0,0 +1,168 @@
1
+ import { z } from 'zod';
2
+ import { handleIdentityCreate, handleIdentityDerive, handleIdentityDerivePersona, handleIdentitySwitch, handleIdentityList, handleIdentityProve, } from './handlers.js';
3
+ import { handleBackupShamir, handleRestoreShamir } from './shamir.js';
4
+ import { hexId } from '../validation.js';
5
+ import { handleIdentityBackup, handleIdentityRestore, handleIdentityMigrate } from './migration.js';
6
+ export function registerIdentityTools(server, deps) {
7
+ server.registerTool('identity_create', {
8
+ description: 'Generate a fresh Nostr identity with a BIP-39 mnemonic seed. Returns the master npub and mnemonic for backup.',
9
+ annotations: { readOnlyHint: false },
10
+ }, async () => {
11
+ const result = handleIdentityCreate();
12
+ return {
13
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
14
+ };
15
+ });
16
+ server.registerTool('identity_derive', {
17
+ description: 'Derive a child Nostr identity from the active tree root. Returns the new npub.',
18
+ inputSchema: {
19
+ purpose: z.string().describe('Purpose string for derivation (e.g. "messaging", "signing")'),
20
+ index: z.number().int().min(0).default(0).describe('Derivation index (default 0)'),
21
+ },
22
+ annotations: { readOnlyHint: false },
23
+ }, async ({ purpose, index }) => {
24
+ const result = handleIdentityDerive(deps.ctx, { purpose, index });
25
+ return {
26
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
27
+ };
28
+ });
29
+ server.registerTool('identity_derive_persona', {
30
+ description: 'Derive a named persona identity (e.g. "work", "personal", "anonymous"). Returns the new npub.',
31
+ inputSchema: {
32
+ name: z.string().describe('Persona name (e.g. "work", "personal", "anonymous")'),
33
+ index: z.number().int().min(0).default(0).describe('Derivation index (default 0)'),
34
+ },
35
+ annotations: { readOnlyHint: false },
36
+ }, async ({ name, index }) => {
37
+ const result = handleIdentityDerivePersona(deps.ctx, { name, index });
38
+ return {
39
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
40
+ };
41
+ });
42
+ server.registerTool('identity_switch', {
43
+ description: 'Switch the active Nostr identity. All subsequent tools operate as the new identity. Use "master" to return to root.',
44
+ inputSchema: {
45
+ target: z.string().describe('Purpose, persona name, or "master" to switch to'),
46
+ index: z.number().int().min(0).optional().describe('Derivation index (if switching by purpose)'),
47
+ },
48
+ annotations: { readOnlyHint: false },
49
+ }, async ({ target, index }) => {
50
+ const result = handleIdentitySwitch(deps.ctx, { target, index });
51
+ return {
52
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
53
+ };
54
+ });
55
+ server.registerTool('identity_list', {
56
+ description: 'List all known Nostr identities. Returns npub, purpose, and persona name for each. Never includes private keys.',
57
+ annotations: { readOnlyHint: true },
58
+ }, async () => {
59
+ const result = handleIdentityList(deps.ctx);
60
+ return {
61
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
62
+ };
63
+ });
64
+ server.registerTool('identity_prove', {
65
+ description: 'Create a cryptographic linkage proof between the master key and the active identity. Defaults to blind proof (no purpose/index revealed).',
66
+ inputSchema: {
67
+ mode: z.enum(['blind', 'full']).default('blind').describe('Proof mode: "blind" hides derivation path, "full" reveals purpose and index'),
68
+ },
69
+ annotations: { readOnlyHint: true },
70
+ }, async ({ mode }) => {
71
+ const proof = handleIdentityProve(deps.ctx, { mode });
72
+ return {
73
+ content: [{ type: 'text', text: JSON.stringify(proof, null, 2) }],
74
+ };
75
+ });
76
+ server.registerTool('identity_backup_shamir', {
77
+ description: 'Split the master secret into Shamir shard files. Shard content is written to disk — never returned in the response.',
78
+ inputSchema: {
79
+ threshold: z.number().int().min(2).describe('Minimum shards needed to reconstruct'),
80
+ shares: z.number().int().min(2).describe('Total number of shards to create'),
81
+ outputDir: z.string().describe('Directory to write shard files to'),
82
+ },
83
+ annotations: { readOnlyHint: false, destructiveHint: false },
84
+ }, async ({ threshold, shares, outputDir }) => {
85
+ const result = handleBackupShamir({
86
+ secret: new Uint8Array(deps.ctx.activePrivateKey),
87
+ threshold,
88
+ shares,
89
+ outputDir,
90
+ });
91
+ return {
92
+ content: [{ type: 'text', text: JSON.stringify({
93
+ message: `Created ${result.shares} shards (threshold: ${result.threshold})`,
94
+ files: result.files,
95
+ }, null, 2) }],
96
+ };
97
+ });
98
+ server.registerTool('identity_restore_shamir', {
99
+ description: 'Reconstruct a secret from Shamir shard files. Returns the restored npub for verification.',
100
+ inputSchema: {
101
+ files: z.array(z.string()).describe('Paths to shard files'),
102
+ threshold: z.number().int().min(2).describe('Threshold used during backup'),
103
+ },
104
+ annotations: { readOnlyHint: true },
105
+ }, async ({ files, threshold }) => {
106
+ const secret = handleRestoreShamir({ files, threshold });
107
+ // Verify by creating a temporary context from the restored secret
108
+ const { fromNsec } = await import('nsec-tree');
109
+ const root = fromNsec(secret);
110
+ const npub = root.masterPubkey;
111
+ root.destroy();
112
+ // Zeroise the restored secret
113
+ secret.fill(0);
114
+ return {
115
+ content: [{ type: 'text', text: JSON.stringify({
116
+ message: 'Secret reconstructed successfully',
117
+ masterNpub: npub,
118
+ }, null, 2) }],
119
+ };
120
+ });
121
+ server.registerTool('identity_backup', {
122
+ description: 'Fetch profile, contacts, relay list, and attestations for a pubkey. Returns a portable JSON bundle (no private keys).',
123
+ inputSchema: {
124
+ pubkeyHex: hexId.describe('Hex pubkey to back up'),
125
+ npub: z.string().optional().describe('Bech32 npub for relay routing (defaults to active identity)'),
126
+ },
127
+ annotations: { readOnlyHint: true },
128
+ }, async ({ pubkeyHex, npub }) => {
129
+ const resolvedNpub = npub ?? deps.ctx.activeNpub;
130
+ const bundle = await handleIdentityBackup(deps.pool, pubkeyHex, resolvedNpub);
131
+ return {
132
+ content: [{ type: 'text', text: JSON.stringify({
133
+ pubkeyHex: bundle.pubkeyHex,
134
+ eventCount: bundle.events.length,
135
+ kinds: [...new Set(bundle.events.map(e => e.kind))],
136
+ }, null, 2) }],
137
+ };
138
+ });
139
+ server.registerTool('identity_restore', {
140
+ description: 'Re-sign migratable events (profile, contacts, relay list) under the active identity. Skips attestations (trust chain protection).',
141
+ inputSchema: {
142
+ pubkeyHex: hexId.describe('Hex pubkey of the original identity to restore from'),
143
+ npub: z.string().optional().describe('Bech32 npub for relay routing'),
144
+ },
145
+ annotations: { readOnlyHint: false },
146
+ }, async ({ pubkeyHex, npub }) => {
147
+ const resolvedNpub = npub ?? deps.ctx.activeNpub;
148
+ const backup = await handleIdentityBackup(deps.pool, pubkeyHex, resolvedNpub);
149
+ const result = await handleIdentityRestore(deps.ctx, deps.pool, backup);
150
+ return {
151
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
152
+ };
153
+ });
154
+ server.registerTool('identity_migrate', {
155
+ description: 'Migrate from an old identity to the active one. Shows preview first — set confirm: true to execute. Publishes linkage proof and re-signs migratable events.',
156
+ inputSchema: {
157
+ oldPubkeyHex: z.string().describe('Hex pubkey of the old identity'),
158
+ oldNpub: z.string().describe('Bech32 npub of the old identity'),
159
+ confirm: z.boolean().default(false).describe('Set true to execute migration (preview by default)'),
160
+ },
161
+ annotations: { readOnlyHint: false },
162
+ }, async ({ oldPubkeyHex, oldNpub, confirm }) => {
163
+ const result = await handleIdentityMigrate(deps.ctx, deps.pool, { oldPubkeyHex, oldNpub, confirm });
164
+ return {
165
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
166
+ };
167
+ });
168
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { IdentityContext } from './context.js';
3
+ import { RelayPool } from './relay-pool.js';
4
+ import { Nip65Manager } from './nip65.js';
5
+ export declare const deps: {
6
+ ctx: IdentityContext;
7
+ pool: RelayPool;
8
+ nip65: Nip65Manager;
9
+ nwcUri: string | undefined;
10
+ };