@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.
- package/README.md +6 -0
- package/api.ts +10 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +60 -36
- package/openclaw.plugin.json +190 -1
- package/package.json +41 -9
- package/runtime-api.ts +6 -0
- package/setup-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/channel-api.ts +15 -0
- package/src/channel.inbound.test.ts +176 -0
- package/src/channel.outbound.test.ts +89 -49
- package/src/channel.setup.ts +231 -0
- package/src/channel.test.ts +439 -71
- package/src/channel.ts +146 -284
- package/src/config-schema.ts +18 -12
- package/src/default-relays.ts +1 -0
- package/src/gateway.ts +302 -0
- package/src/inbound-direct-dm-runtime.ts +1 -0
- package/src/metrics.ts +6 -6
- package/src/nostr-bus.fuzz.test.ts +74 -247
- package/src/nostr-bus.inbound.test.ts +526 -0
- package/src/nostr-bus.integration.test.ts +88 -64
- package/src/nostr-bus.test.ts +22 -31
- package/src/nostr-bus.ts +206 -136
- package/src/nostr-key-utils.ts +94 -0
- package/src/nostr-profile-core.ts +134 -0
- package/src/nostr-profile-http-runtime.ts +6 -0
- package/src/nostr-profile-http.test.ts +310 -192
- package/src/nostr-profile-http.ts +51 -36
- package/src/nostr-profile-import.ts +3 -3
- package/src/nostr-profile-url-safety.ts +21 -0
- package/src/nostr-profile.fuzz.test.ts +7 -57
- package/src/nostr-profile.test.ts +16 -14
- package/src/nostr-profile.ts +13 -146
- package/src/nostr-state-store.test.ts +106 -2
- package/src/nostr-state-store.ts +46 -49
- package/src/runtime.ts +6 -3
- package/src/seen-tracker.ts +1 -1
- package/src/session-route.ts +25 -0
- package/src/setup-surface.ts +265 -0
- package/src/test-fixtures.ts +45 -0
- package/src/types.ts +26 -25
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -110
- 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-
|
|
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
|
|
13
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
141
|
-
|
|
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
|
|
159
|
-
|
|
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
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
});
|