@nostrify/nostrify 0.46.4

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 (182) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/BunkerURI.test.ts +28 -0
  3. package/BunkerURI.ts +58 -0
  4. package/CHANGELOG.md +562 -0
  5. package/LICENSE +21 -0
  6. package/NBrowserSigner.test.ts +170 -0
  7. package/NBrowserSigner.ts +100 -0
  8. package/NCache.bench.ts +81 -0
  9. package/NCache.test.ts +22 -0
  10. package/NCache.ts +73 -0
  11. package/NConnectSigner.test.ts +102 -0
  12. package/NConnectSigner.ts +189 -0
  13. package/NIP05.test.ts +67 -0
  14. package/NIP05.ts +52 -0
  15. package/NIP50.test.ts +58 -0
  16. package/NIP50.ts +24 -0
  17. package/NIP98.test.ts +181 -0
  18. package/NIP98.ts +97 -0
  19. package/NKinds.test.ts +42 -0
  20. package/NKinds.ts +26 -0
  21. package/NPool.test.ts +117 -0
  22. package/NPool.ts +224 -0
  23. package/NRelay1.test.ts +174 -0
  24. package/NRelay1.ts +440 -0
  25. package/NSchema.test.ts +94 -0
  26. package/NSchema.ts +255 -0
  27. package/NSecSigner.bench.ts +55 -0
  28. package/NSecSigner.test.ts +26 -0
  29. package/NSecSigner.ts +60 -0
  30. package/NSet.bench.ts +10 -0
  31. package/NSet.test.ts +92 -0
  32. package/NSet.ts +203 -0
  33. package/README.md +314 -0
  34. package/RelayError.test.ts +23 -0
  35. package/RelayError.ts +22 -0
  36. package/dist/BunkerURI.d.ts +23 -0
  37. package/dist/BunkerURI.d.ts.map +1 -0
  38. package/dist/BunkerURI.js +52 -0
  39. package/dist/BunkerURI.js.map +1 -0
  40. package/dist/NBrowserSigner.d.ts +27 -0
  41. package/dist/NBrowserSigner.d.ts.map +1 -0
  42. package/dist/NBrowserSigner.js +96 -0
  43. package/dist/NBrowserSigner.js.map +1 -0
  44. package/dist/NCache.d.ts +34 -0
  45. package/dist/NCache.d.ts.map +1 -0
  46. package/dist/NCache.js +63 -0
  47. package/dist/NCache.js.map +1 -0
  48. package/dist/NConnectSigner.d.ts +50 -0
  49. package/dist/NConnectSigner.d.ts.map +1 -0
  50. package/dist/NConnectSigner.js +130 -0
  51. package/dist/NConnectSigner.js.map +1 -0
  52. package/dist/NIP05.d.ts +15 -0
  53. package/dist/NIP05.d.ts.map +1 -0
  54. package/dist/NIP05.js +40 -0
  55. package/dist/NIP05.js.map +1 -0
  56. package/dist/NIP50.d.ts +10 -0
  57. package/dist/NIP50.d.ts.map +1 -0
  58. package/dist/NIP50.js +26 -0
  59. package/dist/NIP50.js.map +1 -0
  60. package/dist/NIP98.d.ts +15 -0
  61. package/dist/NIP98.d.ts.map +1 -0
  62. package/dist/NIP98.js +71 -0
  63. package/dist/NIP98.js.map +1 -0
  64. package/dist/NKinds.d.ts +13 -0
  65. package/dist/NKinds.d.ts.map +1 -0
  66. package/dist/NKinds.js +27 -0
  67. package/dist/NKinds.js.map +1 -0
  68. package/dist/NPool.d.ts +91 -0
  69. package/dist/NPool.d.ts.map +1 -0
  70. package/dist/NPool.js +185 -0
  71. package/dist/NPool.js.map +1 -0
  72. package/dist/NRelay1.d.ts +80 -0
  73. package/dist/NRelay1.d.ts.map +1 -0
  74. package/dist/NRelay1.js +336 -0
  75. package/dist/NRelay1.js.map +1 -0
  76. package/dist/NSchema.d.ts +73 -0
  77. package/dist/NSchema.d.ts.map +1 -0
  78. package/dist/NSchema.js +215 -0
  79. package/dist/NSchema.js.map +1 -0
  80. package/dist/NSecSigner.d.ts +29 -0
  81. package/dist/NSecSigner.d.ts.map +1 -0
  82. package/dist/NSecSigner.js +52 -0
  83. package/dist/NSecSigner.js.map +1 -0
  84. package/dist/NSet.d.ts +76 -0
  85. package/dist/NSet.d.ts.map +1 -0
  86. package/dist/NSet.js +174 -0
  87. package/dist/NSet.js.map +1 -0
  88. package/dist/RelayError.d.ts +10 -0
  89. package/dist/RelayError.d.ts.map +1 -0
  90. package/dist/RelayError.js +23 -0
  91. package/dist/RelayError.js.map +1 -0
  92. package/dist/ln/LNURL.d.ts +55 -0
  93. package/dist/ln/LNURL.d.ts.map +1 -0
  94. package/dist/ln/LNURL.js +105 -0
  95. package/dist/ln/LNURL.js.map +1 -0
  96. package/dist/ln/mod.d.ts +4 -0
  97. package/dist/ln/mod.d.ts.map +1 -0
  98. package/dist/ln/mod.js +6 -0
  99. package/dist/ln/mod.js.map +1 -0
  100. package/dist/ln/types/LNURLCallback.d.ts +8 -0
  101. package/dist/ln/types/LNURLCallback.d.ts.map +1 -0
  102. package/dist/ln/types/LNURLCallback.js +3 -0
  103. package/dist/ln/types/LNURLCallback.js.map +1 -0
  104. package/dist/ln/types/LNURLDetails.d.ts +20 -0
  105. package/dist/ln/types/LNURLDetails.d.ts.map +1 -0
  106. package/dist/ln/types/LNURLDetails.js +3 -0
  107. package/dist/ln/types/LNURLDetails.js.map +1 -0
  108. package/dist/mod.d.ts +16 -0
  109. package/dist/mod.d.ts.map +1 -0
  110. package/dist/mod.js +32 -0
  111. package/dist/mod.js.map +1 -0
  112. package/dist/test/ErrorRelay.d.ts +21 -0
  113. package/dist/test/ErrorRelay.d.ts.map +1 -0
  114. package/dist/test/ErrorRelay.js +26 -0
  115. package/dist/test/ErrorRelay.js.map +1 -0
  116. package/dist/test/MockRelay.d.ts +20 -0
  117. package/dist/test/MockRelay.d.ts.map +1 -0
  118. package/dist/test/MockRelay.js +66 -0
  119. package/dist/test/MockRelay.js.map +1 -0
  120. package/dist/test/TestRelayServer.d.ts +25 -0
  121. package/dist/test/TestRelayServer.d.ts.map +1 -0
  122. package/dist/test/TestRelayServer.js +134 -0
  123. package/dist/test/TestRelayServer.js.map +1 -0
  124. package/dist/test/mod.d.ts +8 -0
  125. package/dist/test/mod.d.ts.map +1 -0
  126. package/dist/test/mod.js +28 -0
  127. package/dist/test/mod.js.map +1 -0
  128. package/dist/tsconfig.tsbuildinfo +1 -0
  129. package/dist/uploaders/BlossomUploader.d.ts +26 -0
  130. package/dist/uploaders/BlossomUploader.d.ts.map +1 -0
  131. package/dist/uploaders/BlossomUploader.js +71 -0
  132. package/dist/uploaders/BlossomUploader.js.map +1 -0
  133. package/dist/uploaders/NostrBuildUploader.d.ts +24 -0
  134. package/dist/uploaders/NostrBuildUploader.d.ts.map +1 -0
  135. package/dist/uploaders/NostrBuildUploader.js +67 -0
  136. package/dist/uploaders/NostrBuildUploader.js.map +1 -0
  137. package/dist/uploaders/mod.d.ts +3 -0
  138. package/dist/uploaders/mod.d.ts.map +1 -0
  139. package/dist/uploaders/mod.js +8 -0
  140. package/dist/uploaders/mod.js.map +1 -0
  141. package/dist/utils/CircularSet.d.ts +13 -0
  142. package/dist/utils/CircularSet.d.ts.map +1 -0
  143. package/dist/utils/CircularSet.js +35 -0
  144. package/dist/utils/CircularSet.js.map +1 -0
  145. package/dist/utils/Machina.d.ts +36 -0
  146. package/dist/utils/Machina.d.ts.map +1 -0
  147. package/dist/utils/Machina.js +66 -0
  148. package/dist/utils/Machina.js.map +1 -0
  149. package/dist/utils/N64.d.ts +9 -0
  150. package/dist/utils/N64.d.ts.map +1 -0
  151. package/dist/utils/N64.js +23 -0
  152. package/dist/utils/N64.js.map +1 -0
  153. package/dist/utils/mod.d.ts +3 -0
  154. package/dist/utils/mod.d.ts.map +1 -0
  155. package/dist/utils/mod.js +8 -0
  156. package/dist/utils/mod.js.map +1 -0
  157. package/ln/LNURL.test.ts +87 -0
  158. package/ln/LNURL.ts +146 -0
  159. package/ln/mod.ts +4 -0
  160. package/ln/types/LNURLCallback.ts +7 -0
  161. package/ln/types/LNURLDetails.ts +19 -0
  162. package/mod.ts +16 -0
  163. package/package.json +23 -0
  164. package/test/ErrorRelay.test.ts +19 -0
  165. package/test/ErrorRelay.ts +40 -0
  166. package/test/MockRelay.test.ts +20 -0
  167. package/test/MockRelay.ts +92 -0
  168. package/test/TestRelayServer.ts +156 -0
  169. package/test/mod.ts +28 -0
  170. package/tsconfig.json +14 -0
  171. package/uploaders/BlossomUploader.test.ts +26 -0
  172. package/uploaders/BlossomUploader.ts +98 -0
  173. package/uploaders/NostrBuildUploader.test.ts +22 -0
  174. package/uploaders/NostrBuildUploader.ts +89 -0
  175. package/uploaders/mod.ts +2 -0
  176. package/utils/CircularSet.test.ts +15 -0
  177. package/utils/CircularSet.ts +34 -0
  178. package/utils/Machina.test.ts +91 -0
  179. package/utils/Machina.ts +66 -0
  180. package/utils/N64.test.ts +27 -0
  181. package/utils/N64.ts +23 -0
  182. package/utils/mod.ts +2 -0
