@openclaw/nostr 2026.1.29

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.
@@ -0,0 +1,161 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ listNostrAccountIds,
4
+ resolveDefaultNostrAccountId,
5
+ resolveNostrAccount,
6
+ } from "./types.js";
7
+
8
+ const TEST_PRIVATE_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
9
+
10
+ describe("listNostrAccountIds", () => {
11
+ it("returns empty array when not configured", () => {
12
+ const cfg = { channels: {} };
13
+ expect(listNostrAccountIds(cfg)).toEqual([]);
14
+ });
15
+
16
+ it("returns empty array when nostr section exists but no privateKey", () => {
17
+ const cfg = { channels: { nostr: { enabled: true } } };
18
+ expect(listNostrAccountIds(cfg)).toEqual([]);
19
+ });
20
+
21
+ it("returns default when privateKey is configured", () => {
22
+ const cfg = {
23
+ channels: {
24
+ nostr: { privateKey: TEST_PRIVATE_KEY },
25
+ },
26
+ };
27
+ expect(listNostrAccountIds(cfg)).toEqual(["default"]);
28
+ });
29
+ });
30
+
31
+ describe("resolveDefaultNostrAccountId", () => {
32
+ it("returns default when configured", () => {
33
+ const cfg = {
34
+ channels: {
35
+ nostr: { privateKey: TEST_PRIVATE_KEY },
36
+ },
37
+ };
38
+ expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
39
+ });
40
+
41
+ it("returns default when not configured", () => {
42
+ const cfg = { channels: {} };
43
+ expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
44
+ });
45
+ });
46
+
47
+ describe("resolveNostrAccount", () => {
48
+ it("resolves configured account", () => {
49
+ const cfg = {
50
+ channels: {
51
+ nostr: {
52
+ privateKey: TEST_PRIVATE_KEY,
53
+ name: "Test Bot",
54
+ relays: ["wss://test.relay"],
55
+ dmPolicy: "pairing" as const,
56
+ },
57
+ },
58
+ };
59
+ const account = resolveNostrAccount({ cfg });
60
+
61
+ expect(account.accountId).toBe("default");
62
+ expect(account.name).toBe("Test Bot");
63
+ expect(account.enabled).toBe(true);
64
+ expect(account.configured).toBe(true);
65
+ expect(account.privateKey).toBe(TEST_PRIVATE_KEY);
66
+ expect(account.publicKey).toMatch(/^[0-9a-f]{64}$/);
67
+ expect(account.relays).toEqual(["wss://test.relay"]);
68
+ });
69
+
70
+ it("resolves unconfigured account with defaults", () => {
71
+ const cfg = { channels: {} };
72
+ const account = resolveNostrAccount({ cfg });
73
+
74
+ expect(account.accountId).toBe("default");
75
+ expect(account.enabled).toBe(true);
76
+ expect(account.configured).toBe(false);
77
+ expect(account.privateKey).toBe("");
78
+ expect(account.publicKey).toBe("");
79
+ expect(account.relays).toContain("wss://relay.damus.io");
80
+ expect(account.relays).toContain("wss://nos.lol");
81
+ });
82
+
83
+ it("handles disabled channel", () => {
84
+ const cfg = {
85
+ channels: {
86
+ nostr: {
87
+ enabled: false,
88
+ privateKey: TEST_PRIVATE_KEY,
89
+ },
90
+ },
91
+ };
92
+ const account = resolveNostrAccount({ cfg });
93
+
94
+ expect(account.enabled).toBe(false);
95
+ expect(account.configured).toBe(true);
96
+ });
97
+
98
+ it("handles custom accountId parameter", () => {
99
+ const cfg = {
100
+ channels: {
101
+ nostr: { privateKey: TEST_PRIVATE_KEY },
102
+ },
103
+ };
104
+ const account = resolveNostrAccount({ cfg, accountId: "custom" });
105
+
106
+ expect(account.accountId).toBe("custom");
107
+ });
108
+
109
+ it("handles allowFrom config", () => {
110
+ const cfg = {
111
+ channels: {
112
+ nostr: {
113
+ privateKey: TEST_PRIVATE_KEY,
114
+ allowFrom: ["npub1test", "0123456789abcdef"],
115
+ },
116
+ },
117
+ };
118
+ const account = resolveNostrAccount({ cfg });
119
+
120
+ expect(account.config.allowFrom).toEqual(["npub1test", "0123456789abcdef"]);
121
+ });
122
+
123
+ it("handles invalid private key gracefully", () => {
124
+ const cfg = {
125
+ channels: {
126
+ nostr: {
127
+ privateKey: "invalid-key",
128
+ },
129
+ },
130
+ };
131
+ const account = resolveNostrAccount({ cfg });
132
+
133
+ expect(account.configured).toBe(true); // key is present
134
+ expect(account.publicKey).toBe(""); // but can't derive pubkey
135
+ });
136
+
137
+ it("preserves all config options", () => {
138
+ const cfg = {
139
+ channels: {
140
+ nostr: {
141
+ privateKey: TEST_PRIVATE_KEY,
142
+ name: "Bot",
143
+ enabled: true,
144
+ relays: ["wss://relay1", "wss://relay2"],
145
+ dmPolicy: "allowlist" as const,
146
+ allowFrom: ["pubkey1", "pubkey2"],
147
+ },
148
+ },
149
+ };
150
+ const account = resolveNostrAccount({ cfg });
151
+
152
+ expect(account.config).toEqual({
153
+ privateKey: TEST_PRIVATE_KEY,
154
+ name: "Bot",
155
+ enabled: true,
156
+ relays: ["wss://relay1", "wss://relay2"],
157
+ dmPolicy: "allowlist",
158
+ allowFrom: ["pubkey1", "pubkey2"],
159
+ });
160
+ });
161
+ });
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { getPublicKeyFromPrivate } from "./nostr-bus.js";
3
+ import { DEFAULT_RELAYS } from "./nostr-bus.js";
4
+ import type { NostrProfile } from "./config-schema.js";
5
+
6
+ export interface NostrAccountConfig {
7
+ enabled?: boolean;
8
+ name?: string;
9
+ privateKey?: string;
10
+ relays?: string[];
11
+ dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
12
+ allowFrom?: Array<string | number>;
13
+ profile?: NostrProfile;
14
+ }
15
+
16
+ export interface ResolvedNostrAccount {
17
+ accountId: string;
18
+ name?: string;
19
+ enabled: boolean;
20
+ configured: boolean;
21
+ privateKey: string;
22
+ publicKey: string;
23
+ relays: string[];
24
+ profile?: NostrProfile;
25
+ config: NostrAccountConfig;
26
+ }
27
+
28
+ const DEFAULT_ACCOUNT_ID = "default";
29
+
30
+ /**
31
+ * List all configured Nostr account IDs
32
+ */
33
+ export function listNostrAccountIds(cfg: OpenClawConfig): string[] {
34
+ const nostrCfg = (cfg.channels as Record<string, unknown> | undefined)?.nostr as
35
+ | NostrAccountConfig
36
+ | undefined;
37
+
38
+ // If privateKey is configured at top level, we have a default account
39
+ if (nostrCfg?.privateKey) {
40
+ return [DEFAULT_ACCOUNT_ID];
41
+ }
42
+
43
+ return [];
44
+ }
45
+
46
+ /**
47
+ * Get the default account ID
48
+ */
49
+ export function resolveDefaultNostrAccountId(cfg: OpenClawConfig): string {
50
+ const ids = listNostrAccountIds(cfg);
51
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
52
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
53
+ }
54
+
55
+ /**
56
+ * Resolve a Nostr account from config
57
+ */
58
+ export function resolveNostrAccount(opts: {
59
+ cfg: OpenClawConfig;
60
+ accountId?: string | null;
61
+ }): ResolvedNostrAccount {
62
+ const accountId = opts.accountId ?? DEFAULT_ACCOUNT_ID;
63
+ const nostrCfg = (opts.cfg.channels as Record<string, unknown> | undefined)?.nostr as
64
+ | NostrAccountConfig
65
+ | undefined;
66
+
67
+ const baseEnabled = nostrCfg?.enabled !== false;
68
+ const privateKey = nostrCfg?.privateKey ?? "";
69
+ const configured = Boolean(privateKey.trim());
70
+
71
+ let publicKey = "";
72
+ if (configured) {
73
+ try {
74
+ publicKey = getPublicKeyFromPrivate(privateKey);
75
+ } catch {
76
+ // Invalid key - leave publicKey empty, configured will indicate issues
77
+ }
78
+ }
79
+
80
+ return {
81
+ accountId,
82
+ name: nostrCfg?.name?.trim() || undefined,
83
+ enabled: baseEnabled,
84
+ configured,
85
+ privateKey,
86
+ publicKey,
87
+ relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
88
+ profile: nostrCfg?.profile,
89
+ config: {
90
+ enabled: nostrCfg?.enabled,
91
+ name: nostrCfg?.name,
92
+ privateKey: nostrCfg?.privateKey,
93
+ relays: nostrCfg?.relays,
94
+ dmPolicy: nostrCfg?.dmPolicy,
95
+ allowFrom: nostrCfg?.allowFrom,
96
+ profile: nostrCfg?.profile,
97
+ },
98
+ };
99
+ }
package/test/setup.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Test setup file for nostr extension
2
+ import { vi } from "vitest";
3
+
4
+ // Mock console.error to suppress noise in tests
5
+ vi.spyOn(console, "error").mockImplementation(() => {});