@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.
- package/README.md +6 -0
- package/api.ts +10 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +60 -36
- package/openclaw.plugin.json +190 -1
- package/package.json +41 -9
- package/runtime-api.ts +6 -0
- package/setup-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/channel-api.ts +15 -0
- package/src/channel.inbound.test.ts +176 -0
- package/src/channel.outbound.test.ts +89 -49
- package/src/channel.setup.ts +231 -0
- package/src/channel.test.ts +439 -71
- package/src/channel.ts +147 -283
- package/src/config-schema.ts +18 -12
- package/src/default-relays.ts +1 -0
- package/src/gateway.ts +302 -0
- package/src/inbound-direct-dm-runtime.ts +1 -0
- package/src/metrics.ts +6 -6
- package/src/nostr-bus.fuzz.test.ts +74 -247
- package/src/nostr-bus.inbound.test.ts +526 -0
- package/src/nostr-bus.integration.test.ts +88 -64
- package/src/nostr-bus.test.ts +22 -31
- package/src/nostr-bus.ts +206 -136
- package/src/nostr-key-utils.ts +94 -0
- package/src/nostr-profile-core.ts +134 -0
- package/src/nostr-profile-http-runtime.ts +6 -0
- package/src/nostr-profile-http.test.ts +276 -167
- package/src/nostr-profile-http.ts +51 -36
- package/src/nostr-profile-import.ts +3 -3
- package/src/nostr-profile-url-safety.ts +21 -0
- package/src/nostr-profile.fuzz.test.ts +7 -57
- package/src/nostr-profile.test.ts +16 -14
- package/src/nostr-profile.ts +13 -146
- package/src/nostr-state-store.test.ts +106 -2
- package/src/nostr-state-store.ts +46 -49
- package/src/runtime.ts +6 -3
- package/src/seen-tracker.ts +1 -1
- package/src/session-route.ts +25 -0
- package/src/setup-surface.ts +265 -0
- package/src/test-fixtures.ts +45 -0
- package/src/types.ts +26 -25
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -116
- 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
|
|
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 {
|
|
9
|
-
import {
|
|
14
|
+
import { DEFAULT_RELAYS } from "./default-relays.js";
|
|
15
|
+
import { getPublicKeyFromPrivate } from "./nostr-key-utils.js";
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
interface NostrAccountConfig {
|
|
12
18
|
enabled?: boolean;
|
|
13
19
|
name?: string;
|
|
14
20
|
defaultAccount?: string;
|
|
15
|
-
privateKey?:
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
86
|
+
const privateKey = normalizeSecretInputString(nostrCfg?.privateKey) ?? "";
|
|
87
|
+
const configured = Boolean(privateKey);
|
|
87
88
|
|
|
88
89
|
let publicKey = "";
|
|
89
|
-
if (
|
|
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
|
|
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
|
-
});
|