Two major enhancements to the Nostr extension: ## NIP-17 Private Direct Messages Adds NIP-17 with NIP-59 gift wrapping for privacy-preserving DMs. Protocol comparison: - NIP-04 (legacy): Kind 4, sender/timing visible, AES-CBC - NIP-17 (new): Kind 1059→13→14, sender hidden, timestamps randomized ±48h, XChaCha20-Poly1305 New dmProtocol config option: - "dual" (default): Accept both, send NIP-17 - "nip17": NIP-17 only (max privacy) - "nip04": Legacy only Changes: - Subscribe to kind:4 (NIP-04) and kind:1059 (GiftWrap) events - 48-hour lookback for NIP-17 randomized timestamps - Unwrap gift wraps via nostr-tools/nip59 ## NIP-46 Bunker Authentication Users can connect their Nostr identity via remote signers (Amber, nsec.app). Agent tools: - nostr_connect: Link via bunker URL - nostr_post: Kind 1 notes with NIP-10 threading - nostr_react: Reactions (+/-/emoji) per NIP-25 - nostr_repost: Kind 6/16 reposts per NIP-18 - nostr_fetch: Query by author/hashtag/mentions/NIP-50 search - nostr_article: Kind 30023 long-form content per NIP-23 - nostr_disconnect/nostr_status: Session management Bunker features: - Session persistence (client key saved to disk) - NIP-65 relay discovery - Auth URL flow for browser approval - Multi-account via bunkerIndex HTTP endpoints for web UI: - GET/POST/DELETE /api/channels/nostr/:id/bunker/:idx ## Safety - NIP-09 deletion excluded (no agent deletion capability) - Private keys remain in signer app - Explicit user connection required NIPs: 01, 10, 17, 18, 23, 25, 44, 46, 50, 59, 65 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import type { MoltbotPluginApi, MoltbotConfig } from "clawdbot/plugin-sdk";
|
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
|
|
|
import { nostrPlugin } from "./src/channel.js";
|
|
import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js";
|
|
import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
|
|
import { createNostrBunkerHttpHandler } from "./src/nostr-bunker-http.js";
|
|
import { createNostrAgentTools } from "./src/agent-tools.js";
|
|
import { resolveNostrAccount } from "./src/types.js";
|
|
import type { NostrProfile, BunkerAccountConfig } from "./src/config-schema.js";
|
|
|
|
const plugin = {
|
|
id: "nostr",
|
|
name: "Nostr",
|
|
description: "Nostr DM channel plugin via NIP-04",
|
|
configSchema: emptyPluginConfigSchema(),
|
|
register(api: MoltbotPluginApi) {
|
|
setNostrRuntime(api.runtime);
|
|
api.registerChannel({ plugin: nostrPlugin });
|
|
|
|
// Register HTTP handler for profile management
|
|
const httpHandler = createNostrProfileHttpHandler({
|
|
getConfigProfile: (accountId: string) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as MoltbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
return account.profile;
|
|
},
|
|
updateConfigProfile: async (accountId: string, profile: NostrProfile) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as MoltbotConfig;
|
|
|
|
// Build the config patch for channels.nostr.profile
|
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
|
|
const updatedNostrConfig = {
|
|
...nostrConfig,
|
|
profile,
|
|
};
|
|
|
|
const updatedChannels = {
|
|
...channels,
|
|
nostr: updatedNostrConfig,
|
|
};
|
|
|
|
await runtime.config.writeConfigFile({
|
|
...cfg,
|
|
channels: updatedChannels,
|
|
});
|
|
},
|
|
getAccountInfo: (accountId: string) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as MoltbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
if (!account.configured || !account.publicKey) {
|
|
return null;
|
|
}
|
|
return {
|
|
pubkey: account.publicKey,
|
|
relays: account.relays,
|
|
};
|
|
},
|
|
log: api.logger,
|
|
});
|
|
|
|
api.registerHttpHandler(httpHandler);
|
|
|
|
// Register HTTP handler for bunker management
|
|
const bunkerHttpHandler = createNostrBunkerHttpHandler({
|
|
getBunkerAccounts: (accountId: string) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
return account.bunkerAccounts;
|
|
},
|
|
updateBunkerAccount: async (
|
|
accountId: string,
|
|
bunkerIndex: number,
|
|
update: Partial<BunkerAccountConfig>
|
|
) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
|
|
// Update the specific bunker account
|
|
const bunkerAccounts = [...account.bunkerAccounts];
|
|
while (bunkerAccounts.length <= bunkerIndex) {
|
|
bunkerAccounts.push({ bunkerUrl: "" });
|
|
}
|
|
bunkerAccounts[bunkerIndex] = {
|
|
...bunkerAccounts[bunkerIndex],
|
|
...update,
|
|
};
|
|
|
|
// Write back to config
|
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
await runtime.config.writeConfigFile({
|
|
...cfg,
|
|
channels: {
|
|
...channels,
|
|
nostr: {
|
|
...nostrConfig,
|
|
bunkerAccounts,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
clearConfigBunkerUrl: async (accountId: string, bunkerIndex: number) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
|
|
// Clear the specific bunker account
|
|
const bunkerAccounts = [...account.bunkerAccounts];
|
|
if (bunkerIndex < bunkerAccounts.length) {
|
|
bunkerAccounts[bunkerIndex] = {
|
|
...bunkerAccounts[bunkerIndex],
|
|
bunkerUrl: "",
|
|
userPubkey: undefined,
|
|
connectedAt: undefined,
|
|
};
|
|
}
|
|
|
|
// Write back to config
|
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
await runtime.config.writeConfigFile({
|
|
...cfg,
|
|
channels: {
|
|
...channels,
|
|
nostr: {
|
|
...nostrConfig,
|
|
bunkerAccounts,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
log: api.logger,
|
|
});
|
|
|
|
api.registerHttpHandler(bunkerHttpHandler);
|
|
|
|
// Register agent tools for bunker operations
|
|
for (const tool of createNostrAgentTools()) {
|
|
api.registerTool(tool);
|
|
}
|
|
},
|
|
};
|
|
|
|
export default plugin;
|