nostr-bray 1.9.1 ā 1.10.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/dist/cli.js +27 -14
- package/dist/context.d.ts +4 -1
- package/dist/context.js +38 -8
- package/dist/format.d.ts +30 -0
- package/dist/format.js +153 -0
- package/dist/identity/migration.js +18 -13
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { handleDuressConfigure, handleDuressActivate } from './safety/handlers.j
|
|
|
20
20
|
import { handleZapSend, handleZapBalance, handleZapMakeInvoice, handleZapLookupInvoice, handleZapListTransactions, handleZapReceipts, handleZapDecode } from './zap/handlers.js';
|
|
21
21
|
import { handleDecode, handleEncodeNpub, handleEncodeNote, handleEncodeNprofile, handleEncodeNevent, handleVerify, handleEncrypt, handleDecrypt, handleCount, handleFetch, handleKeyPublic, handleEncodeNsec, handleFilter, handleNipList, handleNipShow } from './util/handlers.js';
|
|
22
22
|
import { getCommandHelp } from './help.js';
|
|
23
|
+
import * as fmt from './format.js';
|
|
23
24
|
const args = process.argv.slice(2);
|
|
24
25
|
const command = args[0];
|
|
25
26
|
// No command = start MCP server
|
|
@@ -147,6 +148,10 @@ Quick examples:
|
|
|
147
148
|
nostr-bray nips # browse official NIPs
|
|
148
149
|
nostr-bray shell # interactive mode
|
|
149
150
|
|
|
151
|
+
Flags:
|
|
152
|
+
--json Output raw JSON instead of human-readable
|
|
153
|
+
--help Show help for a command
|
|
154
|
+
|
|
150
155
|
Use 'nostr-bray <command> --help' for detailed help on any command.
|
|
151
156
|
|
|
152
157
|
Learn more:
|
|
@@ -171,8 +176,16 @@ config.secretKey = '';
|
|
|
171
176
|
config.nwcUri = undefined;
|
|
172
177
|
const masterRelays = await nip65.loadForIdentity(ctx.activeNpub);
|
|
173
178
|
pool.reconfigure(ctx.activeNpub, masterRelays);
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
const jsonMode = args.includes('--json');
|
|
180
|
+
/** Print JSON (always) or human-readable (default) */
|
|
181
|
+
function out(data, humanFormatter) {
|
|
182
|
+
if (jsonMode || !humanFormatter) {
|
|
183
|
+
console.log(JSON.stringify(data, null, 2));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(humanFormatter(data));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
176
189
|
/** Parse a shell line into args, respecting quotes */
|
|
177
190
|
function parseShellLine(line) {
|
|
178
191
|
const result = [];
|
|
@@ -232,7 +245,7 @@ async function run(cmdArgs) {
|
|
|
232
245
|
out(handleIdentityCreate());
|
|
233
246
|
break;
|
|
234
247
|
case 'list':
|
|
235
|
-
out(handleIdentityList(ctx));
|
|
248
|
+
out(handleIdentityList(ctx), fmt.formatIdentityList);
|
|
236
249
|
break;
|
|
237
250
|
case 'derive':
|
|
238
251
|
out(ctx.derive(req(1, 'derive <purpose> [index]'), parseInt(cmdArgs[2] ?? '0', 10)));
|
|
@@ -284,7 +297,7 @@ async function run(cmdArgs) {
|
|
|
284
297
|
break;
|
|
285
298
|
// === Social ===
|
|
286
299
|
case 'post':
|
|
287
|
-
out(await handleSocialPost(ctx, pool, { content: req(1, 'post "message"') }));
|
|
300
|
+
out(await handleSocialPost(ctx, pool, { content: req(1, 'post "message"') }), fmt.formatPost);
|
|
288
301
|
break;
|
|
289
302
|
case 'reply':
|
|
290
303
|
out(await handleSocialReply(ctx, pool, {
|
|
@@ -301,7 +314,7 @@ async function run(cmdArgs) {
|
|
|
301
314
|
}));
|
|
302
315
|
break;
|
|
303
316
|
case 'profile':
|
|
304
|
-
out(await handleSocialProfileGet(pool, ctx.activeNpub, req(1, 'profile <pubkey-hex>')));
|
|
317
|
+
out(await handleSocialProfileGet(pool, ctx.activeNpub, req(1, 'profile <pubkey-hex>')), fmt.formatProfile);
|
|
305
318
|
break;
|
|
306
319
|
case 'profile-set': {
|
|
307
320
|
const profile = JSON.parse(req(1, 'profile-set \'{"name":"..."}\''));
|
|
@@ -321,7 +334,7 @@ async function run(cmdArgs) {
|
|
|
321
334
|
}));
|
|
322
335
|
break;
|
|
323
336
|
case 'contacts':
|
|
324
|
-
out(await handleContactsGet(pool, ctx.activeNpub, req(1, 'contacts <pubkey-hex>')));
|
|
337
|
+
out(await handleContactsGet(pool, ctx.activeNpub, req(1, 'contacts <pubkey-hex>')), fmt.formatContacts);
|
|
325
338
|
break;
|
|
326
339
|
case 'follow':
|
|
327
340
|
out(await handleContactsFollow(ctx, pool, {
|
|
@@ -344,10 +357,10 @@ async function run(cmdArgs) {
|
|
|
344
357
|
}));
|
|
345
358
|
break;
|
|
346
359
|
case 'dm-read':
|
|
347
|
-
out(await handleDmRead(ctx, pool));
|
|
360
|
+
out(await handleDmRead(ctx, pool), fmt.formatDms);
|
|
348
361
|
break;
|
|
349
362
|
case 'feed':
|
|
350
|
-
out(await handleFeed(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }));
|
|
363
|
+
out(await handleFeed(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }), fmt.formatFeed);
|
|
351
364
|
break;
|
|
352
365
|
case 'nip-publish': {
|
|
353
366
|
const id = req(1, 'nip-publish <identifier> <title> <content-or-file>');
|
|
@@ -370,7 +383,7 @@ async function run(cmdArgs) {
|
|
|
370
383
|
}));
|
|
371
384
|
break;
|
|
372
385
|
case 'notifications':
|
|
373
|
-
out(await handleNotifications(ctx, pool, { limit: parseInt(flag('limit', '50'), 10) }));
|
|
386
|
+
out(await handleNotifications(ctx, pool, { limit: parseInt(flag('limit', '50'), 10) }), fmt.formatNotifications);
|
|
374
387
|
break;
|
|
375
388
|
// === Trust ===
|
|
376
389
|
case 'attest':
|
|
@@ -434,7 +447,7 @@ async function run(cmdArgs) {
|
|
|
434
447
|
break;
|
|
435
448
|
// === Relay ===
|
|
436
449
|
case 'relay-list':
|
|
437
|
-
out(handleRelayList(ctx, pool, flag('compare')));
|
|
450
|
+
out(handleRelayList(ctx, pool, flag('compare')), fmt.formatRelays);
|
|
438
451
|
break;
|
|
439
452
|
case 'relay-set': {
|
|
440
453
|
const urls = args.slice(1).filter(a => !a.startsWith('--'));
|
|
@@ -477,7 +490,7 @@ async function run(cmdArgs) {
|
|
|
477
490
|
}));
|
|
478
491
|
break;
|
|
479
492
|
case 'zap-receipts':
|
|
480
|
-
out(await handleZapReceipts(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }));
|
|
493
|
+
out(await handleZapReceipts(ctx, pool, { limit: parseInt(flag('limit', '20'), 10) }), fmt.formatZapReceipts);
|
|
481
494
|
break;
|
|
482
495
|
case 'zap-decode':
|
|
483
496
|
out(handleZapDecode(req(1, 'zap-decode <bolt11>')));
|
|
@@ -520,7 +533,7 @@ async function run(cmdArgs) {
|
|
|
520
533
|
out(await handleGroupChat(pool, ctx.activeNpub, {
|
|
521
534
|
groupId: req(1, 'group-chat <group-id>'),
|
|
522
535
|
limit: parseInt(flag('limit', '20'), 10),
|
|
523
|
-
}));
|
|
536
|
+
}), fmt.formatGroupChat);
|
|
524
537
|
break;
|
|
525
538
|
case 'group-send':
|
|
526
539
|
out(await handleGroupSend(ctx, pool, {
|
|
@@ -535,7 +548,7 @@ async function run(cmdArgs) {
|
|
|
535
548
|
break;
|
|
536
549
|
// === Utility ===
|
|
537
550
|
case 'decode':
|
|
538
|
-
out(handleDecode(req(1, 'decode <nip19>')));
|
|
551
|
+
out(handleDecode(req(1, 'decode <nip19>')), fmt.formatDecode);
|
|
539
552
|
break;
|
|
540
553
|
case 'encode-npub':
|
|
541
554
|
console.log(handleEncodeNpub(req(1, 'encode-npub <hex>')));
|
|
@@ -563,7 +576,7 @@ async function run(cmdArgs) {
|
|
|
563
576
|
out(handleFilter(JSON.parse(req(1, 'filter <event-json> <filter-json>')), JSON.parse(req(2, 'filter <event-json> <filter-json>'))));
|
|
564
577
|
break;
|
|
565
578
|
case 'nips':
|
|
566
|
-
out(await handleNipList());
|
|
579
|
+
out(await handleNipList(), fmt.formatNipList);
|
|
567
580
|
break;
|
|
568
581
|
case 'nip': {
|
|
569
582
|
const num = parseInt(req(1, 'nip <number>'), 10);
|
package/dist/context.d.ts
CHANGED
|
@@ -24,8 +24,11 @@ export declare class IdentityContext {
|
|
|
24
24
|
getSigningFunction(): SignFn;
|
|
25
25
|
/** List all known identities ā returns public info only, never private keys */
|
|
26
26
|
listIdentities(): PublicIdentity[];
|
|
27
|
-
/** Create a linkage proof for the active identity
|
|
27
|
+
/** Create a linkage proof for the active identity.
|
|
28
|
+
* Only works for derived identities ā the master IS the raw key, not a tree child. */
|
|
28
29
|
prove(mode?: 'blind' | 'full'): LinkageProof;
|
|
30
|
+
/** Get the nsec-tree root's master pubkey (the derivation anchor, distinct from the raw key's pubkey) */
|
|
31
|
+
get treeRootPubkey(): string;
|
|
29
32
|
/** Get the active identity's private key for NIP-17/44/04 crypto operations */
|
|
30
33
|
get activePrivateKey(): Uint8Array;
|
|
31
34
|
/** Test helper ā returns reference to private key bytes for verifying zeroise */
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { fromNsec, fromMnemonic, derive, zeroise } from 'nsec-tree';
|
|
2
2
|
import { derivePersona } from 'nsec-tree/persona';
|
|
3
3
|
import { createBlindProof, createFullProof } from 'nsec-tree/proof';
|
|
4
|
-
import { finalizeEvent } from 'nostr-tools/pure';
|
|
4
|
+
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
|
5
|
+
import { decode, npubEncode, nsecEncode } from 'nostr-tools/nip19';
|
|
6
|
+
/** Build an Identity-compatible object from raw key bytes */
|
|
7
|
+
function rawIdentity(privateKey) {
|
|
8
|
+
const publicKeyHex = getPublicKey(privateKey);
|
|
9
|
+
const publicKey = Buffer.from(publicKeyHex, 'hex');
|
|
10
|
+
return {
|
|
11
|
+
nsec: nsecEncode(privateKey),
|
|
12
|
+
npub: npubEncode(publicKeyHex),
|
|
13
|
+
privateKey: new Uint8Array(privateKey), // copy so original can be cleaned
|
|
14
|
+
publicKey: new Uint8Array(publicKey),
|
|
15
|
+
purpose: 'master',
|
|
16
|
+
index: 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
5
19
|
export class IdentityContext {
|
|
6
20
|
root;
|
|
7
21
|
cache = new Map();
|
|
@@ -10,25 +24,33 @@ export class IdentityContext {
|
|
|
10
24
|
maxCacheSize;
|
|
11
25
|
constructor(secretKey, format, opts) {
|
|
12
26
|
this.maxCacheSize = opts?.maxCache ?? 5;
|
|
27
|
+
// Parse the raw secret key bytes ā this IS the user's actual Nostr identity
|
|
28
|
+
let rawKeyBytes;
|
|
13
29
|
if (format === 'mnemonic') {
|
|
30
|
+
// Mnemonic: create tree root, derive a "default" identity as master
|
|
14
31
|
this.root = fromMnemonic(secretKey);
|
|
32
|
+
const derived = derive(this.root, 'master', 0);
|
|
33
|
+
this.masterEntry = { identity: derived, purpose: 'master', index: 0, lastUsed: Date.now() };
|
|
34
|
+
this.activeEntry = this.masterEntry;
|
|
35
|
+
return;
|
|
15
36
|
}
|
|
16
37
|
else if (format === 'hex') {
|
|
17
|
-
|
|
18
|
-
this.root = fromNsec(bytes);
|
|
38
|
+
rawKeyBytes = Buffer.from(secretKey, 'hex');
|
|
19
39
|
}
|
|
20
40
|
else {
|
|
21
|
-
|
|
41
|
+
// nsec bech32
|
|
42
|
+
rawKeyBytes = decode(secretKey).data;
|
|
22
43
|
}
|
|
23
|
-
//
|
|
24
|
-
const masterIdentity = derive(this.root, 'master', 0);
|
|
44
|
+
// Master identity = the user's actual keypair (their real npub)
|
|
25
45
|
this.masterEntry = {
|
|
26
|
-
identity:
|
|
46
|
+
identity: rawIdentity(rawKeyBytes),
|
|
27
47
|
purpose: 'master',
|
|
28
48
|
index: 0,
|
|
29
49
|
lastUsed: Date.now(),
|
|
30
50
|
};
|
|
31
51
|
this.activeEntry = this.masterEntry;
|
|
52
|
+
// nsec-tree root for child derivation
|
|
53
|
+
this.root = fromNsec(rawKeyBytes);
|
|
32
54
|
}
|
|
33
55
|
/** Current active identity's npub (bech32) */
|
|
34
56
|
get activeNpub() {
|
|
@@ -131,14 +153,22 @@ export class IdentityContext {
|
|
|
131
153
|
}
|
|
132
154
|
return result;
|
|
133
155
|
}
|
|
134
|
-
/** Create a linkage proof for the active identity
|
|
156
|
+
/** Create a linkage proof for the active identity.
|
|
157
|
+
* Only works for derived identities ā the master IS the raw key, not a tree child. */
|
|
135
158
|
prove(mode = 'blind') {
|
|
159
|
+
if (this.activeEntry === this.masterEntry) {
|
|
160
|
+
throw new Error('Cannot prove master identity ā it is the raw key, not a derived child. Switch to a derived identity first.');
|
|
161
|
+
}
|
|
136
162
|
const child = this.activeEntry.identity;
|
|
137
163
|
if (mode === 'full') {
|
|
138
164
|
return createFullProof(this.root, child);
|
|
139
165
|
}
|
|
140
166
|
return createBlindProof(this.root, child);
|
|
141
167
|
}
|
|
168
|
+
/** Get the nsec-tree root's master pubkey (the derivation anchor, distinct from the raw key's pubkey) */
|
|
169
|
+
get treeRootPubkey() {
|
|
170
|
+
return this.root.masterPubkey;
|
|
171
|
+
}
|
|
142
172
|
/** Get the active identity's private key for NIP-17/44/04 crypto operations */
|
|
143
173
|
get activePrivateKey() {
|
|
144
174
|
return this.activeEntry.identity.privateKey;
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Human-readable formatters for CLI output */
|
|
2
|
+
import type { PublicIdentity } from './types.js';
|
|
3
|
+
export declare function formatIdentity(id: PublicIdentity): string;
|
|
4
|
+
export declare function formatIdentityList(ids: PublicIdentity[]): string;
|
|
5
|
+
export declare function formatPost(result: any): string;
|
|
6
|
+
export declare function formatPublish(result: any): string;
|
|
7
|
+
export declare function formatProfile(profile: Record<string, unknown>): string;
|
|
8
|
+
export declare function formatContacts(contacts: Array<{
|
|
9
|
+
pubkey: string;
|
|
10
|
+
relay?: string;
|
|
11
|
+
petname?: string;
|
|
12
|
+
}>): string;
|
|
13
|
+
export declare function formatNotifications(notifications: any[]): string;
|
|
14
|
+
export declare function formatFeed(events: any[]): string;
|
|
15
|
+
export declare function formatDms(dms: any[]): string;
|
|
16
|
+
export declare function formatRelays(relays: {
|
|
17
|
+
read: string[];
|
|
18
|
+
write: string[];
|
|
19
|
+
sharedWarning?: string;
|
|
20
|
+
}): string;
|
|
21
|
+
export declare function formatZapReceipts(receipts: any[]): string;
|
|
22
|
+
export declare function formatGroupChat(messages: any[]): string;
|
|
23
|
+
export declare function formatNipList(nips: Array<{
|
|
24
|
+
number: number;
|
|
25
|
+
title: string;
|
|
26
|
+
}>): string;
|
|
27
|
+
export declare function formatDecode(result: {
|
|
28
|
+
type: string;
|
|
29
|
+
data: unknown;
|
|
30
|
+
}): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Human-readable formatters for CLI output */
|
|
2
|
+
export function formatIdentity(id) {
|
|
3
|
+
let line = `${id.npub}`;
|
|
4
|
+
if (id.personaName)
|
|
5
|
+
line += ` (persona: ${id.personaName})`;
|
|
6
|
+
else if (id.purpose)
|
|
7
|
+
line += ` (${id.purpose}${id.index ? ` #${id.index}` : ''})`;
|
|
8
|
+
return line;
|
|
9
|
+
}
|
|
10
|
+
export function formatIdentityList(ids) {
|
|
11
|
+
if (ids.length === 0)
|
|
12
|
+
return 'No identities found.';
|
|
13
|
+
const lines = ids.map((id, i) => {
|
|
14
|
+
const marker = i === 0 ? '* ' : ' ';
|
|
15
|
+
return `${marker}${formatIdentity(id)}`;
|
|
16
|
+
});
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
19
|
+
export function formatPost(result) {
|
|
20
|
+
const status = result.publish?.success ? 'ā' : 'ā';
|
|
21
|
+
return `${status} ${result.event?.id?.slice(0, 12)}... published to ${result.publish?.accepted?.length ?? 0} relay(s)`;
|
|
22
|
+
}
|
|
23
|
+
export function formatPublish(result) {
|
|
24
|
+
const status = result.publish?.success ? 'ā Published' : 'ā Failed';
|
|
25
|
+
const relays = result.publish?.accepted?.length ?? 0;
|
|
26
|
+
return `${status} (${relays} relay${relays !== 1 ? 's' : ''})`;
|
|
27
|
+
}
|
|
28
|
+
export function formatProfile(profile) {
|
|
29
|
+
if (Object.keys(profile).length === 0)
|
|
30
|
+
return 'No profile found.';
|
|
31
|
+
const lines = [];
|
|
32
|
+
if (profile.name)
|
|
33
|
+
lines.push(`Name: ${profile.name}`);
|
|
34
|
+
if (profile.display_name)
|
|
35
|
+
lines.push(`Display: ${profile.display_name}`);
|
|
36
|
+
if (profile.about)
|
|
37
|
+
lines.push(`About: ${profile.about}`);
|
|
38
|
+
if (profile.nip05)
|
|
39
|
+
lines.push(`NIP-05: ${profile.nip05}`);
|
|
40
|
+
if (profile.lud16)
|
|
41
|
+
lines.push(`LN: ${profile.lud16}`);
|
|
42
|
+
if (profile.picture)
|
|
43
|
+
lines.push(`Picture: ${profile.picture}`);
|
|
44
|
+
if (profile.banner)
|
|
45
|
+
lines.push(`Banner: ${profile.banner}`);
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
export function formatContacts(contacts) {
|
|
49
|
+
if (contacts.length === 0)
|
|
50
|
+
return 'No contacts found.';
|
|
51
|
+
const lines = contacts.map(c => {
|
|
52
|
+
let line = c.pubkey.slice(0, 12) + '...';
|
|
53
|
+
if (c.petname)
|
|
54
|
+
line += ` ${c.petname}`;
|
|
55
|
+
if (c.relay)
|
|
56
|
+
line += ` (${c.relay})`;
|
|
57
|
+
return line;
|
|
58
|
+
});
|
|
59
|
+
return `${contacts.length} contacts:\n${lines.join('\n')}`;
|
|
60
|
+
}
|
|
61
|
+
export function formatNotifications(notifications) {
|
|
62
|
+
if (notifications.length === 0)
|
|
63
|
+
return 'No notifications.';
|
|
64
|
+
return notifications.map(n => {
|
|
65
|
+
const time = new Date(n.createdAt * 1000).toLocaleString();
|
|
66
|
+
const from = n.from?.slice(0, 12) + '...';
|
|
67
|
+
switch (n.type) {
|
|
68
|
+
case 'reply':
|
|
69
|
+
return `š¬ ${from} replied: ${n.content?.slice(0, 80)} (${time})`;
|
|
70
|
+
case 'reaction':
|
|
71
|
+
return `${n.content === '+' ? 'ā¤ļø' : n.content} ${from} reacted (${time})`;
|
|
72
|
+
case 'mention':
|
|
73
|
+
return `@ ${from} mentioned you: ${n.content?.slice(0, 80)} (${time})`;
|
|
74
|
+
case 'zap':
|
|
75
|
+
const sats = n.amountMsats ? `${Math.round(n.amountMsats / 1000)} sats` : 'zap';
|
|
76
|
+
return `ā” ${n.zapSender?.slice(0, 12) ?? from}... zapped ${sats}${n.zapMessage ? `: ${n.zapMessage}` : ''} (${time})`;
|
|
77
|
+
default:
|
|
78
|
+
return `${n.type} from ${from} (${time})`;
|
|
79
|
+
}
|
|
80
|
+
}).join('\n');
|
|
81
|
+
}
|
|
82
|
+
export function formatFeed(events) {
|
|
83
|
+
if (events.length === 0)
|
|
84
|
+
return 'No posts found.';
|
|
85
|
+
return events.map(e => {
|
|
86
|
+
const time = new Date(e.createdAt * 1000).toLocaleString();
|
|
87
|
+
const author = e.pubkey?.slice(0, 12) + '...';
|
|
88
|
+
return `${author} ${time}\n ${e.content?.slice(0, 200)}\n`;
|
|
89
|
+
}).join('\n');
|
|
90
|
+
}
|
|
91
|
+
export function formatDms(dms) {
|
|
92
|
+
if (dms.length === 0)
|
|
93
|
+
return 'No messages.';
|
|
94
|
+
return dms.map(dm => {
|
|
95
|
+
const time = new Date(dm.createdAt * 1000).toLocaleString();
|
|
96
|
+
const from = dm.from?.slice(0, 12) + '...';
|
|
97
|
+
const proto = dm.protocol === 'nip04-deprecated' ? ' [NIP-04]' : '';
|
|
98
|
+
if (!dm.decrypted)
|
|
99
|
+
return `ā ${from} Could not decrypt${proto} (${time})`;
|
|
100
|
+
return `${from}${proto} ${time}\n ${dm.content}`;
|
|
101
|
+
}).join('\n\n');
|
|
102
|
+
}
|
|
103
|
+
export function formatRelays(relays) {
|
|
104
|
+
const lines = [];
|
|
105
|
+
if (relays.read.length > 0) {
|
|
106
|
+
lines.push('Read:');
|
|
107
|
+
relays.read.forEach(r => lines.push(` ${r}`));
|
|
108
|
+
}
|
|
109
|
+
if (relays.write.length > 0) {
|
|
110
|
+
lines.push('Write:');
|
|
111
|
+
relays.write.forEach(r => lines.push(` ${r}`));
|
|
112
|
+
}
|
|
113
|
+
if (relays.sharedWarning)
|
|
114
|
+
lines.push(`\nā ļø ${relays.sharedWarning}`);
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
export function formatZapReceipts(receipts) {
|
|
118
|
+
if (receipts.length === 0)
|
|
119
|
+
return 'No zap receipts.';
|
|
120
|
+
return receipts.map(r => {
|
|
121
|
+
const time = new Date(r.createdAt * 1000).toLocaleString();
|
|
122
|
+
const sats = r.amountMsats ? `${Math.round(r.amountMsats / 1000)} sats` : '? sats';
|
|
123
|
+
const from = r.sender ? r.sender.slice(0, 12) + '...' : 'unknown';
|
|
124
|
+
return `ā” ${sats} from ${from}${r.message ? ` ā ${r.message}` : ''} (${time})`;
|
|
125
|
+
}).join('\n');
|
|
126
|
+
}
|
|
127
|
+
export function formatGroupChat(messages) {
|
|
128
|
+
if (messages.length === 0)
|
|
129
|
+
return 'No messages.';
|
|
130
|
+
return messages.map(m => {
|
|
131
|
+
const time = new Date(m.createdAt * 1000).toLocaleString();
|
|
132
|
+
const author = m.pubkey?.slice(0, 12) + '...';
|
|
133
|
+
return `${author} ${time}\n ${m.content}`;
|
|
134
|
+
}).join('\n\n');
|
|
135
|
+
}
|
|
136
|
+
export function formatNipList(nips) {
|
|
137
|
+
return nips.map(n => `NIP-${String(n.number).padStart(2, '0')} ${n.title}`).join('\n');
|
|
138
|
+
}
|
|
139
|
+
export function formatDecode(result) {
|
|
140
|
+
const lines = [`Type: ${result.type}`];
|
|
141
|
+
if (typeof result.data === 'string') {
|
|
142
|
+
lines.push(`Data: ${result.data}`);
|
|
143
|
+
}
|
|
144
|
+
else if (typeof result.data === 'object' && result.data !== null) {
|
|
145
|
+
for (const [k, v] of Object.entries(result.data)) {
|
|
146
|
+
if (Array.isArray(v))
|
|
147
|
+
lines.push(`${k}: ${v.join(', ')}`);
|
|
148
|
+
else
|
|
149
|
+
lines.push(`${k}: ${v}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
@@ -67,18 +67,23 @@ export async function handleIdentityMigrate(ctx, pool, args) {
|
|
|
67
67
|
}
|
|
68
68
|
// Execute migration: re-sign migratable events
|
|
69
69
|
await handleIdentityRestore(ctx, pool, backup);
|
|
70
|
-
// Publish linkage proof connecting old ā new
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
// Publish linkage proof connecting old ā new (only if operating as a derived identity)
|
|
71
|
+
try {
|
|
72
|
+
const proof = ctx.prove('full');
|
|
73
|
+
const sign = ctx.getSigningFunction();
|
|
74
|
+
const proofEvent = await sign({
|
|
75
|
+
kind: 30078,
|
|
76
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
77
|
+
tags: [
|
|
78
|
+
['d', `migration:${args.oldPubkeyHex}`],
|
|
79
|
+
['p', args.oldPubkeyHex],
|
|
80
|
+
],
|
|
81
|
+
content: JSON.stringify(proof),
|
|
82
|
+
});
|
|
83
|
+
await pool.publish(args.oldNpub, proofEvent);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Master identity can't produce tree proofs ā migration proceeds without linkage proof
|
|
87
|
+
}
|
|
83
88
|
return { status: 'migrated', summary };
|
|
84
89
|
}
|
package/package.json
CHANGED