openclaw/extensions/nostr/index.ts
Patrick Ulrich 0abcc80b2a feat(nostr): add NIP-46 bunker authentication for user signing
Add support for users to connect their own Nostr identities via NIP-46
remote signers (bunkers) so agents can post, react, and repost on their
behalf.

New features:
- Multi-account bunker connection management (bunker-store.ts)
- Event posting: notes (NIP-10), reactions (NIP-25), reposts (NIP-18),
  long-form articles (NIP-23)
- 8 agent tools: nostr_connect, nostr_post, nostr_react, nostr_repost,
  nostr_fetch, nostr_article, nostr_disconnect, nostr_status
- HTTP endpoints for bunker management via web UI
- bunkerAccounts array in config schema for multi-bunker support
- Shared pool utility for bunker tools

Excludes NIP-09 deletion for safety (no agent deletion capability).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:00:05 -05:00

153 lines
5.1 KiB
TypeScript

import type { ClawdbotPluginApi, ClawdbotConfig } 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: ClawdbotPluginApi) {
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 ClawdbotConfig;
const account = resolveNostrAccount({ cfg, accountId });
return account.profile;
},
updateConfigProfile: async (accountId: string, profile: NostrProfile) => {
const runtime = getNostrRuntime();
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
// 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 ClawdbotConfig;
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;