@openclaw/nostr 2026.3.13 → 2026.5.2-beta.1

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.
Files changed (48) hide show
  1. package/README.md +6 -0
  2. package/api.ts +10 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +60 -36
  5. package/openclaw.plugin.json +190 -1
  6. package/package.json +41 -9
  7. package/runtime-api.ts +6 -0
  8. package/setup-api.ts +1 -0
  9. package/setup-entry.ts +9 -0
  10. package/setup-plugin-api.ts +3 -0
  11. package/src/channel-api.ts +15 -0
  12. package/src/channel.inbound.test.ts +176 -0
  13. package/src/channel.outbound.test.ts +89 -49
  14. package/src/channel.setup.ts +231 -0
  15. package/src/channel.test.ts +439 -71
  16. package/src/channel.ts +147 -283
  17. package/src/config-schema.ts +18 -12
  18. package/src/default-relays.ts +1 -0
  19. package/src/gateway.ts +302 -0
  20. package/src/inbound-direct-dm-runtime.ts +1 -0
  21. package/src/metrics.ts +6 -6
  22. package/src/nostr-bus.fuzz.test.ts +74 -247
  23. package/src/nostr-bus.inbound.test.ts +526 -0
  24. package/src/nostr-bus.integration.test.ts +88 -64
  25. package/src/nostr-bus.test.ts +22 -31
  26. package/src/nostr-bus.ts +206 -136
  27. package/src/nostr-key-utils.ts +94 -0
  28. package/src/nostr-profile-core.ts +134 -0
  29. package/src/nostr-profile-http-runtime.ts +6 -0
  30. package/src/nostr-profile-http.test.ts +276 -167
  31. package/src/nostr-profile-http.ts +51 -36
  32. package/src/nostr-profile-import.ts +3 -3
  33. package/src/nostr-profile-url-safety.ts +21 -0
  34. package/src/nostr-profile.fuzz.test.ts +7 -57
  35. package/src/nostr-profile.test.ts +16 -14
  36. package/src/nostr-profile.ts +13 -146
  37. package/src/nostr-state-store.test.ts +106 -2
  38. package/src/nostr-state-store.ts +46 -49
  39. package/src/runtime.ts +6 -3
  40. package/src/seen-tracker.ts +1 -1
  41. package/src/session-route.ts +25 -0
  42. package/src/setup-surface.ts +265 -0
  43. package/src/test-fixtures.ts +45 -0
  44. package/src/types.ts +26 -25
  45. package/test-api.ts +1 -0
  46. package/tsconfig.json +16 -0
  47. package/CHANGELOG.md +0 -116
  48. package/src/types.test.ts +0 -175
package/src/types.ts CHANGED
@@ -3,16 +3,22 @@ import {
3
3
  normalizeAccountId,
4
4
  normalizeOptionalAccountId,
5
5
  } from "openclaw/plugin-sdk/account-id";
6
- import type { OpenClawConfig } from "openclaw/plugin-sdk/nostr";
6
+ import {
7
+ listCombinedAccountIds,
8
+ resolveListedDefaultAccountId,
9
+ } from "openclaw/plugin-sdk/account-resolution";
10
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
11
+ import { normalizeSecretInputString, type SecretInput } from "openclaw/plugin-sdk/secret-input";
12
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
7
13
  import type { NostrProfile } from "./config-schema.js";
8
- import { getPublicKeyFromPrivate } from "./nostr-bus.js";
9
- import { DEFAULT_RELAYS } from "./nostr-bus.js";
14
+ import { DEFAULT_RELAYS } from "./default-relays.js";
15
+ import { getPublicKeyFromPrivate } from "./nostr-key-utils.js";
10
16
 
