@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 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 | Type | Default | Description |
51
- |-----|------|---------|-------------|
52
- | `privateKey` | string | required | Bot's private key (nsec or hex format) |
53
- | `relays` | string[] | `["wss://relay.damus.io", "wss://nos.lol"]` | WebSocket relay URLs |
54
- | `dmPolicy` | string | `"pairing"` | Access control: `pairing`, `allowlist`, `open`, `disabled` |
55
- | `allowFrom` | string[] | `[]` | Allowed sender pubkeys (npub or hex) |
56
- | `enabled` | boolean | `true` | Enable/disable the channel |
57
- | `name` | string | - | Display name for the account |
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 | Status | Notes |
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 | Gift-wrapped DMs (v2) |
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 { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
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() as OpenClawConfig;
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() as OpenClawConfig;
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() as OpenClawConfig;
51
+ const cfg = runtime.config.loadConfig();
53
52
  const account = resolveNostrAccount({ cfg, accountId });
54
53
  if (!account.configured || !account.publicKey) {
55
54
  return null;
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "id": "nostr",
3
- "channels": [
4
- "nostr"
5
- ],
3
+ "channels": ["nostr"],
6
4
  "configSchema": {
7
5
  "type": "object",
8
6
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@openclaw/nostr",
3
- "version": "2026.1.29",
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
  }
@@ -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) return;
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) return;
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) return;
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) return;
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) return;
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 === "*") return "*";
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) return [];
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(`[${account.accountId}] starting Nostr provider (pubkey: ${account.publicKey})`);
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(`[${account.accountId}] Circuit breaker opened for relay: ${event.labels?.relay}`);
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(`[${account.accountId}] Circuit breaker closed for relay: ${event.labels?.relay}`);
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(`[${account.accountId}] Nostr provider started, connected to ${account.relays.length} relay(s)`);
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(accountId: string = DEFAULT_ACCOUNT_ID): MetricsSnapshot | undefined {
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;
@@ -18,7 +18,7 @@ const safeUrlSchema = z
18
18
  return false;
19
19
  }
20
20
  },
21
- { message: "URL must use https:// protocol" }
21
+ { message: "URL must use https:// protocol" },
22
22
  );
23
23
 
24
24
  /**
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) getOrCreateRelay(relayUrl).connects += value;
303
+ if (relayUrl) {
304
+ getOrCreateRelay(relayUrl).connects += value;
305
+ }
310
306
  break;
311
307
  case "relay.disconnect":
312
- if (relayUrl) getOrCreateRelay(relayUrl).disconnects += value;
308
+ if (relayUrl) {
309
+ getOrCreateRelay(relayUrl).disconnects += value;
310
+ }
313
311
  break;
314
312
  case "relay.reconnect":
315
- if (relayUrl) getOrCreateRelay(relayUrl).reconnects += value;
313
+ if (relayUrl) {
314
+ getOrCreateRelay(relayUrl).reconnects += value;
315
+ }
316
316
  break;
317
317
  case "relay.error":
318
- if (relayUrl) getOrCreateRelay(relayUrl).errors += value;
318
+ if (relayUrl) {
319
+ getOrCreateRelay(relayUrl).errors += value;
320
+ }
319
321
  break;
320
322
  case "relay.message.event":
321
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.event += value;
323
+ if (relayUrl) {
324
+ getOrCreateRelay(relayUrl).messagesReceived.event += value;
325
+ }
322
326
  break;
323
327
  case "relay.message.eose":
324
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.eose += value;
328
+ if (relayUrl) {
329
+ getOrCreateRelay(relayUrl).messagesReceived.eose += value;
330
+ }
325
331
  break;
326
332
  case "relay.message.closed":
327
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.closed += value;
333
+ if (relayUrl) {
334
+ getOrCreateRelay(relayUrl).messagesReceived.closed += value;
335
+ }
328
336
  break;
329
337
  case "relay.message.notice":
330
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.notice += value;
338
+ if (relayUrl) {
339
+ getOrCreateRelay(relayUrl).messagesReceived.notice += value;
340
+ }
331
341
  break;
332
342
  case "relay.message.ok":
333
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.ok += value;
343
+ if (relayUrl) {
344
+ getOrCreateRelay(relayUrl).messagesReceived.ok += value;
345
+ }
334
346
  break;
335
347
  case "relay.message.auth":
336
- if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.auth += value;
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, beforeEach, afterEach } from "vitest";
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
@@ -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
  });