@openclaw/nostr 2026.3.12 → 2026.5.1-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 (48) hide show
  1. package/README.md +6 -0
  2. package/api.ts +10 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +60 -36
  5. package/openclaw.plugin.json +190 -1
  6. package/package.json +41 -9
  7. package/runtime-api.ts +6 -0
  8. package/setup-api.ts +1 -0
  9. package/setup-entry.ts +9 -0
  10. package/setup-plugin-api.ts +3 -0
  11. package/src/channel-api.ts +15 -0
  12. package/src/channel.inbound.test.ts +176 -0
  13. package/src/channel.outbound.test.ts +89 -49
  14. package/src/channel.setup.ts +231 -0
  15. package/src/channel.test.ts +439 -71
  16. package/src/channel.ts +146 -284
  17. package/src/config-schema.ts +18 -12
  18. package/src/default-relays.ts +1 -0
  19. package/src/gateway.ts +302 -0
  20. package/src/inbound-direct-dm-runtime.ts +1 -0
  21. package/src/metrics.ts +6 -6
  22. package/src/nostr-bus.fuzz.test.ts +74 -247
  23. package/src/nostr-bus.inbound.test.ts +526 -0
  24. package/src/nostr-bus.integration.test.ts +88 -64
  25. package/src/nostr-bus.test.ts +22 -31
  26. package/src/nostr-bus.ts +206 -136
  27. package/src/nostr-key-utils.ts +94 -0
  28. package/src/nostr-profile-core.ts +134 -0
  29. package/src/nostr-profile-http-runtime.ts +6 -0
  30. package/src/nostr-profile-http.test.ts +310 -192
  31. package/src/nostr-profile-http.ts +51 -36
  32. package/src/nostr-profile-import.ts +3 -3
  33. package/src/nostr-profile-url-safety.ts +21 -0
  34. package/src/nostr-profile.fuzz.test.ts +7 -57
  35. package/src/nostr-profile.test.ts +16 -14
  36. package/src/nostr-profile.ts +13 -146
  37. package/src/nostr-state-store.test.ts +106 -2
  38. package/src/nostr-state-store.ts +46 -49
  39. package/src/runtime.ts +6 -3
  40. package/src/seen-tracker.ts +1 -1
  41. package/src/session-route.ts +25 -0
  42. package/src/setup-surface.ts +265 -0
  43. package/src/test-fixtures.ts +45 -0
  44. package/src/types.ts +26 -25
  45. package/test-api.ts +1 -0
  46. package/tsconfig.json +16 -0
  47. package/CHANGELOG.md +0 -110
  48. package/src/types.test.ts +0 -175
@@ -1,7 +1,24 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { createMetrics, type MetricName } from "./metrics.js";
3
- import { validatePrivateKey, isValidPubkey, normalizePubkey } from "./nostr-bus.js";
3
+ import { validatePrivateKey, isValidPubkey, normalizePubkey } from "./nostr-key-utils.js";
4
4
  import { createSeenTracker } from "./seen-tracker.js";
5
+ import { TEST_HEX_PRIVATE_KEY } from "./test-fixtures.js";
6
+
7
+ function createTracker(maxEntries = 100) {
8
+ return createSeenTracker({ maxEntries });
9
+ }
10
+
11
+ function createPlainMetrics() {
12
+ return createMetrics();
13
+ }
14
+
15
+ function createCollectingMetrics() {
16
+ const events: unknown[] = [];
17
+ return {
18
+ events,
19
+ metrics: createMetrics((event) => events.push(event)),
20
+ };
21
+ }
5
22
 
6
23
  // ============================================================================
7
24
  // Fuzz Tests for validatePrivateKey
@@ -9,90 +26,31 @@ import { createSeenTracker } from "./seen-tracker.js";
9
26
 
