@openclaw/nostr 2026.1.29 → 2026.2.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/CHANGELOG.md +23 -0
- package/README.md +16 -16
- package/index.ts +6 -7
- package/openclaw.plugin.json +1 -3
- package/package.json +10 -7
- package/src/channel.test.ts +15 -5
- package/src/channel.ts +28 -17
- package/src/config-schema.ts +1 -1
- package/src/metrics.ts +33 -19
- package/src/nostr-bus.fuzz.test.ts +11 -22
- package/src/nostr-bus.integration.test.ts +2 -6
- package/src/nostr-bus.test.ts +3 -3
- package/src/nostr-bus.ts +56 -82
- package/src/nostr-profile-http.test.ts +10 -10
- package/src/nostr-profile-http.ts +37 -18
- package/src/nostr-profile-import.test.ts +2 -3
- package/src/nostr-profile-import.ts +10 -7
- package/src/nostr-profile.fuzz.test.ts +7 -9
- package/src/nostr-profile.test.ts +7 -7
- package/src/nostr-profile.ts +56 -21
- package/src/nostr-state-store.test.ts +10 -8
- package/src/nostr-state-store.ts +29 -29
- package/src/seen-tracker.ts +48 -16
- package/src/types.test.ts +1 -5
- package/src/types.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,28 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.2.1
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.1.31
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.1.30
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.1.29
|
|
4
22
|
|
|
5
23
|
### Changes
|
|
24
|
+
|
|
6
25
|
- Version alignment with core OpenClaw release numbers.
|
|
7
26
|
|
|
8
27
|
## 2026.1.23
|
|
9
28
|
|
|
10
29
|
### Changes
|
|
30
|
+
|
|
11
31
|
- Version alignment with core OpenClaw release numbers.
|
|
12
32
|
|
|
13
33
|
## 2026.1.22
|
|
14
34
|
|
|
15
35
|
### Changes
|
|
36
|
+
|
|
16
37
|
- Version alignment with core OpenClaw release numbers.
|
|
17
38
|
|
|
18
39
|
## 2026.1.21
|
|
19
40
|
|
|
20
41
|
### Changes
|
|
42
|
+
|
|
21
43
|
- Version alignment with core OpenClaw release numbers.
|
|
22
44
|
|
|
23
45
|
## 2026.1.20
|
|
24
46
|
|
|
25
47
|
### Changes
|
|
48
|
+
|
|
26
49
|
- Version alignment with core OpenClaw release numbers.
|
|
27
50
|
|
|
28
51
|
## 2026.1.19-1
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ openclaw plugins install @openclaw/nostr
|
|
|
19
19
|
## Quick Setup
|
|
20
20
|
|
|
21
21
|
1. Generate a Nostr keypair (if you don't have one):
|
|
22
|
+
|
|
22
23
|
```bash
|
|
23
24
|
# Using nak CLI
|
|
24
25
|
nak key generate
|
|
@@ -27,6 +28,7 @@ openclaw plugins install @openclaw/nostr
|
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
2. Add to your config:
|
|
31
|
+
|
|
30
32
|
```json
|
|
31
33
|
{
|
|
32
34
|
"channels": {
|
|
@@ -39,6 +41,7 @@ openclaw plugins install @openclaw/nostr
|
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
3. Set the environment variable:
|
|
44
|
+
|
|
42
45
|
```bash
|
|
43
46
|
export NOSTR_PRIVATE_KEY="nsec1..." # or hex format
|
|
44
47
|
```
|
|
@@ -47,14 +50,14 @@ openclaw plugins install @openclaw/nostr
|
|
|
47
50
|
|
|
48
51
|
## Configuration
|
|
49
52
|
|
|
50
|
-
| Key
|
|
51
|
-
|
|
52
|
-
| `privateKey` | string
|
|
53
|
-
| `relays`
|
|
54
|
-
| `dmPolicy`
|
|
55
|
-
| `allowFrom`
|
|
56
|
-
| `enabled`
|
|
57
|
-
| `name`
|
|
53
|
+
| Key | Type | Default | Description |
|
|
54
|
+
| ------------ | -------- | ------------------------------------------- | ---------------------------------------------------------- |
|
|
55
|
+
| `privateKey` | string | required | Bot's private key (nsec or hex format) |
|
|
56
|
+
| `relays` | string[] | `["wss://relay.damus.io", "wss://nos.lol"]` | WebSocket relay URLs |
|
|
57
|
+
| `dmPolicy` | string | `"pairing"` | Access control: `pairing`, `allowlist`, `open`, `disabled` |
|
|
58
|
+
| `allowFrom` | string[] | `[]` | Allowed sender pubkeys (npub or hex) |
|
|
59
|
+
| `enabled` | boolean | `true` | Enable/disable the channel |
|
|
60
|
+
| `name` | string | - | Display name for the account |
|
|
58
61
|
|
|
59
62
|
## Access Control
|
|
60
63
|
|
|
@@ -73,10 +76,7 @@ openclaw plugins install @openclaw/nostr
|
|
|
73
76
|
"nostr": {
|
|
74
77
|
"privateKey": "${NOSTR_PRIVATE_KEY}",
|
|
75
78
|
"dmPolicy": "allowlist",
|
|
76
|
-
"allowFrom": [
|
|
77
|
-
"npub1abc...",
|
|
78
|
-
"0123456789abcdef..."
|
|
79
|
-
]
|
|
79
|
+
"allowFrom": ["npub1abc...", "0123456789abcdef..."]
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -103,11 +103,11 @@ docker run -p 7777:7777 ghcr.io/hoytech/strfry
|
|
|
103
103
|
|
|
104
104
|
## Protocol Support
|
|
105
105
|
|
|
106
|
-
| NIP
|
|
107
|
-
|
|
108
|
-
| NIP-01 | Supported | Basic event structure
|
|
106
|
+
| NIP | Status | Notes |
|
|
107
|
+
| ------ | --------- | ---------------------- |
|
|
108
|
+
| NIP-01 | Supported | Basic event structure |
|
|
109
109
|
| NIP-04 | Supported | Encrypted DMs (kind:4) |
|
|
110
|
-
| NIP-17 | Planned
|
|
110
|
+
| NIP-17 | Planned | Gift-wrapped DMs (v2) |
|
|
111
111
|
|
|
112
112
|
## Security Notes
|
|
113
113
|
|
package/index.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
-
|
|
3
|
+
import type { NostrProfile } from "./src/config-schema.js";
|
|
4
4
|
import { nostrPlugin } from "./src/channel.js";
|
|
5
|
-
import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js";
|
|
6
5
|
import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
|
|
6
|
+
import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js";
|
|
7
7
|
import { resolveNostrAccount } from "./src/types.js";
|
|
8
|
-
import type { NostrProfile } from "./src/config-schema.js";
|
|
9
8
|
|
|
10
9
|
const plugin = {
|
|
11
10
|
id: "nostr",
|
|
@@ -20,13 +19,13 @@ const plugin = {
|
|
|
20
19
|
const httpHandler = createNostrProfileHttpHandler({
|
|
21
20
|
getConfigProfile: (accountId: string) => {
|
|
22
21
|
const runtime = getNostrRuntime();
|
|
23
|
-
const cfg = runtime.config.loadConfig()
|
|
22
|
+
const cfg = runtime.config.loadConfig();
|
|
24
23
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
25
24
|
return account.profile;
|
|
26
25
|
},
|
|
27
26
|
updateConfigProfile: async (accountId: string, profile: NostrProfile) => {
|
|
28
27
|
const runtime = getNostrRuntime();
|
|
29
|
-
const cfg = runtime.config.loadConfig()
|
|
28
|
+
const cfg = runtime.config.loadConfig();
|
|
30
29
|
|
|
31
30
|
// Build the config patch for channels.nostr.profile
|
|
32
31
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
@@ -49,7 +48,7 @@ const plugin = {
|
|
|
49
48
|
},
|
|
50
49
|
getAccountInfo: (accountId: string) => {
|
|
51
50
|
const runtime = getNostrRuntime();
|
|
52
|
-
const cfg = runtime.config.loadConfig()
|
|
51
|
+
const cfg = runtime.config.loadConfig();
|
|
53
52
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
54
53
|
if (!account.configured || !account.publicKey) {
|
|
55
54
|
return null;
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/nostr",
|
|
3
|
-
"version": "2026.1
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2026.2.1",
|
|
5
4
|
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"nostr-tools": "^2.22.1",
|
|
8
|
+
"openclaw": "workspace:*",
|
|
9
|
+
"zod": "^4.3.6"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"openclaw": "workspace:*"
|
|
13
|
+
},
|
|
6
14
|
"openclaw": {
|
|
7
15
|
"extensions": [
|
|
8
16
|
"./index.ts"
|
|
@@ -22,10 +30,5 @@
|
|
|
22
30
|
"localPath": "extensions/nostr",
|
|
23
31
|
"defaultChoice": "npm"
|
|
24
32
|
}
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"openclaw": "workspace:*",
|
|
28
|
-
"nostr-tools": "^2.20.0",
|
|
29
|
-
"zod": "^4.3.6"
|
|
30
33
|
}
|
|
31
34
|
}
|
package/src/channel.test.ts
CHANGED
|
@@ -61,14 +61,18 @@ describe("nostrPlugin", () => {
|
|
|
61
61
|
|
|
62
62
|
it("recognizes npub as valid target", () => {
|
|
63
63
|
const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
|
|
64
|
-
if (!looksLikeId)
|
|
64
|
+
if (!looksLikeId) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
65
67
|
|
|
66
68
|
expect(looksLikeId("npub1xyz123")).toBe(true);
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
it("recognizes hex pubkey as valid target", () => {
|
|
70
72
|
const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
|
|
71
|
-
if (!looksLikeId)
|
|
73
|
+
if (!looksLikeId) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
72
76
|
|
|
73
77
|
const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
74
78
|
expect(looksLikeId(hexPubkey)).toBe(true);
|
|
@@ -76,7 +80,9 @@ describe("nostrPlugin", () => {
|
|
|
76
80
|
|
|
77
81
|
it("rejects invalid input", () => {
|
|
78
82
|
const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
|
|
79
|
-
if (!looksLikeId)
|
|
83
|
+
if (!looksLikeId) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
80
86
|
|
|
81
87
|
expect(looksLikeId("not-a-pubkey")).toBe(false);
|
|
82
88
|
expect(looksLikeId("")).toBe(false);
|
|
@@ -84,7 +90,9 @@ describe("nostrPlugin", () => {
|
|
|
84
90
|
|
|
85
91
|
it("normalizeTarget strips nostr: prefix", () => {
|
|
86
92
|
const normalize = nostrPlugin.messaging?.normalizeTarget;
|
|
87
|
-
if (!normalize)
|
|
93
|
+
if (!normalize) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
88
96
|
|
|
89
97
|
const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
90
98
|
expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey);
|
|
@@ -108,7 +116,9 @@ describe("nostrPlugin", () => {
|
|
|
108
116
|
|
|
109
117
|
it("normalizes nostr: prefix in allow entries", () => {
|
|
110
118
|
const normalize = nostrPlugin.pairing?.normalizeAllowEntry;
|
|
111
|
-
if (!normalize)
|
|
119
|
+
if (!normalize) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
112
122
|
|
|
113
123
|
const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
114
124
|
expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey);
|
package/src/channel.ts
CHANGED
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
formatPairingApproveHint,
|
|
5
5
|
type ChannelPlugin,
|
|
6
6
|
} from "openclaw/plugin-sdk";
|
|
7
|
-
|
|
7
|
+
import type { NostrProfile } from "./config-schema.js";
|
|
8
|
+
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";
|
|
9
|
+
import type { ProfilePublishResult } from "./nostr-profile.js";
|
|
8
10
|
import { NostrConfigSchema } from "./config-schema.js";
|
|
11
|
+
import { normalizePubkey, startNostrBus, type NostrBusHandle } from "./nostr-bus.js";
|
|
9
12
|
import { getNostrRuntime } from "./runtime.js";
|
|
10
13
|
import {
|
|
11
14
|
listNostrAccountIds,
|
|
@@ -13,10 +16,6 @@ import {
|
|
|
13
16
|
resolveNostrAccount,
|
|
14
17
|
type ResolvedNostrAccount,
|
|
15
18
|
} from "./types.js";
|
|
16
|
-
import { normalizePubkey, startNostrBus, type NostrBusHandle } from "./nostr-bus.js";
|
|
17
|
-
import type { MetricEvent, MetricsSnapshot } from "./metrics.js";
|
|
18
|
-
import type { NostrProfile } from "./config-schema.js";
|
|
19
|
-
import type { ProfilePublishResult } from "./nostr-profile.js";
|
|
20
19
|
|
|
21
20
|
// Store active bus handles per account
|
|
22
21
|
const activeBuses = new Map<string, NostrBusHandle>();
|
|
@@ -56,14 +55,16 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
56
55
|
}),
|
|
57
56
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
58
57
|
(resolveNostrAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) =>
|
|
59
|
-
String(entry)
|
|
58
|
+
String(entry),
|
|
60
59
|
),
|
|
61
60
|
formatAllowFrom: ({ allowFrom }) =>
|
|
62
61
|
allowFrom
|
|
63
62
|
.map((entry) => String(entry).trim())
|
|
64
63
|
.filter(Boolean)
|
|
65
64
|
.map((entry) => {
|
|
66
|
-
if (entry === "*")
|
|
65
|
+
if (entry === "*") {
|
|
66
|
+
return "*";
|
|
67
|
+
}
|
|
67
68
|
try {
|
|
68
69
|
return normalizePubkey(entry);
|
|
69
70
|
} catch {
|
|
@@ -162,7 +163,9 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
162
163
|
collectStatusIssues: (accounts) =>
|
|
163
164
|
accounts.flatMap((account) => {
|
|
164
165
|
const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
|
|
165
|
-
if (!lastError)
|
|
166
|
+
if (!lastError) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
166
169
|
return [
|
|
167
170
|
{
|
|
168
171
|
channel: "nostr",
|
|
@@ -203,7 +206,9 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
203
206
|
accountId: account.accountId,
|
|
204
207
|
publicKey: account.publicKey,
|
|
205
208
|
});
|
|
206
|
-
ctx.log?.info(
|
|
209
|
+
ctx.log?.info(
|
|
210
|
+
`[${account.accountId}] starting Nostr provider (pubkey: ${account.publicKey})`,
|
|
211
|
+
);
|
|
207
212
|
|
|
208
213
|
if (!account.configured) {
|
|
209
214
|
throw new Error("Nostr private key not configured");
|
|
@@ -251,9 +256,13 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
251
256
|
if (event.name.startsWith("event.rejected.")) {
|
|
252
257
|
ctx.log?.debug(`[${account.accountId}] Metric: ${event.name}`, event.labels);
|
|
253
258
|
} else if (event.name === "relay.circuit_breaker.open") {
|
|
254
|
-
ctx.log?.warn(
|
|
259
|
+
ctx.log?.warn(
|
|
260
|
+
`[${account.accountId}] Circuit breaker opened for relay: ${event.labels?.relay}`,
|
|
261
|
+
);
|
|
255
262
|
} else if (event.name === "relay.circuit_breaker.close") {
|
|
256
|
-
ctx.log?.info(
|
|
263
|
+
ctx.log?.info(
|
|
264
|
+
`[${account.accountId}] Circuit breaker closed for relay: ${event.labels?.relay}`,
|
|
265
|
+
);
|
|
257
266
|
} else if (event.name === "relay.error") {
|
|
258
267
|
ctx.log?.debug(`[${account.accountId}] Relay error: ${event.labels?.relay}`);
|
|
259
268
|
}
|
|
@@ -269,7 +278,9 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
269
278
|
// Store the bus handle
|
|
270
279
|
activeBuses.set(account.accountId, bus);
|
|
271
280
|
|
|
272
|
-
ctx.log?.info(
|
|
281
|
+
ctx.log?.info(
|
|
282
|
+
`[${account.accountId}] Nostr provider started, connected to ${account.relays.length} relay(s)`,
|
|
283
|
+
);
|
|
273
284
|
|
|
274
285
|
// Return cleanup function
|
|
275
286
|
return {
|
|
@@ -288,7 +299,9 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
|
|
288
299
|
* Get metrics snapshot for a Nostr account.
|
|
289
300
|
* Returns undefined if account is not running.
|
|
290
301
|
*/
|
|
291
|
-
export function getNostrMetrics(
|
|
302
|
+
export function getNostrMetrics(
|
|
303
|
+
accountId: string = DEFAULT_ACCOUNT_ID,
|
|
304
|
+
): MetricsSnapshot | undefined {
|
|
292
305
|
const bus = activeBuses.get(accountId);
|
|
293
306
|
if (bus) {
|
|
294
307
|
return bus.getMetrics();
|
|
@@ -313,7 +326,7 @@ export function getActiveNostrBuses(): Map<string, NostrBusHandle> {
|
|
|
313
326
|
*/
|
|
314
327
|
export async function publishNostrProfile(
|
|
315
328
|
accountId: string = DEFAULT_ACCOUNT_ID,
|
|
316
|
-
profile: NostrProfile
|
|
329
|
+
profile: NostrProfile,
|
|
317
330
|
): Promise<ProfilePublishResult> {
|
|
318
331
|
const bus = activeBuses.get(accountId);
|
|
319
332
|
if (!bus) {
|
|
@@ -327,9 +340,7 @@ export async function publishNostrProfile(
|
|
|
327
340
|
* @param accountId - Account ID (defaults to "default")
|
|
328
341
|
* @returns Profile publish state or null if account not running
|
|
329
342
|
*/
|
|
330
|
-
export async function getNostrProfileState(
|
|
331
|
-
accountId: string = DEFAULT_ACCOUNT_ID
|
|
332
|
-
): Promise<{
|
|
343
|
+
export async function getNostrProfileState(accountId: string = DEFAULT_ACCOUNT_ID): Promise<{
|
|
333
344
|
lastPublishedAt: number | null;
|
|
334
345
|
lastPublishedEventId: string | null;
|
|
335
346
|
lastPublishResults: Record<string, "ok" | "failed" | "timeout"> | null;
|
package/src/config-schema.ts
CHANGED
package/src/metrics.ts
CHANGED
|
@@ -41,9 +41,7 @@ export type RateLimitMetricName = "rate_limit.per_sender" | "rate_limit.global";
|
|
|
41
41
|
|
|
42
42
|
export type DecryptMetricName = "decrypt.success" | "decrypt.failure";
|
|
43
43
|
|
|
44
|
-
export type MemoryMetricName =
|
|
45
|
-
| "memory.seen_tracker_size"
|
|
46
|
-
| "memory.rate_limiter_entries";
|
|
44
|
+
export type MemoryMetricName = "memory.seen_tracker_size" | "memory.rate_limiter_entries";
|
|
47
45
|
|
|
48
46
|
export type MetricName =
|
|
49
47
|
| EventMetricName
|
|
@@ -144,11 +142,7 @@ export interface MetricsSnapshot {
|
|
|
144
142
|
|
|
145
143
|
export interface NostrMetrics {
|
|
146
144
|
/** Emit a metric event */
|
|
147
|
-
emit: (
|
|
148
|
-
name: MetricName,
|
|
149
|
-
value?: number,
|
|
150
|
-
labels?: Record<string, string | number>
|
|
151
|
-
) => void;
|
|
145
|
+
emit: (name: MetricName, value?: number, labels?: Record<string, string | number>) => void;
|
|
152
146
|
|
|
153
147
|
/** Get current metrics snapshot */
|
|
154
148
|
getSnapshot: () => MetricsSnapshot;
|
|
@@ -247,7 +241,7 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|
|
247
241
|
function emit(
|
|
248
242
|
name: MetricName,
|
|
249
243
|
value: number = 1,
|
|
250
|
-
labels?: Record<string, string | number
|
|
244
|
+
labels?: Record<string, string | number>,
|
|
251
245
|
): void {
|
|
252
246
|
// Fire callback if provided
|
|
253
247
|
if (onMetric) {
|
|
@@ -306,34 +300,54 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|
|
306
300
|
|
|
307
301
|
// Relay metrics
|
|
308
302
|
case "relay.connect":
|
|
309
|
-
if (relayUrl)
|
|
303
|
+
if (relayUrl) {
|
|
304
|
+
getOrCreateRelay(relayUrl).connects += value;
|
|
305
|
+
}
|
|
310
306
|
break;
|
|
311
307
|
case "relay.disconnect":
|
|
312
|
-
if (relayUrl)
|
|
308
|
+
if (relayUrl) {
|
|
309
|
+
getOrCreateRelay(relayUrl).disconnects += value;
|
|
310
|
+
}
|
|
313
311
|
break;
|
|
314
312
|
case "relay.reconnect":
|
|
315
|
-
if (relayUrl)
|
|
313
|
+
if (relayUrl) {
|
|
314
|
+
getOrCreateRelay(relayUrl).reconnects += value;
|
|
315
|
+
}
|
|
316
316
|
break;
|
|
317
317
|
case "relay.error":
|
|
318
|
-
if (relayUrl)
|
|
318
|
+
if (relayUrl) {
|
|
319
|
+
getOrCreateRelay(relayUrl).errors += value;
|
|
320
|
+
}
|
|
319
321
|
break;
|
|
320
322
|
case "relay.message.event":
|
|
321
|
-
if (relayUrl)
|
|
323
|
+
if (relayUrl) {
|
|
324
|
+
getOrCreateRelay(relayUrl).messagesReceived.event += value;
|
|
325
|
+
}
|
|
322
326
|
break;
|
|
323
327
|
case "relay.message.eose":
|
|
324
|
-
if (relayUrl)
|
|
328
|
+
if (relayUrl) {
|
|
329
|
+
getOrCreateRelay(relayUrl).messagesReceived.eose += value;
|
|
330
|
+
}
|
|
325
331
|
break;
|
|
326
332
|
case "relay.message.closed":
|
|
327
|
-
if (relayUrl)
|
|
333
|
+
if (relayUrl) {
|
|
334
|
+
getOrCreateRelay(relayUrl).messagesReceived.closed += value;
|
|
335
|
+
}
|
|
328
336
|
break;
|
|
329
337
|
case "relay.message.notice":
|
|
330
|
-
if (relayUrl)
|
|
338
|
+
if (relayUrl) {
|
|
339
|
+
getOrCreateRelay(relayUrl).messagesReceived.notice += value;
|
|
340
|
+
}
|
|
331
341
|
break;
|
|
332
342
|
case "relay.message.ok":
|
|
333
|
-
if (relayUrl)
|
|
343
|
+
if (relayUrl) {
|
|
344
|
+
getOrCreateRelay(relayUrl).messagesReceived.ok += value;
|
|
345
|
+
}
|
|
334
346
|
break;
|
|
335
347
|
case "relay.message.auth":
|
|
336
|
-
if (relayUrl)
|
|
348
|
+
if (relayUrl) {
|
|
349
|
+
getOrCreateRelay(relayUrl).messagesReceived.auth += value;
|
|
350
|
+
}
|
|
337
351
|
break;
|
|
338
352
|
case "relay.circuit_breaker.open":
|
|
339
353
|
if (relayUrl) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createMetrics, type MetricName } from "./metrics.js";
|
|
2
3
|
import { validatePrivateKey, isValidPubkey, normalizePubkey } from "./nostr-bus.js";
|
|
3
4
|
import { createSeenTracker } from "./seen-tracker.js";
|
|
4
|
-
import { createMetrics, type MetricName } from "./metrics.js";
|
|
5
5
|
|
|
6
6
|
// ============================================================================
|
|
7
7
|
// Fuzz Tests for validatePrivateKey
|
|
@@ -47,60 +47,51 @@ describe("validatePrivateKey fuzz", () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("rejects RTL override", () => {
|
|
50
|
-
const withRtl =
|
|
51
|
-
"\u202E0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
50
|
+
const withRtl = "\u202E0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
52
51
|
expect(() => validatePrivateKey(withRtl)).toThrow();
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
it("rejects homoglyph 'a' (Cyrillic а)", () => {
|
|
56
55
|
// Using Cyrillic 'а' (U+0430) instead of Latin 'a'
|
|
57
|
-
const withCyrillicA =
|
|
58
|
-
"0123456789\u0430bcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
56
|
+
const withCyrillicA = "0123456789\u0430bcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
59
57
|
expect(() => validatePrivateKey(withCyrillicA)).toThrow();
|
|
60
58
|
});
|
|
61
59
|
|
|
62
60
|
it("rejects emoji", () => {
|
|
63
|
-
const withEmoji =
|
|
64
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab😀";
|
|
61
|
+
const withEmoji = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab😀";
|
|
65
62
|
expect(() => validatePrivateKey(withEmoji)).toThrow();
|
|
66
63
|
});
|
|
67
64
|
|
|
68
65
|
it("rejects combining characters", () => {
|
|
69
66
|
// 'a' followed by combining acute accent
|
|
70
|
-
const withCombining =
|
|
71
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u0301";
|
|
67
|
+
const withCombining = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u0301";
|
|
72
68
|
expect(() => validatePrivateKey(withCombining)).toThrow();
|
|
73
69
|
});
|
|
74
70
|
});
|
|
75
71
|
|
|
76
72
|
describe("injection attempts", () => {
|
|
77
73
|
it("rejects null byte injection", () => {
|
|
78
|
-
const withNullByte =
|
|
79
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\x00f";
|
|
74
|
+
const withNullByte = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\x00f";
|
|
80
75
|
expect(() => validatePrivateKey(withNullByte)).toThrow();
|
|
81
76
|
});
|
|
82
77
|
|
|
83
78
|
it("rejects newline injection", () => {
|
|
84
|
-
const withNewline =
|
|
85
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\nf";
|
|
79
|
+
const withNewline = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\nf";
|
|
86
80
|
expect(() => validatePrivateKey(withNewline)).toThrow();
|
|
87
81
|
});
|
|
88
82
|
|
|
89
83
|
it("rejects carriage return injection", () => {
|
|
90
|
-
const withCR =
|
|
91
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\rf";
|
|
84
|
+
const withCR = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\rf";
|
|
92
85
|
expect(() => validatePrivateKey(withCR)).toThrow();
|
|
93
86
|
});
|
|
94
87
|
|
|
95
88
|
it("rejects tab injection", () => {
|
|
96
|
-
const withTab =
|
|
97
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\tf";
|
|
89
|
+
const withTab = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\tf";
|
|
98
90
|
expect(() => validatePrivateKey(withTab)).toThrow();
|
|
99
91
|
});
|
|
100
92
|
|
|
101
93
|
it("rejects form feed injection", () => {
|
|
102
|
-
const withFormFeed =
|
|
103
|
-
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\ff";
|
|
94
|
+
const withFormFeed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\ff";
|
|
104
95
|
expect(() => validatePrivateKey(withFormFeed)).toThrow();
|
|
105
96
|
});
|
|
106
97
|
});
|
|
@@ -530,9 +521,7 @@ describe("JSON parsing edge cases", () => {
|
|
|
530
521
|
if (!parseError) {
|
|
531
522
|
// If it parsed, we need to validate the structure
|
|
532
523
|
const isValidRelayMessage =
|
|
533
|
-
Array.isArray(parsed) &&
|
|
534
|
-
parsed.length >= 2 &&
|
|
535
|
-
typeof parsed[0] === "string";
|
|
524
|
+
Array.isArray(parsed) && parsed.length >= 2 && typeof parsed[0] === "string";
|
|
536
525
|
|
|
537
526
|
// Most malformed cases won't produce valid relay messages
|
|
538
527
|
if (["null literal", "plain number", "plain string"].includes(desc)) {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { describe, expect, it, vi
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js";
|
|
2
3
|
import { createSeenTracker } from "./seen-tracker.js";
|
|
3
|
-
import {
|
|
4
|
-
createMetrics,
|
|
5
|
-
createNoopMetrics,
|
|
6
|
-
type MetricEvent,
|
|
7
|
-
} from "./metrics.js";
|
|
8
4
|
|
|
9
5
|
// ============================================================================
|
|
10
6
|
// Seen Tracker Integration Tests
|
package/src/nostr-bus.test.ts
CHANGED
|
@@ -47,13 +47,13 @@ describe("validatePrivateKey", () => {
|
|
|
47
47
|
|
|
48
48
|
it("rejects 63-char hex (too short)", () => {
|
|
49
49
|
expect(() => validatePrivateKey(TEST_HEX_KEY.slice(0, 63))).toThrow(
|
|
50
|
-
"Private key must be 64 hex characters"
|
|
50
|
+
"Private key must be 64 hex characters",
|
|
51
51
|
);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it("rejects 65-char hex (too long)", () => {
|
|
55
55
|
expect(() => validatePrivateKey(TEST_HEX_KEY + "0")).toThrow(
|
|
56
|
-
"Private key must be 64 hex characters"
|
|
56
|
+
"Private key must be 64 hex characters",
|
|
57
57
|
);
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -72,7 +72,7 @@ describe("validatePrivateKey", () => {
|
|
|
72
72
|
|
|
73
73
|
it("rejects key with 0x prefix", () => {
|
|
74
74
|
expect(() => validatePrivateKey("0x" + TEST_HEX_KEY)).toThrow(
|
|
75
|
-
"Private key must be 64 hex characters"
|
|
75
|
+
"Private key must be 64 hex characters",
|
|
76
76
|
);
|
|
77
77
|
});
|
|
78
78
|
});
|