@openclaw/nostr 2026.5.2 → 2026.5.3-beta.2

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 (59) hide show
  1. package/dist/api.js +532 -0
  2. package/dist/channel-DfEqBtUh.js +1466 -0
  3. package/dist/channel-plugin-api.js +2 -0
  4. package/dist/config-schema-DIk4jlBg.js +64 -0
  5. package/dist/default-relays-DLwdWOTu.js +4 -0
  6. package/dist/inbound-direct-dm-runtime-22bZWcIW.js +2 -0
  7. package/dist/index.js +84 -0
  8. package/dist/runtime-api.js +2 -0
  9. package/dist/setup-api.js +2 -0
  10. package/dist/setup-entry.js +11 -0
  11. package/dist/setup-plugin-api.js +165 -0
  12. package/dist/setup-surface-DxAaUTyC.js +336 -0
  13. package/dist/test-api.js +2 -0
  14. package/package.json +15 -6
  15. package/api.ts +0 -10
  16. package/channel-plugin-api.ts +0 -1
  17. package/index.ts +0 -97
  18. package/runtime-api.ts +0 -6
  19. package/setup-api.ts +0 -1
  20. package/setup-entry.ts +0 -9
  21. package/setup-plugin-api.ts +0 -3
  22. package/src/channel-api.ts +0 -15
  23. package/src/channel.inbound.test.ts +0 -176
  24. package/src/channel.outbound.test.ts +0 -128
  25. package/src/channel.setup.ts +0 -231
  26. package/src/channel.test.ts +0 -519
  27. package/src/channel.ts +0 -207
  28. package/src/config-schema.ts +0 -98
  29. package/src/default-relays.ts +0 -1
  30. package/src/gateway.ts +0 -302
  31. package/src/inbound-direct-dm-runtime.ts +0 -1
  32. package/src/metrics.ts +0 -458
  33. package/src/nostr-bus.fuzz.test.ts +0 -360
  34. package/src/nostr-bus.inbound.test.ts +0 -526
  35. package/src/nostr-bus.integration.test.ts +0 -472
  36. package/src/nostr-bus.test.ts +0 -190
  37. package/src/nostr-bus.ts +0 -789
  38. package/src/nostr-key-utils.ts +0 -94
  39. package/src/nostr-profile-core.ts +0 -134
  40. package/src/nostr-profile-http-runtime.ts +0 -6
  41. package/src/nostr-profile-http.test.ts +0 -632
  42. package/src/nostr-profile-http.ts +0 -594
  43. package/src/nostr-profile-import.test.ts +0 -119
  44. package/src/nostr-profile-import.ts +0 -262
  45. package/src/nostr-profile-url-safety.ts +0 -21
  46. package/src/nostr-profile.fuzz.test.ts +0 -430
  47. package/src/nostr-profile.test.ts +0 -412
  48. package/src/nostr-profile.ts +0 -144
  49. package/src/nostr-state-store.test.ts +0 -237
  50. package/src/nostr-state-store.ts +0 -223
  51. package/src/runtime.ts +0 -9
  52. package/src/seen-tracker.ts +0 -289
  53. package/src/session-route.ts +0 -25
  54. package/src/setup-surface.ts +0 -265
  55. package/src/test-fixtures.ts +0 -45
  56. package/src/types.ts +0 -117
  57. package/test/setup.ts +0 -5
  58. package/test-api.ts +0 -1
  59. package/tsconfig.json +0 -16