11
- export interface NostrAccountConfig {
17
+ interface NostrAccountConfig {
12
18
  enabled?: boolean;
13
19
  name?: string;
14
20
  defaultAccount?: string;
15
- privateKey?: string;
21
+ privateKey?: SecretInput;
16
22
  relays?: string[];
17
23
  dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
18
24
  allowFrom?: Array<string | number>;
@@ -45,28 +51,23 @@ export function listNostrAccountIds(cfg: OpenClawConfig): string[] {
45
51
  const nostrCfg = (cfg.channels as Record<string, unknown> | undefined)?.nostr as
46
52
  | NostrAccountConfig
47
53
  | undefined;
48
-
49
- // If privateKey is configured at top level, we have a default account
50
- if (nostrCfg?.privateKey) {
51
- return [resolveConfiguredDefaultNostrAccountId(cfg) ?? DEFAULT_ACCOUNT_ID];
52
- }
53
-
54
- return [];
54
+ const privateKey = normalizeSecretInputString(nostrCfg?.privateKey);
55
+ return listCombinedAccountIds({
56
+ configuredAccountIds: [],
57
+ implicitAccountId: privateKey
58
+ ? (resolveConfiguredDefaultNostrAccountId(cfg) ?? DEFAULT_ACCOUNT_ID)
59
+ : undefined,
60
+ });
55
61
  }
56
62
 
57
63
  /**
58
64
  * Get the default account ID
59
65
  */
60
66
  export function resolveDefaultNostrAccountId(cfg: OpenClawConfig): string {
61
- const preferred = resolveConfiguredDefaultNostrAccountId(cfg);
62
- if (preferred) {
63
- return preferred;
64
- }
65
- const ids = listNostrAccountIds(cfg);
66
- if (ids.includes(DEFAULT_ACCOUNT_ID)) {
67
- return DEFAULT_ACCOUNT_ID;
68
- }
69
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
67
+ return resolveListedDefaultAccountId({
68
+ accountIds: listNostrAccountIds(cfg),
69
+ configuredDefaultAccountId: resolveConfiguredDefaultNostrAccountId(cfg),
70
+ });
70
71
  }
71
72
 
72
73
  /**
@@ -82,11 +83,11 @@ export function resolveNostrAccount(opts: {
82
83
  | undefined;
83
84
 
84
85
  const baseEnabled = nostrCfg?.enabled !== false;
85
- const privateKey = nostrCfg?.privateKey ?? "";
86
- const configured = Boolean(privateKey.trim());
86
+ const privateKey = normalizeSecretInputString(nostrCfg?.privateKey) ?? "";
87
+ const configured = Boolean(privateKey);
87
88
 
88
89
  let publicKey = "";
89
- if (configured) {
90
+ if (privateKey) {
90
91
  try {
91
92
  publicKey = getPublicKeyFromPrivate(privateKey);
92
93
  } catch {
@@ -96,7 +97,7 @@ export function resolveNostrAccount(opts: {
96
97
 
97
98
  return {
98
99
  accountId,
99
- name: nostrCfg?.name?.trim() || undefined,
100
+ name: normalizeOptionalString(nostrCfg?.name),
100
101
  enabled: baseEnabled,
101
102
  configured,
102
103
  privateKey,
package/test-api.ts ADDED
@@ -0,0 +1 @@
1
+ export { nostrPlugin } from "./src/channel.js";
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.package-boundary.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./*.ts", "./src/**/*.ts"],
7
+ "exclude": [
8
+ "./**/*.test.ts",
9
+ "./dist/**",
10
+ "./node_modules/**",
11
+ "./src/test-support/**",
12
+ "./src/**/*test-helpers.ts",
13
+ "./src/**/*test-harness.ts",
14
+ "./src/**/*test-support.ts"
15
+ ]
16
+ }
package/CHANGELOG.md DELETED
@@ -1,116 +0,0 @@
1
- # Changelog
2
-
3
- ## 2026.3.13
4
-
5
- ### Changes
6
-
7
- - Version alignment with core OpenClaw release numbers.
8
-
9
- ## 2026.3.12
10
-
11
- ### Changes
12
-
13
- - Version alignment with core OpenClaw release numbers.
14
-
15
- ## 2026.3.11
16
-
17
- ### Changes
18
-
19
- - Version alignment with core OpenClaw release numbers.
20
-
21
- ## 2026.3.10
22
-
23
- ### Changes
24
-
25
- - Version alignment with core OpenClaw release numbers.
26
-
27
- ## 2026.3.9
28
-
29
- ### Changes
30
-
31
- - Version alignment with core OpenClaw release numbers.
32
-
33
- ## 2026.3.8-beta.1
34
-
35
- ### Changes
36
-
37
- - Version alignment with core OpenClaw release numbers.
38
-
39
- ## 2026.3.8
40
-
41
- ### Changes
42
-
43
- - Version alignment with core OpenClaw release numbers.
44
-
45
- ## 2026.3.7
46
-
47
- ### Changes
48
-
49
- - Version alignment with core OpenClaw release numbers.
50
-
51
- ## 2026.3.3
52
-
53
- ### Changes
54
-
55
- - Version alignment with core OpenClaw release numbers.
56
-
57
- ## 2026.3.2
58
-
59
- ### Changes
60
-
61
- - Version alignment with core OpenClaw release numbers.
62
-
63
- ## 2026.3.1
64
-
65
- ### Changes
66
-
67
- - Version alignment with core OpenClaw release numbers.
68
-
69
- ## 2026.2.26
70
-
71
- ### Changes
72
-
73
- - Version alignment with core OpenClaw release numbers.
74
-
75
- ## 2026.2.25
76
-
77
- ### Changes
78
-
79
- - Version alignment with core OpenClaw release numbers.
80
-
81
- ## 2026.2.24
82
-
83
- ### Changes
84
-
85
- - Version alignment with core OpenClaw release numbers.
86
-
87
- ## 2026.2.22
88
-
89
- ### Changes
90
-
91
- - Version alignment with core OpenClaw release numbers.
92
-
93
- ## 2026.1.19-1
94
-
95
- Initial release.
96
-
97
- ### Features
98
-
99
- - NIP-04 encrypted DM support (kind:4 events)
100
- - Key validation (hex and nsec formats)
101
- - Multi-relay support with sequential fallback
102
- - Event signature verification
103
- - TTL-based deduplication (24h)
104
- - Access control via dmPolicy (pairing, allowlist, open, disabled)
105
- - Pubkey normalization (hex/npub)
106
-
107
- ### Protocol Support
108
-
109
- - NIP-01: Basic event structure
110
- - NIP-04: Encrypted direct messages
111
-
112
- ### Planned for v2
113
-
114
- - NIP-17: Gift-wrapped DMs
115
- - NIP-44: Versioned encryption
116
- - Media attachments
package/src/types.test.ts DELETED
@@ -1,175 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { listNostrAccountIds, resolveDefaultNostrAccountId, resolveNostrAccount } from "./types.js";
3
-
4
- const TEST_PRIVATE_KEY = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
5
-
6
- describe("listNostrAccountIds", () => {
7
- it("returns empty array when not configured", () => {
8
- const cfg = { channels: {} };
9
- expect(listNostrAccountIds(cfg)).toEqual([]);
10
- });
11
-
12
- it("returns empty array when nostr section exists but no privateKey", () => {
13
- const cfg = { channels: { nostr: { enabled: true } } };
14
- expect(listNostrAccountIds(cfg)).toEqual([]);
15
- });
16
-
17
- it("returns default when privateKey is configured", () => {
18
- const cfg = {
19
- channels: {
20
- nostr: { privateKey: TEST_PRIVATE_KEY },
21
- },
22
- };
23
- expect(listNostrAccountIds(cfg)).toEqual(["default"]);
24
- });
25
-
26
- it("returns configured defaultAccount when privateKey is configured", () => {
27
- const cfg = {
28
- channels: {
29
- nostr: { privateKey: TEST_PRIVATE_KEY, defaultAccount: "work" },
30
- },
31
- };
32
- expect(listNostrAccountIds(cfg)).toEqual(["work"]);
33
- });
34
- });
35
-
36
- describe("resolveDefaultNostrAccountId", () => {
37
- it("returns default when configured", () => {
38
- const cfg = {
39
- channels: {
40
- nostr: { privateKey: TEST_PRIVATE_KEY },
41
- },
42
- };
43
- expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
44
- });
45
-
46
- it("returns default when not configured", () => {
47
- const cfg = { channels: {} };
48
- expect(resolveDefaultNostrAccountId(cfg)).toBe("default");
49
- });
50
-
51
- it("prefers configured defaultAccount when present", () => {
52
- const cfg = {
53
- channels: {
54
- nostr: { privateKey: TEST_PRIVATE_KEY, defaultAccount: "work" },
55
- },
56
- };
57
- expect(resolveDefaultNostrAccountId(cfg)).toBe("work");
58
- });
59
- });
60
-
61
- describe("resolveNostrAccount", () => {
62
- it("resolves configured account", () => {
63
- const cfg = {
64
- channels: {
65
- nostr: {
66
- privateKey: TEST_PRIVATE_KEY,
67
- name: "Test Bot",
68
- relays: ["wss://test.relay"],
69
- dmPolicy: "pairing" as const,
70
- },
71
- },
72
- };
73
- const account = resolveNostrAccount({ cfg });
74
-
75
- expect(account.accountId).toBe("default");
76
- expect(account.name).toBe("Test Bot");
77
- expect(account.enabled).toBe(true);
78
- expect(account.configured).toBe(true);
79
- expect(account.privateKey).toBe(TEST_PRIVATE_KEY);
80
- expect(account.publicKey).toMatch(/^[0-9a-f]{64}$/);
81
- expect(account.relays).toEqual(["wss://test.relay"]);
82
- });
83
-
84
- it("resolves unconfigured account with defaults", () => {
85
- const cfg = { channels: {} };
86
- const account = resolveNostrAccount({ cfg });
87
-
88
- expect(account.accountId).toBe("default");
89
- expect(account.enabled).toBe(true);
90
- expect(account.configured).toBe(false);
91
- expect(account.privateKey).toBe("");
92
- expect(account.publicKey).toBe("");
93
- expect(account.relays).toContain("wss://relay.damus.io");
94
- expect(account.relays).toContain("wss://nos.lol");
95
- });
96
-
97
- it("handles disabled channel", () => {
98
- const cfg = {
99
- channels: {
100
- nostr: {
101
- enabled: false,
102
- privateKey: TEST_PRIVATE_KEY,
103
- },
104
- },
105
- };
106
- const account = resolveNostrAccount({ cfg });
107
-
108
- expect(account.enabled).toBe(false);
109
- expect(account.configured).toBe(true);
110
- });
111
-
112
- it("handles custom accountId parameter", () => {
113
- const cfg = {
114
- channels: {
115
- nostr: { privateKey: TEST_PRIVATE_KEY },
116
- },
117
- };
118
- const account = resolveNostrAccount({ cfg, accountId: "custom" });
119
-
120
- expect(account.accountId).toBe("custom");
121
- });
122
-
123
- it("handles allowFrom config", () => {
124
- const cfg = {
125
- channels: {
126
- nostr: {
127
- privateKey: TEST_PRIVATE_KEY,
128
- allowFrom: ["npub1test", "0123456789abcdef"],
129
- },
130
- },
131
- };
132
- const account = resolveNostrAccount({ cfg });
133
-
134
- expect(account.config.allowFrom).toEqual(["npub1test", "0123456789abcdef"]);
135
- });
136
-
137
- it("handles invalid private key gracefully", () => {
138
- const cfg = {
139
- channels: {
140
- nostr: {
141
- privateKey: "invalid-key",
142
- },
143
- },
144
- };
145
- const account = resolveNostrAccount({ cfg });
146
-
147
- expect(account.configured).toBe(true); // key is present
148
- expect(account.publicKey).toBe(""); // but can't derive pubkey
149
- });
150
-
151
- it("preserves all config options", () => {
152
- const cfg = {
153
- channels: {
154
- nostr: {
155
- privateKey: TEST_PRIVATE_KEY,
156
- name: "Bot",
157
- enabled: true,
158
- relays: ["wss://relay1", "wss://relay2"],
159
- dmPolicy: "allowlist" as const,
160
- allowFrom: ["pubkey1", "pubkey2"],
161
- },
162
- },
163
- };
164
- const account = resolveNostrAccount({ cfg });
165
-
166
- expect(account.config).toEqual({
167
- privateKey: TEST_PRIVATE_KEY,
168
- name: "Bot",
169
- enabled: true,
170
- relays: ["wss://relay1", "wss://relay2"],
171
- dmPolicy: "allowlist",
172
- allowFrom: ["pubkey1", "pubkey2"],
173
- });
174
- });
175
- });