package/NRelay1.ts ADDED
@@ -0,0 +1,440 @@
1
+ import {
2
+ NostrClientMsg,
3
+ NostrClientREQ,
4
+ NostrEvent,
5
+ NostrFilter,
6
+ NostrRelayCLOSED,
7
+ NostrRelayCOUNT,
8
+ NostrRelayEOSE,
9
+ NostrRelayEVENT,
10
+ NostrRelayMsg,
11
+ NostrRelayNOTICE,
12
+ NostrRelayOK,
13
+ NRelay,
14
+ } from '@nostrify/types';
15
+ import { getFilterLimit, matchFilters, verifyEvent as _verifyEvent } from 'nostr-tools';
16
+ import { ArrayQueue, Backoff, ExponentialBackoff, Websocket, WebsocketBuilder, WebsocketEvent } from 'websocket-ts';
17
+
18
+ import { Machina } from './utils/Machina';
19
+ import { NSchema as n } from './NSchema';
20
+ import { NSet } from './NSet';
21
+
22
+ /** Map of EventEmitter events. */
23
+ type EventMap = {
24
+ [k: `ok:${string}`]: NostrRelayOK;
25
+ [k: `sub:${string}`]: NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED;
26
+ [k: `count:${string}`]: NostrRelayCOUNT | NostrRelayCLOSED;
27
+ notice: NostrRelayNOTICE;
28
+ };
29
+
30
+ /** Options used for constructing an `NRelay1` instance. */
31
+ export interface NRelay1Opts {
32
+ /** Respond to `AUTH` challenges by producing a signed kind `22242` event. */
33
+ auth?(challenge: string): Promise<NostrEvent>;
34
+ /** Configure reconnection strategy, or set to `false` to disable. Default: `new ExponentialBackoff(1000)`. */
35
+ backoff?: Backoff | false;
36
+ /** How long to wait (in milliseconds) for the caller to create a subscription before closing the connection. Set to `false` to disable. Default: `30_000`. */
37
+ idleTimeout?: number | false;
38
+ /** Ensure the event is valid before returning it. Default: `nostrTools.verifyEvent`. */
39
+ verifyEvent?(event: NostrEvent): boolean;
40
+ /** Logger callback. */
41
+ log?(log: NRelay1Log): void;
42
+ }
43
+
44
+ export interface NRelay1Log {
45
+ level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical';
46
+ ns: string;
47
+ [k: string]: JsonValue | undefined | { toJSON(): JsonValue } | Error;
48
+ }
49
+
50
+ /** Single relay connection over WebSocket. */
51
+ export class NRelay1 implements NRelay {
52
+ socket: Websocket;
53
+
54
+ private subs = new Map<string, NostrClientREQ>();
55
+ private closedByUser = false;
56
+ private idleTimer?: ReturnType<typeof setTimeout>;
57
+ private controller = new AbortController();
58
+
59
+ private ee = new EventTarget();
60
+
61
+ get subscriptions(): readonly NostrClientREQ[] {
62
+ return [...this.subs.values()];
63
+ }
64
+
65
+ private log(log: NRelay1Log): void {
66
+ this.opts.log?.({ ...log, url: this.url });
67
+ }
68
+
69
+ constructor(private url: string, private opts: NRelay1Opts = {}) {
70
+ this.socket = this.createSocket();
71
+ this.maybeStartIdleTimer();
72
+ }
73
+
74
+ /** Create (and open) a WebSocket connection with automatic reconnect. */
75
+ private createSocket(): Websocket {
76
+ const { backoff = new ExponentialBackoff(1000) } = this.opts;
77
+
78
+ return new WebsocketBuilder(this.url)
79
+ .withBuffer(new ArrayQueue())
80
+ .withBackoff(backoff === false ? undefined : backoff)
81
+ .onOpen((socket) => {
82
+ this.log({
83
+ level: 'debug',
84
+ ns: 'relay.ws.state',
85
+ state: 'open',
86
+ readyState: socket.readyState,
87
+ });
88
+ for (const req of this.subs.values()) {
89
+ this.send(req);
90
+ }
91
+ })
92
+ .onClose((socket) => {
93
+ this.log({
94
+ level: 'debug',
95
+ ns: 'relay.ws.state',
96
+ state: 'close',
97
+ readyState: socket.readyState,
98
+ });
99
+ // If the connection closes on its own and there are no active subscriptions, let it stay closed.
100
+ if (!this.subs.size) {
101
+ this.socket.close();
102
+ }
103
+ })
104
+ .onReconnect((socket) => {
105
+ this.log({
106
+ level: 'debug',
107
+ ns: 'relay.ws.state',
108
+ state: 'reconnect',
109
+ readyState: socket.readyState,
110
+ });
111
+ })
112
+ .onRetry((socket, e) => {
113
+ this.log({
114
+ level: 'warn',
115
+ ns: 'relay.ws.retry',
116
+ readyState: socket.readyState,
117
+ backoff: e.detail.backoff,
118
+ });
119
+ })
120
+ .onError((socket) => {
121
+ this.log({
122
+ level: 'error',
123
+ ns: 'relay.ws.error',
124
+ readyState: socket.readyState,
125
+ });
126
+ })
127
+ .onMessage((_socket, e) => {
128
+ if (typeof e.data !== 'string') {
129
+ this.close();
130
+ return;
131
+ }
132
+
133
+ const result = n.json().pipe(n.relayMsg()).safeParse(e.data);
134
+
135
+ if (result.success) {
136
+ this.log({
137
+ level: 'trace',
138
+ ns: 'relay.ws.message',
139
+ data: result.data as JsonValue,
140
+ });
141
+ this.receive(result.data);
142
+ } else {
143
+ this.log({
144
+ level: 'warn',
145
+ ns: 'relay.ws.message',
146
+ error: result.error,
147
+ });
148
+ }
149
+ })
150
+ .build();
151
+ }
152
+
153
+ /** Handle a NIP-01 relay message. */
154
+ protected receive(msg: NostrRelayMsg): void {
155
+ const { auth, verifyEvent = _verifyEvent } = this.opts;
156
+
157
+ switch (msg[0]) {
158
+ case 'EVENT':
159
+ if (!verifyEvent(msg[2])) break;
160
+ this.ee.dispatchEvent(
161
+ new CustomEvent(`sub:${msg[1]}`, { detail: msg }),
162
+ );
163
+ break;
164
+ case 'EOSE':
165
+ this.ee.dispatchEvent(
166
+ new CustomEvent(`sub:${msg[1]}`, { detail: msg }),
167
+ );
168
+ break;
169
+ case 'CLOSED':
170
+ this.subs.delete(msg[1]);
171
+ this.maybeStartIdleTimer();
172
+ this.ee.dispatchEvent(
173
+ new CustomEvent(`sub:${msg[1]}`, { detail: msg }),
174
+ );
175
+ this.ee.dispatchEvent(
176
+ new CustomEvent(`count:${msg[1]}`, { detail: msg }),
177
+ );
178
+ break;
179
+ case 'OK':
180
+ this.ee.dispatchEvent(new CustomEvent(`ok:${msg[1]}`, { detail: msg }));
181
+ break;
182
+ case 'NOTICE':
183
+ this.ee.dispatchEvent(new CustomEvent('notice', { detail: msg }));
184
+ break;
185
+ case 'COUNT':
186
+ this.ee.dispatchEvent(
187
+ new CustomEvent(`count:${msg[1]}`, { detail: msg }),
188
+ );
189
+ break;
190
+ case 'AUTH':
191
+ auth?.(msg[1]).then((event) => this.send(['AUTH', event])).catch(
192
+ () => {},
193
+ );
194
+ }
195
+ }
196
+
197
+ /** Send a NIP-01 client message to the relay. */
198
+ protected send(msg: NostrClientMsg): void {
199
+ this.log({ level: 'trace', ns: 'relay.ws.send', data: msg as JsonValue });
200
+ this.wake();
201
+
202
+ switch (msg[0]) {
203
+ case 'REQ':
204
+ this.subs.set(msg[1], msg);
205
+ break;
206
+ case 'CLOSE':
207
+ this.subs.delete(msg[1]);
208
+ this.maybeStartIdleTimer();
209
+ break;
210
+ case 'EVENT':
211
+ case 'COUNT':
212
+ return this.socket.send(JSON.stringify(msg));
213
+ }
214
+
215
+ if (this.socket.readyState === WebSocket.OPEN) {
216
+ this.socket.send(JSON.stringify(msg));
217
+ }
218
+ }
219
+
220
+ async *req(
221
+ filters: NostrFilter[],
222
+ opts: { signal?: AbortSignal } = {},
223
+ ): AsyncGenerator<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED> {
224
+ const { signal } = opts;
225
+ const subscriptionId = crypto.randomUUID();
226
+
227
+ const msgs = this.on(`sub:${subscriptionId}`, signal);
228
+ const req: NostrClientREQ = ['REQ', subscriptionId, ...filters];
229
+
230
+ this.send(req);
231
+
232
+ try {
233
+ for await (const msg of msgs) {
234
+ if (msg[0] === 'EOSE') yield msg;
235
+ if (msg[0] === 'CLOSED') break;
236
+ if (msg[0] === 'EVENT') {
237
+ if (matchFilters(filters, msg[2])) {
238
+ yield msg;
239
+ } else {
240
+ continue;
241
+ }
242
+ }
243
+ }
244
+ } finally {
245
+ this.send(['CLOSE', subscriptionId]);
246
+ }
247
+ }
248
+
249
+ async query(
250
+ filters: NostrFilter[],
251
+ opts?: { signal?: AbortSignal },
252
+ ): Promise<NostrEvent[]> {
253
+ const events = new NSet();
254
+
255
+ const limit = filters.reduce(
256
+ (result, filter) => result + getFilterLimit(filter),
257
+ 0,
258
+ );
259
+ if (limit === 0) return [];
260
+
261
+ for await (const msg of this.req(filters, opts)) {
262
+ if (msg[0] === 'EOSE') break;
263
+ if (msg[0] === 'EVENT') events.add(msg[2]);
264
+ if (msg[0] === 'CLOSED') throw new Error('Subscription closed');
265
+
266
+ if (events.size >= limit) {
267
+ break;
268
+ }
269
+ }
270
+
271
+ return [...events];
272
+ }
273
+
274
+ async event(
275
+ event: NostrEvent,
276
+ opts?: { signal?: AbortSignal },
277
+ ): Promise<void> {
278
+ const result = this.once(`ok:${event.id}`, opts?.signal);
279
+
280
+ try {
281
+ this.send(['EVENT', event]);
282
+ } catch (e) {
283
+ result.catch(() => {});
284
+ throw e;
285
+ }
286
+
287
+ const [, , ok, reason] = await result;
288
+
289
+ if (!ok) {
290
+ throw new Error(reason);
291
+ }
292
+ }
293
+
294
+ async count(
295
+ filters: NostrFilter[],
296
+ opts?: { signal?: AbortSignal },
297
+ ): Promise<{ count: number; approximate?: boolean }> {
298
+ const subscriptionId = crypto.randomUUID();
299
+ const result = this.once(`count:${subscriptionId}`, opts?.signal);
300
+
301
+ try {
302
+ this.send(['COUNT', subscriptionId, ...filters]);
303
+ } catch (e) {
304
+ result.catch(() => {});
305
+ throw e;
306
+ }
307
+
308
+ const msg = await result;
309
+
310
+ switch (msg[0]) {
311
+ case 'CLOSED':
312
+ throw new Error('Subscription closed');
313
+ case 'COUNT': {
314
+ const [, , count] = msg;
315
+ return count;
316
+ }
317
+ }
318
+ }
319
+
320
+ /** Get a stream of EE events. */
321
+ private async *on<K extends keyof EventMap>(
322
+ key: K,
323
+ signal?: AbortSignal,
324
+ ): AsyncIterable<EventMap[K]> {
325
+ const _signal = signal ? AbortSignal.any([this.controller.signal, signal]) : this.controller.signal;
326
+
327
+ if (_signal.aborted) throw this.abortError();
328
+
329
+ const machina = new Machina<EventMap[K]>(_signal);
330
+ const onMsg = (e: Event) => machina.push((e as CustomEvent<EventMap[K]>).detail);
331
+
332
+ this.ee.addEventListener(key, onMsg);
333
+
334
+ try {
335
+ for await (const msg of machina) {
336
+ yield msg;
337
+ }
338
+ } finally {
339
+ this.ee.removeEventListener(key, onMsg);
340
+ }
341
+ }
342
+
343
+ /** Wait for a single EE event. */
344
+ private async once<K extends keyof EventMap>(
345
+ key: K,
346
+ signal?: AbortSignal,
347
+ ): Promise<EventMap[K]> {
348
+ for await (const msg of this.on(key, signal)) {
349
+ return msg;
350
+ }
351
+ throw new Error('Unreachable');
352
+ }
353
+
354
+ protected abortError(): DOMException {
355
+ return new DOMException('The signal has been aborted', 'AbortError');
356
+ }
357
+
358
+ /** Start the idle time if applicable. */
359
+ private maybeStartIdleTimer(): void {
360
+ const { idleTimeout = 30_000 } = this.opts;
361
+
362
+ // If the idle timeout is disabled, do nothing.
363
+ if (idleTimeout === false) return;
364
+ // If a timer is already running, let it continue without disruption.
365
+ if (this.idleTimer) return;
366
+ // If there are still subscriptions, the connection is not "idle".
367
+ if (this.subs.size) return;
368
+ // If the connection was manually closed, there's no need to start a timer.
369
+ if (this.closedByUser) return;
370
+
371
+ this.log({
372
+ level: 'debug',
373
+ ns: 'relay.idletimer',
374
+ state: 'running',
375
+ timeout: idleTimeout,
376
+ });
377
+ this.idleTimer = setTimeout(() => {
378
+ this.log({
379
+ level: 'debug',
380
+ ns: 'relay.idletimer',
381
+ state: 'aborted',
382
+ timeout: idleTimeout,
383
+ });
384
+ this.socket.close();
385
+ }, idleTimeout);
386
+ }
387
+
388
+ /** Stop the idle timer. */
389
+ private stopIdleTimer(): void {
390
+ this.log({ level: 'debug', ns: 'relay.idletimer', state: 'stopped' });
391
+ clearTimeout(this.idleTimer);
392
+ this.idleTimer = undefined;
393
+ }
394
+
395
+ /** Make a new WebSocket, but only if it was closed by an idle timeout. */
396
+ private wake(): void {
397
+ this.stopIdleTimer();
398
+
399
+ if (!this.closedByUser && this.socket.closedByUser) {
400
+ this.log({ level: 'debug', ns: 'relay.wake', state: 'awoken' });
401
+ this.socket = this.createSocket();
402
+ } else if (this.closedByUser || this.socket.closedByUser) {
403
+ this.log({ level: 'debug', ns: 'relay.wake', state: 'closed' });
404
+ } else {
405
+ this.log({ level: 'debug', ns: 'relay.wake', state: 'awake' });
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Close the relay connection and prevent it from reconnecting.
411
+ * After this you should dispose of the `NRelay1` instance and create a new one to connect again.
412
+ */
413
+ async close(): Promise<void> {
414
+ this.closedByUser = true;
415
+ this.socket.close();
416
+ this.stopIdleTimer();
417
+ this.controller.abort();
418
+
419
+ if (this.socket.readyState !== WebSocket.CLOSED) {
420
+ await new Promise((resolve) => {
421
+ this.socket.addEventListener(WebsocketEvent.close, resolve, {
422
+ once: true,
423
+ });
424
+ });
425
+ }
426
+ }
427
+
428
+ async [Symbol.asyncDispose](): Promise<void> {
429
+ await this.close();
430
+ }
431
+ }
432
+
433
+ /** Native JSON primitive value, including objects and arrays. */
434
+ type JsonValue =
435
+ | { [key: string]: JsonValue | undefined }
436
+ | JsonValue[]
437
+ | string
438
+ | number
439
+ | boolean
440
+ | null;
@@ -0,0 +1,94 @@
1
+ import { assert, assertEquals } from '@std/assert';
2
+
3
+ import { NSchema as n } from './NSchema.ts';
4
+
5
+ import nostrEvent from '../../fixtures/event-1.json' with { type: 'json' };
6
+
7
+ Deno.test('n.id', () => {
8
+ assert(n.id().safeParse(nostrEvent.id).success);
9
+ assert(n.id().safeParse(nostrEvent.pubkey).success);
10
+
11
+ assert(!n.id().safeParse('abc').success);
12
+ assert(!n.id().safeParse(nostrEvent.pubkey.slice(0, -1)).success);
13
+ });
14
+
15
+ Deno.test('n.bech32', () => {
16
+ assert(n.bech32('npub').safeParse('npub108pv4cg5ag52nq082kd5leu9ffrn2gdg6g4xdwatn73y36uzplmq9uyev6').success);
17
+ assert(
18
+ n.bech32().safeParse('lnurl1dp68gurn8ghj7em9w3skccne9e3k7mf09emk2mrv944kummhdchkcmn4wfk8qtmpd3jhsemvv4shxmmw5uhvxu')
19
+ .success,
20
+ );
21
+
22
+ assert(!n.bech32().safeParse('abc').success);
23
+ assert(
24
+ !n.bech32().safeParse(
25
+ 'lnurl1dp68gurn8ghj7em9w3skccne9e3k7mf09emk2mrv944kummhdchkcmn4wfk8qtmpd3jhsemvv4shxmmw5uhvxu' + '_',
26
+ ).success,
27
+ );
28
+ });
29
+
30
+ Deno.test('n.filter', () => {
31
+ assert(n.filter().safeParse({}).success);
32
+ assert(n.filter().safeParse({ kinds: [0] }).success);
33
+ assert(n.filter().safeParse({ ids: [nostrEvent.id] }).success);
34
+ assert(n.filter().safeParse({ authors: [nostrEvent.pubkey] }).success);
35
+ assert(n.filter().safeParse({ kinds: [1], '#t': ['nostrasia'] }).success);
36
+ assert(n.filter().safeParse({ '#t': ['yolo'] }).success);
37
+
38
+ assertEquals(
39
+ n.filter().parse({ kinds: [1], '#t': ['nostrasia'], seenOn: ['wss://relay.mostr.pub/'] }),
40
+ { kinds: [1], '#t': ['nostrasia'] },
41
+ );
42
+
43
+ assert(!n.filter().safeParse({ kinds: [0.5] }).success);
44
+ assert(!n.filter().safeParse({ ids: ['abc'] }).success);
45
+ assert(!n.filter().safeParse({ authors: ['abc'] }).success);
46
+ });
47
+
48
+ Deno.test('n.event', () => {
49
+ assert(n.event().safeParse(nostrEvent).success);
50
+
51
+ assertEquals(
52
+ n.event().parse(nostrEvent),
53
+ {
54
+ id: nostrEvent.id,
55
+ kind: nostrEvent.kind,
56
+ pubkey: nostrEvent.pubkey,
57
+ tags: nostrEvent.tags,
58
+ content: nostrEvent.content,
59
+ created_at: nostrEvent.created_at,
60
+ sig: nostrEvent.sig,
61
+ },
62
+ );
63
+
64
+ assert(!n.event().safeParse({}).success);
65
+ assert(!n.event().safeParse({ id: 'abc' }).success);
66
+ assert(!n.event().safeParse({ kind: 0.5 }).success);
67
+ assert(!n.event().safeParse({ pubkey: 'abc' }).success);
68
+ assert(!n.event().safeParse({ tags: ['abc'] }).success);
69
+ assert(!n.event().safeParse({ content: 1 }).success);
70
+ assert(!n.event().safeParse({ created_at: -1 }).success);
71
+ assert(!n.event().safeParse({ sig: 'abc' }).success);
72
+ });
73
+
74
+ Deno.test('n.metadata', () => {
75
+ // Passing
76
+ assertEquals(n.metadata().parse({ name: 'Alex' }).name, 'Alex');
77
+ assertEquals(n.metadata().parse({ about: 'I am a developer.' }).about, 'I am a developer.');
78
+ assertEquals(n.metadata().parse({ picture: 'https://nostrify.dev/1.png' }).picture, 'https://nostrify.dev/1.png');
79
+ assertEquals(n.metadata().parse({ banner: 'https://nostrify.dev/2.png' }).banner, 'https://nostrify.dev/2.png');
80
+ assertEquals(n.metadata().parse({ nip05: 'alex@gleasonator.dev' }).nip05, 'alex@gleasonator.dev');
81
+ assertEquals(n.metadata().parse({ lud06: 'lnurl1acdacd' }).lud06, 'lnurl1acdacd');
82
+ assertEquals(n.metadata().parse({ lud16: 'alex@alexgleason.me' }).lud16, 'alex@alexgleason.me');
83
+ assertEquals(n.metadata().parse({ website: 'https://nostrify.dev' }).website, 'https://nostrify.dev');
84
+
85
+ // Failing
86
+ assertEquals(n.metadata().parse({ name: 1 }).name, undefined);
87
+ assertEquals(n.metadata().parse({ about: 1 }).about, undefined);
88
+ assertEquals(n.metadata().parse({ picture: 'abc' }).picture, undefined);
89
+ assertEquals(n.metadata().parse({ banner: 'abc' }).banner, undefined);
90
+ assertEquals(n.metadata().parse({ nip05: 'nostrify.dev' }).nip05, undefined);
91
+ assertEquals(n.metadata().parse({ lud06: 'npub1abc' }).lud06, undefined);
92
+ assertEquals(n.metadata().parse({ lud16: 'nostrify.dev' }).lud16, undefined);
93
+ assertEquals(n.metadata().parse({ website: 'nostrify.dev' }).website, undefined);
94
+ });