@@ -1,472 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js";
3
- import { createSeenTracker } from "./seen-tracker.js";
4
- import { TEST_RELAY_URL } from "./test-fixtures.js";
5
-
6
- const TEST_RELAY_URL_1 = "wss://relay1.com";
7
- const TEST_RELAY_URL_2 = "wss://relay2.com";
8
- const TEST_RELAY_URL_PRIMARY = "wss://relay.com";
9
- const TEST_RELAY_URL_GOOD = "wss://good-relay.com";
10
- const TEST_RELAY_URL_BAD = "wss://bad-relay.com";
11
-
12
- function createTracker(overrides?: Partial<Parameters<typeof createSeenTracker>[0]>) {
13
- return createSeenTracker({
14
- maxEntries: 100,
15
- ttlMs: 60000,
16
- ...overrides,
17
- });
18
- }
19
-
20
- function createCollectingMetrics() {
21
- const events: MetricEvent[] = [];
22
- return {
23
- events,
24
- metrics: createMetrics((event) => events.push(event)),
25
- };
26
- }
27
-
28
- function createPlainMetrics() {
29
- return createMetrics();
30
- }
31
-
32
- // ============================================================================
33
- // Seen Tracker Integration Tests
34
- // ============================================================================
35
-
36
- describe("SeenTracker", () => {
37
- describe("basic operations", () => {
38
- it("tracks seen IDs", () => {
39
- const tracker = createTracker();
40
-
41
- // First check returns false and adds
42
- expect(tracker.has("id1")).toBe(false);
43
- // Second check returns true (already seen)
44
- expect(tracker.has("id1")).toBe(true);
45
-
46
- tracker.stop();
47
- });
48
-
49
- it("peek does not add", () => {
50
- const tracker = createTracker();
51
-
52
- expect(tracker.peek("id1")).toBe(false);
53
- expect(tracker.peek("id1")).toBe(false); // Still false
54
-
55
- tracker.add("id1");
56
- expect(tracker.peek("id1")).toBe(true);
57
-
58
- tracker.stop();
59
- });
60
-
61
- it("delete removes entries", () => {
62
- const tracker = createTracker();
63
-
64
- tracker.add("id1");
65
- expect(tracker.peek("id1")).toBe(true);
66
-
67
- tracker.delete("id1");
68
- expect(tracker.peek("id1")).toBe(false);
69
-
70
- tracker.stop();
71
- });
72
-
73
- it("clear removes all entries", () => {
74
- const tracker = createTracker();
75
-
76
- tracker.add("id1");
77
- tracker.add("id2");
78
- tracker.add("id3");
79
- expect(tracker.size()).toBe(3);
80
-
81
- tracker.clear();
82
- expect(tracker.size()).toBe(0);
83
- expect(tracker.peek("id1")).toBe(false);
84
-
85
- tracker.stop();
86
- });
87
-
88
- it("seed pre-populates entries", () => {
89
- const tracker = createTracker();
90
-
91
- tracker.seed(["id1", "id2", "id3"]);
92
- expect(tracker.size()).toBe(3);
93
- expect(tracker.peek("id1")).toBe(true);
94
- expect(tracker.peek("id2")).toBe(true);
95
- expect(tracker.peek("id3")).toBe(true);
96
-
97
- tracker.stop();
98
- });
99
- });
100
-
101
- describe("LRU eviction", () => {
102
- it("evicts least recently used when at capacity", () => {
103
- const tracker = createTracker({ maxEntries: 3 });
104
-
105
- tracker.add("id1");
106
- tracker.add("id2");
107
- tracker.add("id3");
108
- expect(tracker.size()).toBe(3);
109
-
110
- // Adding fourth should evict oldest (id1)
111
- tracker.add("id4");
112
- expect(tracker.size()).toBe(3);
113
- expect(tracker.peek("id1")).toBe(false); // Evicted
114
- expect(tracker.peek("id2")).toBe(true);
115
- expect(tracker.peek("id3")).toBe(true);
116
- expect(tracker.peek("id4")).toBe(true);
117
-
118
- tracker.stop();
119
- });
120
-
121
- it("accessing an entry moves it to front (prevents eviction)", () => {
122
- const tracker = createTracker({ maxEntries: 3 });
123
-
124
- tracker.add("id1");
125
- tracker.add("id2");
126
- tracker.add("id3");
127
-
128
- // Access id1, moving it to front
129
- tracker.has("id1");
130
-
131
- // Add id4 - should evict id2 (now oldest)
132
- tracker.add("id4");
133
- expect(tracker.peek("id1")).toBe(true); // Not evicted, was accessed
134
- expect(tracker.peek("id2")).toBe(false); // Evicted
135
- expect(tracker.peek("id3")).toBe(true);
136
- expect(tracker.peek("id4")).toBe(true);
137
-
138
- tracker.stop();
139
- });
140
-
141
- it("handles capacity of 1", () => {
142
- const tracker = createTracker({ maxEntries: 1 });
143
-
144
- tracker.add("id1");
145
- expect(tracker.peek("id1")).toBe(true);
146
-
147
- tracker.add("id2");
148
- expect(tracker.peek("id1")).toBe(false);
149
- expect(tracker.peek("id2")).toBe(true);
150
-
151
- tracker.stop();
152
- });
153
-
154
- it("seed respects maxEntries", () => {
155
- const tracker = createTracker({ maxEntries: 2 });
156
-
157
- tracker.seed(["id1", "id2", "id3", "id4"]);
158
- expect(tracker.size()).toBe(2);
159
- // Seed stops when maxEntries reached, processing from end to start
160
- // So id4 and id3 get added first, then we're at capacity
161
- expect(tracker.peek("id3")).toBe(true);
162
- expect(tracker.peek("id4")).toBe(true);
163
-
164
- tracker.stop();
165
- });
166
- });
167
-
168
- describe("TTL expiration", () => {
169
- it("expires entries after TTL", async () => {
170
- vi.useFakeTimers();
171
-
172
- const tracker = createTracker({
173
- maxEntries: 100,
174
- ttlMs: 100,
175
- pruneIntervalMs: 50,
176
- });
177
-
178
- tracker.add("id1");
179
- expect(tracker.peek("id1")).toBe(true);
180
-
181
- // Advance past TTL
182
- vi.advanceTimersByTime(150);
183
-
184
- // Entry should be expired
185
- expect(tracker.peek("id1")).toBe(false);
186
-
187
- tracker.stop();
188
- vi.useRealTimers();
189
- });
190
-
191
- it("has() refreshes TTL", async () => {
192
- vi.useFakeTimers();
193
-
194
- const tracker = createTracker({
195
- maxEntries: 100,
196
- ttlMs: 100,
197
- pruneIntervalMs: 50,
198
- });
199
-
200
- tracker.add("id1");
201
-
202
- // Advance halfway
203
- vi.advanceTimersByTime(50);
204
-
205
- // Access to refresh
206
- expect(tracker.has("id1")).toBe(true);
207
-
208
- // Advance another 75ms (total 125ms from add, but only 75ms from last access)
209
- vi.advanceTimersByTime(75);
210
-
211
- // Should still be valid (refreshed at 50ms)
212
- expect(tracker.peek("id1")).toBe(true);
213
-
214
- tracker.stop();
215
- vi.useRealTimers();
216
- });
217
- });
218
- });
219
-
220
- // ============================================================================
221
- // Metrics Integration Tests
222
- // ============================================================================
223
-
224
- describe("Metrics", () => {
225
- describe("createMetrics", () => {
226
- it("emits metric events to callback", () => {
227
- const { events, metrics } = createCollectingMetrics();
228
-
229
- metrics.emit("event.received");
230
- metrics.emit("event.processed");
231
- metrics.emit("event.duplicate");
232
-
233
- expect(events).toHaveLength(3);
234
- expect(events[0].name).toBe("event.received");
235
- expect(events[1].name).toBe("event.processed");
236
- expect(events[2].name).toBe("event.duplicate");
237
- });
238
-
239
- it("includes labels in metric events", () => {
240
- const { events, metrics } = createCollectingMetrics();
241
-
242
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL });
243
-
244
- expect(events[0].labels).toEqual({ relay: TEST_RELAY_URL });
245
- });
246
-
247
- it("accumulates counters in snapshot", () => {
248
- const metrics = createPlainMetrics();
249
-
250
- metrics.emit("event.received");
251
- metrics.emit("event.received");
252
- metrics.emit("event.processed");
253
- metrics.emit("event.duplicate");
254
- metrics.emit("event.duplicate");
255
- metrics.emit("event.duplicate");
256
-
257
- const snapshot = metrics.getSnapshot();
258
- expect(snapshot.eventsReceived).toBe(2);
259
- expect(snapshot.eventsProcessed).toBe(1);
260
- expect(snapshot.eventsDuplicate).toBe(3);
261
- });
262
-
263
- it("tracks per-relay stats", () => {
264
- const metrics = createPlainMetrics();
265
-
266
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_1 });
267
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_2 });
268
- metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_1 });
269
- metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_1 });
270
-
271
- const snapshot = metrics.getSnapshot();
272
- expect(snapshot.relays[TEST_RELAY_URL_1]).toBeDefined();
273
- expect(snapshot.relays[TEST_RELAY_URL_1].connects).toBe(1);
274
- expect(snapshot.relays[TEST_RELAY_URL_1].errors).toBe(2);
275
- expect(snapshot.relays[TEST_RELAY_URL_2].connects).toBe(1);
276
- expect(snapshot.relays[TEST_RELAY_URL_2].errors).toBe(0);
277
- });
278
-
279
- it("tracks circuit breaker state changes", () => {
280
- const metrics = createPlainMetrics();
281
-
282
- metrics.emit("relay.circuit_breaker.open", 1, { relay: TEST_RELAY_URL_PRIMARY });
283
-
284
- let snapshot = metrics.getSnapshot();
285
- expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerState).toBe("open");
286
- expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerOpens).toBe(1);
287
-
288
- metrics.emit("relay.circuit_breaker.close", 1, { relay: TEST_RELAY_URL_PRIMARY });
289
-
290
- snapshot = metrics.getSnapshot();
291
- expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerState).toBe("closed");
292
- expect(snapshot.relays[TEST_RELAY_URL_PRIMARY].circuitBreakerCloses).toBe(1);
293
- });
294
-
295
- it("tracks all rejection reasons", () => {
296
- const metrics = createPlainMetrics();
297
-
298
- metrics.emit("event.rejected.invalid_shape");
299
- metrics.emit("event.rejected.wrong_kind");
300
- metrics.emit("event.rejected.stale");
301
- metrics.emit("event.rejected.future");
302
- metrics.emit("event.rejected.rate_limited");
303
- metrics.emit("event.rejected.invalid_signature");
304
- metrics.emit("event.rejected.oversized_ciphertext");
305
- metrics.emit("event.rejected.oversized_plaintext");
306
- metrics.emit("event.rejected.decrypt_failed");
307
- metrics.emit("event.rejected.self_message");
308
-
309
- const snapshot = metrics.getSnapshot();
310
- expect(snapshot.eventsRejected.invalidShape).toBe(1);
311
- expect(snapshot.eventsRejected.wrongKind).toBe(1);
312
- expect(snapshot.eventsRejected.stale).toBe(1);
313
- expect(snapshot.eventsRejected.future).toBe(1);
314
- expect(snapshot.eventsRejected.rateLimited).toBe(1);
315
- expect(snapshot.eventsRejected.invalidSignature).toBe(1);
316
- expect(snapshot.eventsRejected.oversizedCiphertext).toBe(1);
317
- expect(snapshot.eventsRejected.oversizedPlaintext).toBe(1);
318
- expect(snapshot.eventsRejected.decryptFailed).toBe(1);
319
- expect(snapshot.eventsRejected.selfMessage).toBe(1);
320
- });
321
-
322
- it("tracks relay message types", () => {
323
- const metrics = createPlainMetrics();
324
-
325
- metrics.emit("relay.message.event", 1, { relay: TEST_RELAY_URL_PRIMARY });
326
- metrics.emit("relay.message.eose", 1, { relay: TEST_RELAY_URL_PRIMARY });
327
- metrics.emit("relay.message.closed", 1, { relay: TEST_RELAY_URL_PRIMARY });
328
- metrics.emit("relay.message.notice", 1, { relay: TEST_RELAY_URL_PRIMARY });
329
- metrics.emit("relay.message.ok", 1, { relay: TEST_RELAY_URL_PRIMARY });
330
- metrics.emit("relay.message.auth", 1, { relay: TEST_RELAY_URL_PRIMARY });
331
-
332
- const snapshot = metrics.getSnapshot();
333
- const relay = snapshot.relays[TEST_RELAY_URL_PRIMARY];
334
- expect(relay.messagesReceived.event).toBe(1);
335
- expect(relay.messagesReceived.eose).toBe(1);
336
- expect(relay.messagesReceived.closed).toBe(1);
337
- expect(relay.messagesReceived.notice).toBe(1);
338
- expect(relay.messagesReceived.ok).toBe(1);
339
- expect(relay.messagesReceived.auth).toBe(1);
340
- });
341
-
342
- it("tracks decrypt success/failure", () => {
343
- const metrics = createPlainMetrics();
344
-
345
- metrics.emit("decrypt.success");
346
- metrics.emit("decrypt.success");
347
- metrics.emit("decrypt.failure");
348
-
349
- const snapshot = metrics.getSnapshot();
350
- expect(snapshot.decrypt.success).toBe(2);
351
- expect(snapshot.decrypt.failure).toBe(1);
352
- });
353
-
354
- it("tracks memory gauges (replaces rather than accumulates)", () => {
355
- const metrics = createPlainMetrics();
356
-
357
- metrics.emit("memory.seen_tracker_size", 100);
358
- metrics.emit("memory.seen_tracker_size", 150);
359
- metrics.emit("memory.seen_tracker_size", 125);
360
-
361
- const snapshot = metrics.getSnapshot();
362
- expect(snapshot.memory.seenTrackerSize).toBe(125); // Last value, not sum
363
- });
364
-
365
- it("reset clears all counters", () => {
366
- const metrics = createPlainMetrics();
367
-
368
- metrics.emit("event.received");
369
- metrics.emit("event.processed");
370
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_PRIMARY });
371
-
372
- metrics.reset();
373
-
374
- const snapshot = metrics.getSnapshot();
375
- expect(snapshot.eventsReceived).toBe(0);
376
- expect(snapshot.eventsProcessed).toBe(0);
377
- expect(Object.keys(snapshot.relays)).toHaveLength(0);
378
- });
379
- });
380
-
381
- describe("createNoopMetrics", () => {
382
- it("does not throw on emit", () => {
383
- const metrics = createNoopMetrics();
384
-
385
- expect(() => {
386
- metrics.emit("event.received");
387
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_PRIMARY });
388
- }).not.toThrow();
389
- });
390
-
391
- it("returns empty snapshot", () => {
392
- const metrics = createNoopMetrics();
393
-
394
- const snapshot = metrics.getSnapshot();
395
- expect(snapshot.eventsReceived).toBe(0);
396
- expect(snapshot.eventsProcessed).toBe(0);
397
- });
398
- });
399
- });
400
-
401
- // ============================================================================
402
- // Circuit Breaker Behavior Tests
403
- // ============================================================================
404
-
405
- describe("Circuit Breaker Behavior", () => {
406
- // Test the circuit breaker logic through metrics emissions
407
- it("emits circuit breaker metrics in correct sequence", () => {
408
- const { events, metrics } = createCollectingMetrics();
409
-
410
- // Simulate 5 failures -> open
411
- for (let i = 0; i < 5; i++) {
412
- metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_PRIMARY });
413
- }
414
- metrics.emit("relay.circuit_breaker.open", 1, { relay: TEST_RELAY_URL_PRIMARY });
415
-
416
- // Simulate recovery
417
- metrics.emit("relay.circuit_breaker.half_open", 1, { relay: TEST_RELAY_URL_PRIMARY });
418
- metrics.emit("relay.circuit_breaker.close", 1, { relay: TEST_RELAY_URL_PRIMARY });
419
-
420
- const cbEvents = events.filter((e) => e.name.startsWith("relay.circuit_breaker"));
421
- expect(cbEvents).toHaveLength(3);
422
- expect(cbEvents[0].name).toBe("relay.circuit_breaker.open");
423
- expect(cbEvents[1].name).toBe("relay.circuit_breaker.half_open");
424
- expect(cbEvents[2].name).toBe("relay.circuit_breaker.close");
425
- });
426
- });
427
-
428
- // ============================================================================
429
- // Health Scoring Behavior Tests
430
- // ============================================================================
431
-
432
- describe("Health Scoring", () => {
433
- it("metrics track relay errors for health scoring", () => {
434
- const metrics = createPlainMetrics();
435
-
436
- // Simulate mixed success/failure pattern
437
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_GOOD });
438
- metrics.emit("relay.connect", 1, { relay: TEST_RELAY_URL_BAD });
439
-
440
- metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
441
- metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
442
- metrics.emit("relay.error", 1, { relay: TEST_RELAY_URL_BAD });
443
-
444
- const snapshot = metrics.getSnapshot();
445
- expect(snapshot.relays[TEST_RELAY_URL_GOOD].errors).toBe(0);
446
- expect(snapshot.relays[TEST_RELAY_URL_BAD].errors).toBe(3);
447
- });
448
- });
449
-
450
- // ============================================================================
451
- // Reconnect Backoff Tests
452
- // ============================================================================
453
-
454
- describe("Reconnect Backoff", () => {
455
- it("computes delays within expected bounds", () => {
456
- // Compute expected delays (1s, 2s, 4s, 8s, 16s, 32s, 60s cap)
457
- const BASE = 1000;
458
- const MAX = 60000;
459
- const JITTER = 0.3;
460
-
461
- for (let attempt = 0; attempt < 10; attempt++) {
462
- const exponential = BASE * 2 ** attempt;
463
- const capped = Math.min(exponential, MAX);
464
- const minDelay = capped * (1 - JITTER);
465
- const maxDelay = capped * (1 + JITTER);
466
-
467
- // These are the expected bounds
468
- expect(minDelay).toBeGreaterThanOrEqual(BASE * 0.7);
469
- expect(maxDelay).toBeLessThanOrEqual(MAX * 1.3);
470
- }
471
- });
472
- });
@@ -1,190 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- validatePrivateKey,
4
- getPublicKeyFromPrivate,
5
- isValidPubkey,
6
- normalizePubkey,
7
- pubkeyToNpub,
8
- } from "./nostr-key-utils.js";
9
- import { TEST_HEX_PRIVATE_KEY, TEST_NSEC } from "./test-fixtures.js";
10
-
11
- describe("validatePrivateKey", () => {
12
- describe("hex format", () => {
13
- it("accepts valid 64-char hex key", () => {
14
- const result = validatePrivateKey(TEST_HEX_PRIVATE_KEY);
15
- expect(result).toBeInstanceOf(Uint8Array);
16
- expect(result.length).toBe(32);
17
- });
18
-
19
- it("accepts lowercase hex", () => {
20
- const result = validatePrivateKey(TEST_HEX_PRIVATE_KEY.toLowerCase());
21
- expect(result).toBeInstanceOf(Uint8Array);
22
- });
23
-
24
- it("accepts uppercase hex", () => {
25
- const result = validatePrivateKey(TEST_HEX_PRIVATE_KEY.toUpperCase());
26
- expect(result).toBeInstanceOf(Uint8Array);
27
- });
28
-
29
- it("accepts mixed case hex", () => {
30
- const mixed = "0123456789ABCdef0123456789abcDEF0123456789abcdef0123456789ABCDEF";
31
- const result = validatePrivateKey(mixed);
32
- expect(result).toBeInstanceOf(Uint8Array);
33
- });
34
-
35
- it("trims whitespace", () => {
36
- const result = validatePrivateKey(` ${TEST_HEX_PRIVATE_KEY} `);
37
- expect(result).toBeInstanceOf(Uint8Array);
38
- });
39
-
40
- it("trims newlines", () => {
41
- const result = validatePrivateKey(`${TEST_HEX_PRIVATE_KEY}\n`);
42
- expect(result).toBeInstanceOf(Uint8Array);
43
- });
44
-
45
- it("rejects 63-char hex (too short)", () => {
46
- expect(() => validatePrivateKey(TEST_HEX_PRIVATE_KEY.slice(0, 63))).toThrow(
47
- "Private key must be 64 hex characters",
48
- );
49
- });
50
-
51
- it("rejects 65-char hex (too long)", () => {
52
- expect(() => validatePrivateKey(TEST_HEX_PRIVATE_KEY + "0")).toThrow(
53
- "Private key must be 64 hex characters",
54
- );
55
- });
56
-
57
- it("rejects non-hex characters", () => {
58
- const invalid = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg"; // 'g' at end
59
- expect(() => validatePrivateKey(invalid)).toThrow("Private key must be 64 hex characters");
60
- });
61
-
62
- it("rejects empty string", () => {
63
- expect(() => validatePrivateKey("")).toThrow("Private key must be 64 hex characters");
64
- });
65
-
66
- it("rejects whitespace-only string", () => {
67
- expect(() => validatePrivateKey(" ")).toThrow("Private key must be 64 hex characters");
68
- });
69
-
70
- it("rejects key with 0x prefix", () => {
71
- expect(() => validatePrivateKey("0x" + TEST_HEX_PRIVATE_KEY)).toThrow(
72
- "Private key must be 64 hex characters",
73
- );
74
- });
75
- });
76
-
77
- describe("nsec format", () => {
78
- it("rejects invalid nsec (wrong checksum)", () => {
79
- const badNsec = "nsec1invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalid";
80
- expect(() => validatePrivateKey(badNsec)).toThrow();
81
- });
82
-
83
- it("rejects npub (wrong type)", () => {
84
- const npub = "npub1qypqxpq9qtpqscx7peytzfwtdjmcv0mrz5rjpej8vjppfkqfqy8s5epk55";
85
- expect(() => validatePrivateKey(npub)).toThrow();
86
- });
87
- });
88
- });
89
-
90
- describe("isValidPubkey", () => {
91
- describe("hex format", () => {
92
- it("accepts valid 64-char hex pubkey", () => {
93
- expect(isValidPubkey(TEST_HEX_PRIVATE_KEY)).toBe(true);
94
- });
95
-
96
- it("accepts uppercase hex", () => {
97
- const validHex = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
98
- expect(isValidPubkey(validHex)).toBe(true);
99
- });
100
-
101
- it("rejects 63-char hex", () => {
102
- const shortHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde";
103
- expect(isValidPubkey(shortHex)).toBe(false);
104
- });
105
-
106
- it("rejects 65-char hex", () => {
107
- const longHex = `${TEST_HEX_PRIVATE_KEY}0`;
108
- expect(isValidPubkey(longHex)).toBe(false);
109
- });
110
-
111
- it("rejects non-hex characters", () => {
112
- const invalid = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg";
113
- expect(isValidPubkey(invalid)).toBe(false);
114
- });
115
- });
116
-
117
- describe("npub format", () => {
118
- it("rejects invalid npub", () => {
119
- expect(isValidPubkey("npub1invalid")).toBe(false);
120
- });
121
-
122
- it("rejects nsec (wrong type)", () => {
123
- expect(isValidPubkey(TEST_NSEC)).toBe(false);
124
- });
125
- });
126
-
127
- describe("edge cases", () => {
128
- it("rejects empty string", () => {
129
- expect(isValidPubkey("")).toBe(false);
130
- });
131
-
132
- it("handles whitespace-padded input", () => {
133
- expect(isValidPubkey(` ${TEST_HEX_PRIVATE_KEY} `)).toBe(true);
134
- });
135
- });
136
- });
137
-
138
- describe("normalizePubkey", () => {
139
- describe("hex format", () => {
140
- it("lowercases hex pubkey", () => {
141
- const upper = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
142
- const result = normalizePubkey(upper);
143
- expect(result).toBe(upper.toLowerCase());
144
- });
145
-
146
- it("trims whitespace", () => {
147
- expect(normalizePubkey(` ${TEST_HEX_PRIVATE_KEY} `)).toBe(TEST_HEX_PRIVATE_KEY);
148
- });
149
-
150
- it("rejects invalid hex", () => {
151
- expect(() => normalizePubkey("invalid")).toThrow("Pubkey must be 64 hex characters");
152
- });
153
- });
154
- });
155
-
156
- describe("getPublicKeyFromPrivate", () => {
157
- it("derives public key from hex private key", () => {
158
- const pubkey = getPublicKeyFromPrivate(TEST_HEX_PRIVATE_KEY);
159
- expect(pubkey).toMatch(/^[0-9a-f]{64}$/);
160
- expect(pubkey.length).toBe(64);
161
- });
162
-
163
- it("derives consistent public key", () => {
164
- const pubkey1 = getPublicKeyFromPrivate(TEST_HEX_PRIVATE_KEY);
165
- const pubkey2 = getPublicKeyFromPrivate(TEST_HEX_PRIVATE_KEY);
166
- expect(pubkey1).toBe(pubkey2);
167
- });
168
-
169
- it("throws for invalid private key", () => {
170
- expect(() => getPublicKeyFromPrivate("invalid")).toThrow();
171
- });
172
- });
173
-
174
- describe("pubkeyToNpub", () => {
175
- it("converts hex pubkey to npub format", () => {
176
- const npub = pubkeyToNpub(TEST_HEX_PRIVATE_KEY);
177
- expect(npub).toMatch(/^npub1[a-z0-9]+$/);
178
- });
179
-
180
- it("produces consistent output", () => {
181
- const npub1 = pubkeyToNpub(TEST_HEX_PRIVATE_KEY);
182
- const npub2 = pubkeyToNpub(TEST_HEX_PRIVATE_KEY);
183
- expect(npub1).toBe(npub2);
184
- });
185
-
186
- it("normalizes uppercase hex first", () => {
187
- const upper = TEST_HEX_PRIVATE_KEY.toUpperCase();
188
- expect(pubkeyToNpub(TEST_HEX_PRIVATE_KEY)).toBe(pubkeyToNpub(upper));
189
- });
190
- });