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.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +229 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +87 -0
- package/dist/context.d.ts +35 -0
- package/dist/context.js +184 -0
- package/dist/identity/handlers.d.ts +31 -0
- package/dist/identity/handlers.js +32 -0
- package/dist/identity/migration.d.ts +37 -0
- package/dist/identity/migration.js +84 -0
- package/dist/identity/shamir.d.ts +18 -0
- package/dist/identity/shamir.js +46 -0
- package/dist/identity/tools.d.ts +11 -0
- package/dist/identity/tools.js +168 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +125 -0
- package/dist/nip65.d.ts +15 -0
- package/dist/nip65.js +71 -0
- package/dist/relay/handlers.d.ts +36 -0
- package/dist/relay/handlers.js +96 -0
- package/dist/relay/tools.d.ts +3 -0
- package/dist/relay/tools.js +60 -0
- package/dist/relay-pool.d.ts +42 -0
- package/dist/relay-pool.js +150 -0
- package/dist/safety/handlers.d.ts +15 -0
- package/dist/safety/handlers.js +12 -0
- package/dist/safety/tools.d.ts +3 -0
- package/dist/safety/tools.js +29 -0
- package/dist/social/dm.d.ts +28 -0
- package/dist/social/dm.js +90 -0
- package/dist/social/handlers.d.ts +42 -0
- package/dist/social/handlers.js +107 -0
- package/dist/social/notifications.d.ts +32 -0
- package/dist/social/notifications.js +74 -0
- package/dist/social/tools.d.ts +3 -0
- package/dist/social/tools.js +168 -0
- package/dist/trust/handlers.d.ts +65 -0
- package/dist/trust/handlers.js +133 -0
- package/dist/trust/ring.d.ts +19 -0
- package/dist/trust/ring.js +54 -0
- package/dist/trust/spoken.d.ts +33 -0
- package/dist/trust/spoken.js +14 -0
- package/dist/trust/tools.d.ts +3 -0
- package/dist/trust/tools.js +182 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +5 -0
- package/dist/zap/handlers.d.ts +80 -0
- package/dist/zap/handlers.js +165 -0
- package/dist/zap/tools.d.ts +3 -0
- package/dist/zap/tools.js +98 -0
- package/llms-full.txt +281 -0
- package/llms.txt +73 -0
- package/package.json +96 -0
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
import type { PublishResult } from '../types.js';
|
|
5
|
+
export interface DmSendResult {
|
|
6
|
+
event: NostrEvent;
|
|
7
|
+
protocol: 'nip17' | 'nip04-deprecated';
|
|
8
|
+
publish: PublishResult;
|
|
9
|
+
}
|
|
10
|
+
export interface DmReadEntry {
|
|
11
|
+
id: string;
|
|
12
|
+
from: string;
|
|
13
|
+
content?: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
protocol: 'nip17' | 'nip04-deprecated';
|
|
16
|
+
decrypted: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Send a DM. Uses NIP-17 gift wrap by default. NIP-04 only if explicitly requested and enabled. */
|
|
20
|
+
export declare function handleDmSend(ctx: IdentityContext, pool: RelayPool, args: {
|
|
21
|
+
recipientPubkeyHex: string;
|
|
22
|
+
message: string;
|
|
23
|
+
nip04?: boolean;
|
|
24
|
+
nip04Enabled?: boolean;
|
|
25
|
+
recipientRelay?: string;
|
|
26
|
+
}): Promise<DmSendResult>;
|
|
27
|
+
/** Read DMs addressed to the active identity */
|
|
28
|
+
export declare function handleDmRead(ctx: IdentityContext, pool: RelayPool): Promise<DmReadEntry[]>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { wrapEvent } from 'nostr-tools/nip17';
|
|
2
|
+
import * as nip04 from 'nostr-tools/nip04';
|
|
3
|
+
import { decode } from 'nostr-tools/nip19';
|
|
4
|
+
/** Send a DM. Uses NIP-17 gift wrap by default. NIP-04 only if explicitly requested and enabled. */
|
|
5
|
+
export async function handleDmSend(ctx, pool, args) {
|
|
6
|
+
if (args.nip04) {
|
|
7
|
+
if (!args.nip04Enabled) {
|
|
8
|
+
throw new Error('NIP-04 is not enabled. Set NIP04_ENABLED=1 to use legacy DMs.');
|
|
9
|
+
}
|
|
10
|
+
// NIP-04 legacy DM (kind 4)
|
|
11
|
+
const encrypted = await nip04.encrypt(ctx.activePrivateKey, args.recipientPubkeyHex, args.message);
|
|
12
|
+
const sign = ctx.getSigningFunction();
|
|
13
|
+
const event = await sign({
|
|
14
|
+
kind: 4,
|
|
15
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
16
|
+
tags: [['p', args.recipientPubkeyHex]],
|
|
17
|
+
content: encrypted,
|
|
18
|
+
});
|
|
19
|
+
console.error('Warning: NIP-04 DMs are deprecated. Prefer NIP-17 gift-wrapped DMs.');
|
|
20
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
21
|
+
return { event, protocol: 'nip04-deprecated', publish };
|
|
22
|
+
}
|
|
23
|
+
// NIP-17 gift-wrapped DM (default)
|
|
24
|
+
const event = wrapEvent(ctx.activePrivateKey, { publicKey: args.recipientPubkeyHex, relayUrl: args.recipientRelay }, args.message);
|
|
25
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
26
|
+
return { event, protocol: 'nip17', publish };
|
|
27
|
+
}
|
|
28
|
+
/** Read DMs addressed to the active identity */
|
|
29
|
+
export async function handleDmRead(ctx, pool) {
|
|
30
|
+
// Fetch gift wraps (kind 1059) and legacy DMs (kind 4) addressed to us
|
|
31
|
+
const activeHex = decode(ctx.activeNpub).data;
|
|
32
|
+
const events = await pool.query(ctx.activeNpub, {
|
|
33
|
+
kinds: [1059, 4],
|
|
34
|
+
'#p': [activeHex],
|
|
35
|
+
limit: 50,
|
|
36
|
+
});
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const event of events) {
|
|
39
|
+
if (event.kind === 1059) {
|
|
40
|
+
// NIP-17 gift wrap — try to unwrap
|
|
41
|
+
try {
|
|
42
|
+
const { unwrapEvent } = await import('nostr-tools/nip17');
|
|
43
|
+
const unwrapped = unwrapEvent(event, ctx.activePrivateKey);
|
|
44
|
+
results.push({
|
|
45
|
+
id: event.id,
|
|
46
|
+
from: unwrapped.pubkey,
|
|
47
|
+
content: unwrapped.content,
|
|
48
|
+
createdAt: unwrapped.created_at,
|
|
49
|
+
protocol: 'nip17',
|
|
50
|
+
decrypted: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
results.push({
|
|
55
|
+
id: event.id,
|
|
56
|
+
from: event.pubkey,
|
|
57
|
+
createdAt: event.created_at,
|
|
58
|
+
protocol: 'nip17',
|
|
59
|
+
decrypted: false,
|
|
60
|
+
error: 'Could not decrypt NIP-17 gift wrap',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else if (event.kind === 4) {
|
|
65
|
+
// NIP-04 legacy DM
|
|
66
|
+
try {
|
|
67
|
+
const decrypted = await nip04.decrypt(ctx.activePrivateKey, event.pubkey, event.content);
|
|
68
|
+
results.push({
|
|
69
|
+
id: event.id,
|
|
70
|
+
from: event.pubkey,
|
|
71
|
+
content: decrypted,
|
|
72
|
+
createdAt: event.created_at,
|
|
73
|
+
protocol: 'nip04-deprecated',
|
|
74
|
+
decrypted: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
results.push({
|
|
79
|
+
id: event.id,
|
|
80
|
+
from: event.pubkey,
|
|
81
|
+
createdAt: event.created_at,
|
|
82
|
+
protocol: 'nip04-deprecated',
|
|
83
|
+
decrypted: false,
|
|
84
|
+
error: 'Could not decrypt NIP-04 message',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
import type { PublishResult } from '../types.js';
|
|
5
|
+
export interface PostResult {
|
|
6
|
+
event: NostrEvent;
|
|
7
|
+
publish: PublishResult;
|
|
8
|
+
}
|
|
9
|
+
/** Create and publish a kind 1 text note */
|
|
10
|
+
export declare function handleSocialPost(ctx: IdentityContext, pool: RelayPool, args: {
|
|
11
|
+
content: string;
|
|
12
|
+
tags?: string[][];
|
|
13
|
+
}): Promise<PostResult>;
|
|
14
|
+
/** Create and publish a reply (kind 1 with e-tag and p-tag) */
|
|
15
|
+
export declare function handleSocialReply(ctx: IdentityContext, pool: RelayPool, args: {
|
|
16
|
+
content: string;
|
|
17
|
+
replyTo: string;
|
|
18
|
+
replyToPubkey: string;
|
|
19
|
+
relay?: string;
|
|
20
|
+
}): Promise<PostResult>;
|
|
21
|
+
/** Create and publish a reaction (kind 7) */
|
|
22
|
+
export declare function handleSocialReact(ctx: IdentityContext, pool: RelayPool, args: {
|
|
23
|
+
eventId: string;
|
|
24
|
+
eventPubkey: string;
|
|
25
|
+
reaction?: string;
|
|
26
|
+
}): Promise<PostResult>;
|
|
27
|
+
/** Fetch and parse the kind 0 profile for a pubkey */
|
|
28
|
+
export declare function handleSocialProfileGet(pool: RelayPool, npub: string, pubkeyHex: string): Promise<Record<string, unknown>>;
|
|
29
|
+
export interface ProfileSetResult {
|
|
30
|
+
published: boolean;
|
|
31
|
+
event: NostrEvent;
|
|
32
|
+
warning?: string;
|
|
33
|
+
diff?: Record<string, {
|
|
34
|
+
old: unknown;
|
|
35
|
+
new: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Set the kind 0 profile for the active identity, with overwrite safety guard */
|
|
39
|
+
export declare function handleSocialProfileSet(ctx: IdentityContext, pool: RelayPool, args: {
|
|
40
|
+
profile: Record<string, unknown>;
|
|
41
|
+
confirm?: boolean;
|
|
42
|
+
}): Promise<ProfileSetResult>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Create and publish a kind 1 text note */
|
|
2
|
+
export async function handleSocialPost(ctx, pool, args) {
|
|
3
|
+
const sign = ctx.getSigningFunction();
|
|
4
|
+
const event = await sign({
|
|
5
|
+
kind: 1,
|
|
6
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
7
|
+
tags: args.tags ?? [],
|
|
8
|
+
content: args.content,
|
|
9
|
+
});
|
|
10
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
11
|
+
return { event, publish };
|
|
12
|
+
}
|
|
13
|
+
/** Create and publish a reply (kind 1 with e-tag and p-tag) */
|
|
14
|
+
export async function handleSocialReply(ctx, pool, args) {
|
|
15
|
+
const tags = [
|
|
16
|
+
['e', args.replyTo, args.relay ?? '', 'reply'],
|
|
17
|
+
['p', args.replyToPubkey],
|
|
18
|
+
];
|
|
19
|
+
const sign = ctx.getSigningFunction();
|
|
20
|
+
const event = await sign({
|
|
21
|
+
kind: 1,
|
|
22
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
23
|
+
tags,
|
|
24
|
+
content: args.content,
|
|
25
|
+
});
|
|
26
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
27
|
+
return { event, publish };
|
|
28
|
+
}
|
|
29
|
+
/** Create and publish a reaction (kind 7) */
|
|
30
|
+
export async function handleSocialReact(ctx, pool, args) {
|
|
31
|
+
const sign = ctx.getSigningFunction();
|
|
32
|
+
const event = await sign({
|
|
33
|
+
kind: 7,
|
|
34
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
35
|
+
tags: [
|
|
36
|
+
['e', args.eventId],
|
|
37
|
+
['p', args.eventPubkey],
|
|
38
|
+
],
|
|
39
|
+
content: args.reaction ?? '+',
|
|
40
|
+
});
|
|
41
|
+
const publish = await pool.publish(ctx.activeNpub, event);
|
|
42
|
+
return { event, publish };
|
|
43
|
+
}
|
|
44
|
+
/** Fetch and parse the kind 0 profile for a pubkey */
|
|
45
|
+
export async function handleSocialProfileGet(pool, npub, pubkeyHex) {
|
|
46
|
+
const events = await pool.query(npub, {
|
|
47
|
+
kinds: [0],
|
|
48
|
+
authors: [pubkeyHex],
|
|
49
|
+
});
|
|
50
|
+
if (events.length === 0)
|
|
51
|
+
return {};
|
|
52
|
+
// Take highest created_at
|
|
53
|
+
const best = events.reduce((a, b) => b.created_at > a.created_at ? b : a);
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(best.content);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Set the kind 0 profile for the active identity, with overwrite safety guard */
|
|
62
|
+
export async function handleSocialProfileSet(ctx, pool, args) {
|
|
63
|
+
// Check for existing profile
|
|
64
|
+
const existing = await pool.query(ctx.activeNpub, {
|
|
65
|
+
kinds: [0],
|
|
66
|
+
authors: [ctx.activeNpub], // simplified — in production, use hex pubkey
|
|
67
|
+
});
|
|
68
|
+
if (existing.length > 0 && !args.confirm) {
|
|
69
|
+
// Build diff
|
|
70
|
+
let oldProfile = {};
|
|
71
|
+
try {
|
|
72
|
+
const best = existing.reduce((a, b) => b.created_at > a.created_at ? b : a);
|
|
73
|
+
oldProfile = JSON.parse(best.content);
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
const diff = {};
|
|
77
|
+
const allKeys = new Set([...Object.keys(oldProfile), ...Object.keys(args.profile)]);
|
|
78
|
+
for (const key of allKeys) {
|
|
79
|
+
if (oldProfile[key] !== args.profile[key]) {
|
|
80
|
+
diff[key] = { old: oldProfile[key], new: args.profile[key] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const sign = ctx.getSigningFunction();
|
|
84
|
+
const event = await sign({
|
|
85
|
+
kind: 0,
|
|
86
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
87
|
+
tags: [],
|
|
88
|
+
content: JSON.stringify(args.profile),
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
published: false,
|
|
92
|
+
event,
|
|
93
|
+
warning: 'Profile already exists. Set confirm: true to overwrite.',
|
|
94
|
+
diff,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Publish
|
|
98
|
+
const sign = ctx.getSigningFunction();
|
|
99
|
+
const event = await sign({
|
|
100
|
+
kind: 0,
|
|
101
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
102
|
+
tags: [],
|
|
103
|
+
content: JSON.stringify(args.profile),
|
|
104
|
+
});
|
|
105
|
+
await pool.publish(ctx.activeNpub, event);
|
|
106
|
+
return { published: true, event };
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IdentityContext } from '../context.js';
|
|
2
|
+
import type { RelayPool } from '../relay-pool.js';
|
|
3
|
+
export interface Notification {
|
|
4
|
+
id: string;
|
|
5
|
+
type: 'reply' | 'reaction' | 'mention' | 'zap' | 'other';
|
|
6
|
+
kind: number;
|
|
7
|
+
from: string;
|
|
8
|
+
content: string;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
replyToEventId?: string;
|
|
11
|
+
amountMsats?: number;
|
|
12
|
+
zapSender?: string;
|
|
13
|
+
zapMessage?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FeedEntry {
|
|
16
|
+
id: string;
|
|
17
|
+
pubkey: string;
|
|
18
|
+
content: string;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
tags: string[][];
|
|
21
|
+
}
|
|
22
|
+
/** Fetch notifications (mentions, replies, reactions, zaps) for the active identity */
|
|
23
|
+
export declare function handleNotifications(ctx: IdentityContext, pool: RelayPool, opts?: {
|
|
24
|
+
since?: number;
|
|
25
|
+
limit?: number;
|
|
26
|
+
}): Promise<Notification[]>;
|
|
27
|
+
/** Fetch feed (kind 1 events) */
|
|
28
|
+
export declare function handleFeed(ctx: IdentityContext, pool: RelayPool, opts: {
|
|
29
|
+
authors?: string[];
|
|
30
|
+
since?: number;
|
|
31
|
+
limit?: number;
|
|
32
|
+
}): Promise<FeedEntry[]>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Fetch notifications (mentions, replies, reactions, zaps) for the active identity */
|
|
2
|
+
export async function handleNotifications(ctx, pool, opts) {
|
|
3
|
+
const events = await pool.query(ctx.activeNpub, {
|
|
4
|
+
kinds: [1, 7, 9735],
|
|
5
|
+
'#p': [ctx.activeNpub],
|
|
6
|
+
limit: opts?.limit ?? 50,
|
|
7
|
+
...(opts?.since ? { since: opts.since } : {}),
|
|
8
|
+
});
|
|
9
|
+
return events
|
|
10
|
+
.filter(e => e.kind !== 3) // exclude follows
|
|
11
|
+
.map(event => classifyNotification(event, ctx.activeNpub))
|
|
12
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
13
|
+
}
|
|
14
|
+
/** Classify a single event into a notification type */
|
|
15
|
+
function classifyNotification(event, activeNpub) {
|
|
16
|
+
const base = {
|
|
17
|
+
id: event.id,
|
|
18
|
+
kind: event.kind,
|
|
19
|
+
from: event.pubkey,
|
|
20
|
+
content: event.content,
|
|
21
|
+
createdAt: event.created_at,
|
|
22
|
+
};
|
|
23
|
+
if (event.kind === 7) {
|
|
24
|
+
return { ...base, type: 'reaction' };
|
|
25
|
+
}
|
|
26
|
+
if (event.kind === 9735) {
|
|
27
|
+
return parseZapReceipt(event, base);
|
|
28
|
+
}
|
|
29
|
+
if (event.kind === 1) {
|
|
30
|
+
// Check if it's a reply (has e-tag with 'reply' marker) or just a mention
|
|
31
|
+
const eTag = event.tags.find(t => t[0] === 'e');
|
|
32
|
+
if (eTag) {
|
|
33
|
+
return { ...base, type: 'reply', replyToEventId: eTag[1] };
|
|
34
|
+
}
|
|
35
|
+
return { ...base, type: 'mention' };
|
|
36
|
+
}
|
|
37
|
+
return { ...base, type: 'other' };
|
|
38
|
+
}
|
|
39
|
+
/** Parse zap receipt (kind 9735) for amount and sender */
|
|
40
|
+
function parseZapReceipt(event, base) {
|
|
41
|
+
let amountMsats;
|
|
42
|
+
let zapSender;
|
|
43
|
+
let zapMessage;
|
|
44
|
+
const descTag = event.tags.find(t => t[0] === 'description');
|
|
45
|
+
if (descTag?.[1]) {
|
|
46
|
+
try {
|
|
47
|
+
const zapRequest = JSON.parse(descTag[1]);
|
|
48
|
+
zapSender = zapRequest.pubkey;
|
|
49
|
+
zapMessage = zapRequest.content;
|
|
50
|
+
const amountTag = zapRequest.tags?.find((t) => t[0] === 'amount');
|
|
51
|
+
if (amountTag?.[1]) {
|
|
52
|
+
amountMsats = parseInt(amountTag[1], 10);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* ignore malformed zap request */ }
|
|
56
|
+
}
|
|
57
|
+
return { ...base, type: 'zap', amountMsats, zapSender, zapMessage };
|
|
58
|
+
}
|
|
59
|
+
/** Fetch feed (kind 1 events) */
|
|
60
|
+
export async function handleFeed(ctx, pool, opts) {
|
|
61
|
+
const events = await pool.query(ctx.activeNpub, {
|
|
62
|
+
kinds: [1],
|
|
63
|
+
...(opts.authors ? { authors: opts.authors } : {}),
|
|
64
|
+
...(opts.since ? { since: opts.since } : {}),
|
|
65
|
+
limit: opts.limit ?? 20,
|
|
66
|
+
});
|
|
67
|
+
return events.map(e => ({
|
|
68
|
+
id: e.id,
|
|
69
|
+
pubkey: e.pubkey,
|
|
70
|
+
content: e.content,
|
|
71
|
+
createdAt: e.created_at,
|
|
72
|
+
tags: e.tags,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { hexId } from '../validation.js';
|
|
3
|
+
import { handleSocialPost, handleSocialReply, handleSocialReact, handleSocialProfileGet, handleSocialProfileSet, } from './handlers.js';
|
|
4
|
+
import { handleDmSend, handleDmRead } from './dm.js';
|
|
5
|
+
import { handleNotifications, handleFeed } from './notifications.js';
|
|
6
|
+
export function registerSocialTools(server, deps) {
|
|
7
|
+
server.registerTool('social_post', {
|
|
8
|
+
description: 'Post a text note (kind 1) as the active identity. Returns the signed event.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
content: z.string().describe('Text content of the note'),
|
|
11
|
+
},
|
|
12
|
+
annotations: { readOnlyHint: false },
|
|
13
|
+
}, async ({ content }) => {
|
|
14
|
+
const result = await handleSocialPost(deps.ctx, deps.pool, { content });
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
17
|
+
id: result.event.id,
|
|
18
|
+
pubkey: result.event.pubkey,
|
|
19
|
+
publish: result.publish,
|
|
20
|
+
}, null, 2) }],
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
server.registerTool('social_reply', {
|
|
24
|
+
description: 'Reply to a Nostr event (kind 1 with e-tag and p-tag) as the active identity.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
content: z.string().describe('Reply text'),
|
|
27
|
+
replyTo: hexId.describe('Event ID being replied to (hex)'),
|
|
28
|
+
replyToPubkey: hexId.describe('Pubkey of the event author (hex)'),
|
|
29
|
+
relay: z.string().optional().describe('Relay URL where the parent event was seen'),
|
|
30
|
+
},
|
|
31
|
+
annotations: { readOnlyHint: false },
|
|
32
|
+
}, async ({ content, replyTo, replyToPubkey, relay }) => {
|
|
33
|
+
const result = await handleSocialReply(deps.ctx, deps.pool, { content, replyTo, replyToPubkey, relay });
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
36
|
+
id: result.event.id,
|
|
37
|
+
publish: result.publish,
|
|
38
|
+
}, null, 2) }],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
server.registerTool('social_react', {
|
|
42
|
+
description: 'React to a Nostr event (kind 7) as the active identity. Default reaction is "+".',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
eventId: hexId.describe('Event ID to react to (hex)'),
|
|
45
|
+
eventPubkey: hexId.describe('Pubkey of the event author (hex)'),
|
|
46
|
+
reaction: z.string().default('+').describe('Reaction content (default "+", or emoji)'),
|
|
47
|
+
},
|
|
48
|
+
annotations: { readOnlyHint: false },
|
|
49
|
+
}, async ({ eventId, eventPubkey, reaction }) => {
|
|
50
|
+
const result = await handleSocialReact(deps.ctx, deps.pool, { eventId, eventPubkey, reaction });
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
53
|
+
id: result.event.id,
|
|
54
|
+
publish: result.publish,
|
|
55
|
+
}, null, 2) }],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
server.registerTool('social_profile_get', {
|
|
59
|
+
description: 'Fetch the kind 0 profile for a Nostr pubkey. Returns parsed profile fields.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
pubkeyHex: hexId.describe('Hex pubkey to fetch profile for'),
|
|
62
|
+
npub: z.string().optional().describe('Bech32 npub (used for relay routing, defaults to active identity)'),
|
|
63
|
+
},
|
|
64
|
+
annotations: { readOnlyHint: true },
|
|
65
|
+
}, async ({ pubkeyHex, npub }) => {
|
|
66
|
+
const resolvedNpub = npub ?? deps.ctx.activeNpub;
|
|
67
|
+
const profile = await handleSocialProfileGet(deps.pool, resolvedNpub, pubkeyHex);
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }],
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
server.registerTool('social_profile_set', {
|
|
73
|
+
description: 'Set the kind 0 profile for the active identity. Warns if profile already exists — set confirm: true to overwrite.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
name: z.string().optional().describe('Display name'),
|
|
76
|
+
about: z.string().optional().describe('About / bio text'),
|
|
77
|
+
picture: z.string().optional().describe('Profile picture URL'),
|
|
78
|
+
nip05: z.string().optional().describe('NIP-05 identifier'),
|
|
79
|
+
banner: z.string().optional().describe('Banner image URL'),
|
|
80
|
+
lud16: z.string().optional().describe('Lightning address'),
|
|
81
|
+
confirm: z.boolean().default(false).describe('Set true to overwrite existing profile'),
|
|
82
|
+
},
|
|
83
|
+
annotations: { readOnlyHint: false },
|
|
84
|
+
}, async ({ confirm, ...fields }) => {
|
|
85
|
+
// Filter out undefined fields
|
|
86
|
+
const profile = {};
|
|
87
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
88
|
+
if (v !== undefined)
|
|
89
|
+
profile[k] = v;
|
|
90
|
+
}
|
|
91
|
+
const result = await handleSocialProfileSet(deps.ctx, deps.pool, { profile, confirm });
|
|
92
|
+
if (!result.published && result.warning) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
95
|
+
warning: result.warning,
|
|
96
|
+
diff: result.diff,
|
|
97
|
+
}, null, 2) }],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
102
|
+
published: true,
|
|
103
|
+
id: result.event.id,
|
|
104
|
+
}, null, 2) }],
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
server.registerTool('dm_send', {
|
|
108
|
+
description: 'Send a direct message to a Nostr pubkey. Uses NIP-17 gift wrap by default. Set nip04: true for legacy NIP-04 (requires NIP04_ENABLED).',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
recipientPubkeyHex: hexId.describe('Recipient hex pubkey'),
|
|
111
|
+
message: z.string().describe('Message text'),
|
|
112
|
+
nip04: z.boolean().default(false).describe('Use legacy NIP-04 instead of NIP-17'),
|
|
113
|
+
recipientRelay: z.string().optional().describe('Relay URL hint for recipient'),
|
|
114
|
+
},
|
|
115
|
+
annotations: { readOnlyHint: false },
|
|
116
|
+
}, async ({ recipientPubkeyHex, message, nip04, recipientRelay }) => {
|
|
117
|
+
const result = await handleDmSend(deps.ctx, deps.pool, {
|
|
118
|
+
recipientPubkeyHex,
|
|
119
|
+
message,
|
|
120
|
+
nip04,
|
|
121
|
+
nip04Enabled: false, // TODO: wire from config
|
|
122
|
+
recipientRelay,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
126
|
+
protocol: result.protocol,
|
|
127
|
+
id: result.event.id,
|
|
128
|
+
publish: result.publish,
|
|
129
|
+
}, null, 2) }],
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
server.registerTool('dm_read', {
|
|
133
|
+
description: 'Read direct messages addressed to the active identity. Decrypts NIP-17 and NIP-04 messages.',
|
|
134
|
+
annotations: { readOnlyHint: true },
|
|
135
|
+
}, async () => {
|
|
136
|
+
const messages = await handleDmRead(deps.ctx, deps.pool);
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: 'text', text: JSON.stringify(messages, null, 2) }],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
server.registerTool('social_notifications', {
|
|
142
|
+
description: 'Fetch notifications for the active identity — mentions, replies, reactions, and zap receipts.',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
since: z.number().optional().describe('Unix timestamp — only fetch notifications after this time'),
|
|
145
|
+
limit: z.number().int().min(1).max(200).default(50).describe('Max notifications to return'),
|
|
146
|
+
},
|
|
147
|
+
annotations: { readOnlyHint: true },
|
|
148
|
+
}, async ({ since, limit }) => {
|
|
149
|
+
const notifications = await handleNotifications(deps.ctx, deps.pool, { since, limit });
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: 'text', text: JSON.stringify(notifications, null, 2) }],
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
server.registerTool('social_feed', {
|
|
155
|
+
description: 'Fetch the kind 1 text note feed. Optionally filter by authors.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
authors: z.array(hexId).optional().describe('Hex pubkeys to filter by'),
|
|
158
|
+
since: z.number().optional().describe('Unix timestamp — only fetch posts after this time'),
|
|
159
|
+
limit: z.number().int().min(1).max(100).default(20).describe('Max posts to return'),
|
|
160
|
+
},
|
|
161
|
+
annotations: { readOnlyHint: true },
|
|
162
|
+
}, async ({ authors, since, limit }) => {
|
|
163
|
+
const feed = await handleFeed(deps.ctx, deps.pool, { authors, since, limit });
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: 'text', text: JSON.stringify(feed, null, 2) }],
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
import type { PublishResult } from '../types.js';
|
|
5
|
+
export interface AttestResult {
|
|
6
|
+
event: NostrEvent;
|
|
7
|
+
publish: PublishResult;
|
|
8
|
+
warning?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Create and publish a kind 31000 attestation */
|
|
11
|
+
export declare function handleTrustAttest(ctx: IdentityContext, pool: RelayPool, args: {
|
|
12
|
+
type: string;
|
|
13
|
+
identifier?: string;
|
|
14
|
+
subject?: string;
|
|
15
|
+
summary?: string;
|
|
16
|
+
content?: string;
|
|
17
|
+
expiration?: number;
|
|
18
|
+
}): Promise<AttestResult>;
|
|
19
|
+
/** Read attestations from relays, filtered by subject/type/attestor */
|
|
20
|
+
export declare function handleTrustRead(pool: RelayPool, npub: string, args: {
|
|
21
|
+
subject?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
attestor?: string;
|
|
24
|
+
}): Promise<NostrEvent[]>;
|
|
25
|
+
/** Validate attestation event structure */
|
|
26
|
+
export declare function handleTrustVerify(event: NostrEvent): {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
errors: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Create and publish a revocation for an attestation */
|
|
31
|
+
export declare function handleTrustRevoke(ctx: IdentityContext, pool: RelayPool, args: {
|
|
32
|
+
type: string;
|
|
33
|
+
identifier: string;
|
|
34
|
+
originalAttestorPubkey?: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
event: NostrEvent;
|
|
37
|
+
publish: PublishResult;
|
|
38
|
+
}>;
|
|
39
|
+
/** Send an attestation request as a NIP-17 structured DM */
|
|
40
|
+
export declare function handleTrustRequest(ctx: IdentityContext, pool: RelayPool, args: {
|
|
41
|
+
recipientPubkeyHex: string;
|
|
42
|
+
subject: string;
|
|
43
|
+
attestationType: string;
|
|
44
|
+
message?: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
event: NostrEvent;
|
|
47
|
+
publish: PublishResult;
|
|
48
|
+
}>;
|
|
49
|
+
/** Scan NIP-17 DMs for attestation request payloads */
|
|
50
|
+
export declare function handleTrustRequestList(ctx: IdentityContext, pool: RelayPool): Promise<Array<{
|
|
51
|
+
from: string;
|
|
52
|
+
subject: string;
|
|
53
|
+
attestationType: string;
|
|
54
|
+
message?: string;
|
|
55
|
+
}>>;
|
|
56
|
+
/** Publish a linkage proof as a kind 30078 event. Requires confirmation. */
|
|
57
|
+
export declare function handleTrustProofPublish(ctx: IdentityContext, pool: RelayPool, args: {
|
|
58
|
+
mode?: 'blind' | 'full';
|
|
59
|
+
confirm: boolean;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
event?: NostrEvent;
|
|
62
|
+
published: boolean;
|
|
63
|
+
warning?: string;
|
|
64
|
+
publish?: PublishResult;
|
|
65
|
+
}>;
|