@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.
- package/CHANGELOG.md +51 -0
- package/README.md +136 -0
- package/index.ts +69 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +31 -0
- package/src/channel.test.ts +141 -0
- package/src/channel.ts +342 -0
- package/src/config-schema.ts +90 -0
- package/src/metrics.ts +464 -0
- package/src/nostr-bus.fuzz.test.ts +544 -0
- package/src/nostr-bus.integration.test.ts +452 -0
- package/src/nostr-bus.test.ts +199 -0
- package/src/nostr-bus.ts +741 -0
- package/src/nostr-profile-http.test.ts +378 -0
- package/src/nostr-profile-http.ts +500 -0
- package/src/nostr-profile-import.test.ts +120 -0
- package/src/nostr-profile-import.ts +259 -0
- package/src/nostr-profile.fuzz.test.ts +479 -0
- package/src/nostr-profile.test.ts +410 -0
- package/src/nostr-profile.ts +242 -0
- package/src/nostr-state-store.test.ts +129 -0
- package/src/nostr-state-store.ts +226 -0
- package/src/runtime.ts +14 -0
- package/src/seen-tracker.ts +271 -0
- package/src/types.test.ts +161 -0
- package/src/types.ts +99 -0
- package/test/setup.ts +5 -0
|
@@ -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
|
+
}
|