10
27
  describe("validatePrivateKey fuzz", () => {
11
28
  describe("type confusion", () => {
12
- it("rejects null input", () => {
13
- expect(() => validatePrivateKey(null as unknown as string)).toThrow();
14
- });
15
-
16
- it("rejects undefined input", () => {
17
- expect(() => validatePrivateKey(undefined as unknown as string)).toThrow();
18
- });
19
-
20
- it("rejects number input", () => {
21
- expect(() => validatePrivateKey(123 as unknown as string)).toThrow();
22
- });
23
-
24
- it("rejects boolean input", () => {
25
- expect(() => validatePrivateKey(true as unknown as string)).toThrow();
26
- });
27
-
28
- it("rejects object input", () => {
29
- expect(() => validatePrivateKey({} as unknown as string)).toThrow();
30
- });
31
-
32
- it("rejects array input", () => {
33
- expect(() => validatePrivateKey([] as unknown as string)).toThrow();
34
- });
35
-
36
- it("rejects function input", () => {
37
- expect(() => validatePrivateKey((() => {}) as unknown as string)).toThrow();
29
+ it("rejects non-string input", () => {
30
+ for (const value of [null, undefined, 123, true, {}, [], () => {}]) {
31
+ expect(() => validatePrivateKey(value as unknown as string)).toThrow();
32
+ }
38
33
  });
39
34
  });
40
35
 
41
36
  describe("unicode attacks", () => {
42
- it("rejects unicode lookalike characters", () => {
43
- // Using zero-width characters
44
- const withZeroWidth =
45
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u200Bf";
46
- expect(() => validatePrivateKey(withZeroWidth)).toThrow();
47
- });
48
-
49
- it("rejects RTL override", () => {
50
- const withRtl = "\u202E0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
51
- expect(() => validatePrivateKey(withRtl)).toThrow();
52
- });
53
-
54
- it("rejects homoglyph 'a' (Cyrillic а)", () => {
55
- // Using Cyrillic 'а' (U+0430) instead of Latin 'a'
56
- const withCyrillicA = "0123456789\u0430bcdef0123456789abcdef0123456789abcdef0123456789abcdef";
57
- expect(() => validatePrivateKey(withCyrillicA)).toThrow();
58
- });
59
-
60
- it("rejects emoji", () => {
61
- const withEmoji = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab😀";
62
- expect(() => validatePrivateKey(withEmoji)).toThrow();
63
- });
64
-
65
- it("rejects combining characters", () => {
66
- // 'a' followed by combining acute accent
67
- const withCombining = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u0301";
68
- expect(() => validatePrivateKey(withCombining)).toThrow();
69
- });
70
- });
71
-
72
- describe("injection attempts", () => {
73
- it("rejects null byte injection", () => {
74
- const withNullByte = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\x00f";
75
- expect(() => validatePrivateKey(withNullByte)).toThrow();
76
- });
77
-
78
- it("rejects newline injection", () => {
79
- const withNewline = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\nf";
80
- expect(() => validatePrivateKey(withNewline)).toThrow();
81
- });
82
-
83
- it("rejects carriage return injection", () => {
84
- const withCR = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\rf";
85
- expect(() => validatePrivateKey(withCR)).toThrow();
86
- });
87
-
88
- it("rejects tab injection", () => {
89
- const withTab = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\tf";
90
- expect(() => validatePrivateKey(withTab)).toThrow();
91
- });
37
+ it("rejects unicode and control-character attacks", () => {
38
+ const invalidKeys = [
39
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u200Bf",
40
+ `\u202E${TEST_HEX_PRIVATE_KEY}`,
41
+ "0123456789\u0430bcdef0123456789abcdef0123456789abcdef0123456789abcdef",
42
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab😀",
43
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\u0301",
44
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\x00f",
45
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\nf",
46
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\rf",
47
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\tf",
48
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\ff",
49
+ ];
92
50
 
93
- it("rejects form feed injection", () => {
94
- const withFormFeed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde\ff";
95
- expect(() => validatePrivateKey(withFormFeed)).toThrow();
51
+ for (const key of invalidKeys) {
52
+ expect(() => validatePrivateKey(key)).toThrow();
53
+ }
96
54
  });
97
55
  });
98
56
 
@@ -137,34 +95,18 @@ describe("validatePrivateKey fuzz", () => {
137
95
 
138
96
  describe("isValidPubkey fuzz", () => {
139
97
  describe("type confusion", () => {
140
- it("handles null gracefully", () => {
141
- expect(isValidPubkey(null as unknown as string)).toBe(false);
142
- });
143
-
144
- it("handles undefined gracefully", () => {
145
- expect(isValidPubkey(undefined as unknown as string)).toBe(false);
146
- });
147
-
148
- it("handles number gracefully", () => {
149
- expect(isValidPubkey(123 as unknown as string)).toBe(false);
150
- });
151
-
152
- it("handles object gracefully", () => {
153
- expect(isValidPubkey({} as unknown as string)).toBe(false);
98
+ it("handles non-string input gracefully", () => {
99
+ for (const value of [null, undefined, 123, {}]) {
100
+ expect(isValidPubkey(value as unknown as string)).toBe(false);
101
+ }
154
102
  });
155
103
  });
156
104
 
157
105
  describe("malicious inputs", () => {
158
- it("rejects __proto__ key", () => {
159
- expect(isValidPubkey("__proto__")).toBe(false);
160
- });
161
-
162
- it("rejects constructor key", () => {
163
- expect(isValidPubkey("constructor")).toBe(false);
164
- });
165
-
166
- it("rejects toString key", () => {
167
- expect(isValidPubkey("toString")).toBe(false);
106
+ it("rejects prototype property names", () => {
107
+ for (const value of ["__proto__", "constructor", "toString"]) {
108
+ expect(isValidPubkey(value)).toBe(false);
109
+ }
168
110
  });
169
111
  });
170
112
  });
@@ -175,30 +117,22 @@ describe("isValidPubkey fuzz", () => {
175
117
 
176
118
  describe("normalizePubkey fuzz", () => {
177
119
  describe("prototype pollution attempts", () => {
178
- it("throws for __proto__", () => {
179
- expect(() => normalizePubkey("__proto__")).toThrow();
180
- });
181
-
182
- it("throws for constructor", () => {
183
- expect(() => normalizePubkey("constructor")).toThrow();
184
- });
185
-
186
- it("throws for prototype", () => {
187
- expect(() => normalizePubkey("prototype")).toThrow();
120
+ it("throws for prototype property names", () => {
121
+ for (const value of ["__proto__", "constructor", "prototype"]) {
122
+ expect(() => normalizePubkey(value)).toThrow();
123
+ }
188
124
  });
189
125
  });
190
126
 
191
127
  describe("case sensitivity", () => {
192
128
  it("normalizes uppercase to lowercase", () => {
193
129
  const upper = "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF";
194
- const lower = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
195
- expect(normalizePubkey(upper)).toBe(lower);
130
+ expect(normalizePubkey(upper)).toBe(TEST_HEX_PRIVATE_KEY);
196
131
  });
197
132
 
198
133
  it("normalizes mixed case to lowercase", () => {
199
134
  const mixed = "0123456789AbCdEf0123456789AbCdEf0123456789AbCdEf0123456789AbCdEf";
200
- const lower = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
201
- expect(normalizePubkey(mixed)).toBe(lower);
135
+ expect(normalizePubkey(mixed)).toBe(TEST_HEX_PRIVATE_KEY);
202
136
  });
203
137
  });
204
138
  });
@@ -210,14 +144,14 @@ describe("normalizePubkey fuzz", () => {
210
144
  describe("SeenTracker fuzz", () => {
211
145
  describe("malformed IDs", () => {
212
146
  it("handles empty string IDs", () => {
213
- const tracker = createSeenTracker({ maxEntries: 100 });
147
+ const tracker = createTracker();
214
148
  expect(() => tracker.add("")).not.toThrow();
215
149
  expect(tracker.peek("")).toBe(true);
216
150
  tracker.stop();
217
151
  });
218
152
 
219
153
  it("handles very long IDs", () => {
220
- const tracker = createSeenTracker({ maxEntries: 100 });
154
+ const tracker = createTracker();
221
155
  const longId = "a".repeat(100000);
222
156
  expect(() => tracker.add(longId)).not.toThrow();
223
157
  expect(tracker.peek(longId)).toBe(true);
@@ -225,7 +159,7 @@ describe("SeenTracker fuzz", () => {
225
159
  });
226
160
 
227
161
  it("handles unicode IDs", () => {
228
- const tracker = createSeenTracker({ maxEntries: 100 });
162
+ const tracker = createTracker();
229
163
  const unicodeId = "事件ID_🎉_тест";
230
164
  expect(() => tracker.add(unicodeId)).not.toThrow();
231
165
  expect(tracker.peek(unicodeId)).toBe(true);
@@ -233,7 +167,7 @@ describe("SeenTracker fuzz", () => {
233
167
  });
234
168
 
235
169
  it("handles IDs with null bytes", () => {
236
- const tracker = createSeenTracker({ maxEntries: 100 });
170
+ const tracker = createTracker();
237
171
  const idWithNull = "event\x00id";
238
172
  expect(() => tracker.add(idWithNull)).not.toThrow();
239
173
  expect(tracker.peek(idWithNull)).toBe(true);
@@ -241,7 +175,7 @@ describe("SeenTracker fuzz", () => {
241
175
  });
242
176
 
243
177
  it("handles prototype property names as IDs", () => {
244
- const tracker = createSeenTracker({ maxEntries: 100 });
178
+ const tracker = createTracker();
245
179
 
246
180
  // These should not affect the tracker's internal operation
247
181
  expect(() => tracker.add("__proto__")).not.toThrow();
@@ -260,7 +194,7 @@ describe("SeenTracker fuzz", () => {
260
194
 
261
195
  describe("rapid operations", () => {
262
196
  it("handles rapid add/check cycles", () => {
263
- const tracker = createSeenTracker({ maxEntries: 1000 });
197
+ const tracker = createTracker(1000);
264
198
 
265
199
  for (let i = 0; i < 10000; i++) {
266
200
  const id = `event-${i}`;
@@ -277,7 +211,7 @@ describe("SeenTracker fuzz", () => {
277
211
  });
278
212
 
279
213
  it("handles concurrent-style operations", () => {
280
- const tracker = createSeenTracker({ maxEntries: 100 });
214
+ const tracker = createTracker();
281
215
 
282
216
  // Simulate interleaved operations
283
217
  for (let i = 0; i < 100; i++) {
@@ -296,21 +230,21 @@ describe("SeenTracker fuzz", () => {
296
230
 
297
231
  describe("seed edge cases", () => {
298
232
  it("handles empty seed array", () => {
299
- const tracker = createSeenTracker({ maxEntries: 100 });
233
+ const tracker = createTracker();
300
234
  expect(() => tracker.seed([])).not.toThrow();
301
235
  expect(tracker.size()).toBe(0);
302
236
  tracker.stop();
303
237
  });
304
238
 
305
239
  it("handles seed with duplicate IDs", () => {
306
- const tracker = createSeenTracker({ maxEntries: 100 });
240
+ const tracker = createTracker();
307
241
  tracker.seed(["id1", "id1", "id1", "id2", "id2"]);
308
242
  expect(tracker.size()).toBe(2);
309
243
  tracker.stop();
310
244
  });
311
245
 
312
246
  it("handles seed larger than maxEntries", () => {
313
- const tracker = createSeenTracker({ maxEntries: 5 });
247
+ const tracker = createTracker(5);
314
248
  const ids = Array.from({ length: 100 }, (_, i) => `id-${i}`);
315
249
  tracker.seed(ids);
316
250
  expect(tracker.size()).toBeLessThanOrEqual(5);
@@ -326,7 +260,7 @@ describe("SeenTracker fuzz", () => {
326
260
  describe("Metrics fuzz", () => {
327
261
  describe("invalid metric names", () => {
328
262
  it("handles unknown metric names gracefully", () => {
329
- const metrics = createMetrics();
263
+ const metrics = createPlainMetrics();
330
264
 
331
265
  // Cast to bypass type checking - testing runtime behavior
332
266
  expect(() => {
@@ -337,21 +271,21 @@ describe("Metrics fuzz", () => {
337
271
 
338
272
  describe("invalid label values", () => {
339
273
  it("handles null relay label", () => {
340
- const metrics = createMetrics();
274
+ const metrics = createPlainMetrics();
341
275
  expect(() => {
342
276
  metrics.emit("relay.connect", 1, { relay: null as unknown as string });
343
277
  }).not.toThrow();
344
278
  });
345
279
 
346
280
  it("handles undefined relay label", () => {
347
- const metrics = createMetrics();
281
+ const metrics = createPlainMetrics();
348
282
  expect(() => {
349
283
  metrics.emit("relay.connect", 1, { relay: undefined as unknown as string });
350
284
  }).not.toThrow();
351
285
  });
352
286
 
353
287
  it("handles very long relay URL", () => {
354
- const metrics = createMetrics();
288
+ const metrics = createPlainMetrics();
355
289
  const longUrl = "wss://" + "a".repeat(10000) + ".com";
356
290
  expect(() => {
357
291
  metrics.emit("relay.connect", 1, { relay: longUrl });
@@ -364,15 +298,15 @@ describe("Metrics fuzz", () => {
364
298
 
365
299
  describe("extreme values", () => {
366
300
  it("handles NaN value", () => {
367
- const metrics = createMetrics();
368
- expect(() => metrics.emit("event.received", NaN)).not.toThrow();
301
+ const metrics = createPlainMetrics();
302
+ expect(() => metrics.emit("event.received", Number.NaN)).not.toThrow();
369
303
 
370
304
  const snapshot = metrics.getSnapshot();
371
- expect(isNaN(snapshot.eventsReceived)).toBe(true);
305
+ expect(Number.isNaN(snapshot.eventsReceived)).toBe(true);
372
306
  });
373
307
 
374
308
  it("handles Infinity value", () => {
375
- const metrics = createMetrics();
309
+ const metrics = createPlainMetrics();
376
310
  expect(() => metrics.emit("event.received", Infinity)).not.toThrow();
377
311
 
378
312
  const snapshot = metrics.getSnapshot();
@@ -380,7 +314,7 @@ describe("Metrics fuzz", () => {
380
314
  });
381
315
 
382
316
  it("handles negative value", () => {
383
- const metrics = createMetrics();
317
+ const metrics = createPlainMetrics();
384
318
  metrics.emit("event.received", -1);
385
319
 
386
320
  const snapshot = metrics.getSnapshot();
@@ -388,7 +322,7 @@ describe("Metrics fuzz", () => {
388
322
  });
389
323
 
390
324
  it("handles very large value", () => {
391
- const metrics = createMetrics();
325
+ const metrics = createPlainMetrics();
392
326
  metrics.emit("event.received", Number.MAX_SAFE_INTEGER);
393
327
 
394
328
  const snapshot = metrics.getSnapshot();
@@ -398,8 +332,7 @@ describe("Metrics fuzz", () => {
398
332
 
399
333
  describe("rapid emissions", () => {
400
334
  it("handles many rapid emissions", () => {
401
- const events: unknown[] = [];
402
- const metrics = createMetrics((e) => events.push(e));
335
+ const { events, metrics } = createCollectingMetrics();
403
336
 
404
337
  for (let i = 0; i < 10000; i++) {
405
338
  metrics.emit("event.received");
@@ -413,7 +346,7 @@ describe("Metrics fuzz", () => {
413
346
 
414
347
  describe("reset during operation", () => {
415
348
  it("handles reset mid-operation safely", () => {
416
- const metrics = createMetrics();
349
+ const metrics = createPlainMetrics();
417
350
 
418
351
  metrics.emit("event.received");
419
352
  metrics.emit("event.received");
@@ -425,109 +358,3 @@ describe("Metrics fuzz", () => {
425
358
  });
426
359
  });
427
360
  });
428
-
429
- // ============================================================================
430
- // Event Shape Validation (simulating malformed events)
431
- // ============================================================================
432
-
433
- describe("Event shape validation", () => {
434
- describe("malformed event structures", () => {
435
- // These test what happens if malformed data somehow gets through
436
-
437
- it("identifies missing required fields", () => {
438
- const malformedEvents = [
439
- {}, // empty
440
- { id: "abc" }, // missing pubkey, created_at, etc.
441
- { id: null, pubkey: null }, // null values
442
- { id: 123, pubkey: 456 }, // wrong types
443
- { tags: "not-an-array" }, // wrong type for tags
444
- { tags: [[1, 2, 3]] }, // wrong type for tag elements
445
- ];
446
-
447
- for (const event of malformedEvents) {
448
- // These should be caught by shape validation before processing
449
- const hasId = typeof event?.id === "string";
450
- const hasPubkey = typeof (event as { pubkey?: unknown })?.pubkey === "string";
451
- const hasTags = Array.isArray((event as { tags?: unknown })?.tags);
452
-
453
- // At least one should be invalid
454
- expect(hasId && hasPubkey && hasTags).toBe(false);
455
- }
456
- });
457
- });
458
-
459
- describe("timestamp edge cases", () => {
460
- const testTimestamps = [
461
- { value: NaN, desc: "NaN" },
462
- { value: Infinity, desc: "Infinity" },
463
- { value: -Infinity, desc: "-Infinity" },
464
- { value: -1, desc: "negative" },
465
- { value: 0, desc: "zero" },
466
- { value: 253402300800, desc: "year 10000" }, // Far future
467
- { value: -62135596800, desc: "year 0001" }, // Far past
468
- { value: 1.5, desc: "float" },
469
- ];
470
-
471
- for (const { value, desc } of testTimestamps) {
472
- it(`handles ${desc} timestamp`, () => {
473
- const isValidTimestamp =
474
- typeof value === "number" &&
475
- !isNaN(value) &&
476
- isFinite(value) &&
477
- value >= 0 &&
478
- Number.isInteger(value);
479
-
480
- // Timestamps should be validated as positive integers
481
- if (["NaN", "Infinity", "-Infinity", "negative", "float"].includes(desc)) {
482
- expect(isValidTimestamp).toBe(false);
483
- }
484
- });
485
- }
486
- });
487
- });
488
-
489
- // ============================================================================
490
- // JSON parsing edge cases (simulating relay responses)
491
- // ============================================================================
492
-
493
- describe("JSON parsing edge cases", () => {
494
- const malformedJsonCases = [
495
- { input: "", desc: "empty string" },
496
- { input: "null", desc: "null literal" },
497
- { input: "undefined", desc: "undefined literal" },
498
- { input: "{", desc: "incomplete object" },
499
- { input: "[", desc: "incomplete array" },
500
- { input: '{"key": undefined}', desc: "undefined value" },
501
- { input: "{'key': 'value'}", desc: "single quotes" },
502
- { input: '{"key": NaN}', desc: "NaN value" },
503
- { input: '{"key": Infinity}', desc: "Infinity value" },
504
- { input: "\x00", desc: "null byte" },
505
- { input: "abc", desc: "plain string" },
506
- { input: "123", desc: "plain number" },
507
- ];
508
-
509
- for (const { input, desc } of malformedJsonCases) {
510
- it(`handles malformed JSON: ${desc}`, () => {
511
- let parsed: unknown;
512
- let parseError = false;
513
-
514
- try {
515
- parsed = JSON.parse(input);
516
- } catch {
517
- parseError = true;
518
- }
519
-
520
- // Either it throws or produces something that needs validation
521
- if (!parseError) {
522
- // If it parsed, we need to validate the structure
523
- const isValidRelayMessage =
524
- Array.isArray(parsed) && parsed.length >= 2 && typeof parsed[0] === "string";
525
-
526
- // Most malformed cases won't produce valid relay messages
527
- if (["null literal", "plain number", "plain string"].includes(desc)) {
528
- expect(isValidRelayMessage).toBe(false);
529
- }
530
- }
531
- });
532
- }
533
- });