onelobby 0.4.1 → 0.6.0

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 (99) hide show
  1. package/README.md +2 -2
  2. package/blueprint.schema.json +244 -0
  3. package/dist/blueprint.d.ts +170 -0
  4. package/dist/blueprint.js +286 -0
  5. package/dist/blueprint.js.map +1 -0
  6. package/dist/codec.d.ts +23 -0
  7. package/dist/codec.js +28 -0
  8. package/dist/codec.js.map +1 -1
  9. package/dist/direct/browser-host.d.ts +46 -0
  10. package/dist/direct/browser-host.js +93 -0
  11. package/dist/direct/browser-host.js.map +1 -0
  12. package/dist/direct/hub-host.d.ts +34 -23
  13. package/dist/direct/hub-host.js +75 -59
  14. package/dist/direct/hub-host.js.map +1 -1
  15. package/dist/direct/hub-local.d.ts +22 -0
  16. package/dist/direct/hub-local.js +46 -0
  17. package/dist/direct/hub-local.js.map +1 -0
  18. package/dist/direct/hub.d.ts +37 -10
  19. package/dist/direct/hub.js +114 -23
  20. package/dist/direct/hub.js.map +1 -1
  21. package/dist/direct/index.d.ts +37 -4
  22. package/dist/direct/index.js +162 -21
  23. package/dist/direct/index.js.map +1 -1
  24. package/dist/direct/metrics.d.ts +12 -0
  25. package/dist/direct/metrics.js +45 -0
  26. package/dist/direct/metrics.js.map +1 -0
  27. package/dist/direct/portmap.d.ts +9 -2
  28. package/dist/direct/portmap.js +7 -2
  29. package/dist/direct/portmap.js.map +1 -1
  30. package/dist/direct/sha1.d.ts +16 -0
  31. package/dist/direct/sha1.js +111 -0
  32. package/dist/direct/sha1.js.map +1 -0
  33. package/dist/direct-address.d.ts +2 -2
  34. package/dist/direct-address.js.map +1 -1
  35. package/dist/friendly.d.ts +28 -0
  36. package/dist/friendly.js +40 -0
  37. package/dist/friendly.js.map +1 -1
  38. package/dist/index.d.ts +11 -3
  39. package/dist/index.js +8 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/internals.d.ts +9 -0
  42. package/dist/internals.js +9 -0
  43. package/dist/internals.js.map +1 -1
  44. package/dist/join-code-kind.d.ts +10 -0
  45. package/dist/join-code-kind.js +10 -0
  46. package/dist/join-code-kind.js.map +1 -0
  47. package/dist/joincode.d.ts +10 -1
  48. package/dist/joincode.js +23 -3
  49. package/dist/joincode.js.map +1 -1
  50. package/dist/lobby.d.ts +65 -4
  51. package/dist/lobby.js +151 -12
  52. package/dist/lobby.js.map +1 -1
  53. package/dist/lockstep.d.ts +8 -0
  54. package/dist/lockstep.js +23 -2
  55. package/dist/lockstep.js.map +1 -1
  56. package/dist/nostr/crypto.d.ts +20 -0
  57. package/dist/nostr/crypto.js +78 -0
  58. package/dist/nostr/crypto.js.map +1 -0
  59. package/dist/nostr/host.d.ts +66 -0
  60. package/dist/nostr/host.js +196 -0
  61. package/dist/nostr/host.js.map +1 -0
  62. package/dist/nostr/limits.d.ts +31 -0
  63. package/dist/nostr/limits.js +36 -0
  64. package/dist/nostr/limits.js.map +1 -0
  65. package/dist/nostr/link.d.ts +70 -0
  66. package/dist/nostr/link.js +180 -0
  67. package/dist/nostr/link.js.map +1 -0
  68. package/dist/nostr/relays.d.ts +79 -0
  69. package/dist/nostr/relays.js +315 -0
  70. package/dist/nostr/relays.js.map +1 -0
  71. package/dist/nostr/room.d.ts +43 -0
  72. package/dist/nostr/room.js +104 -0
  73. package/dist/nostr/room.js.map +1 -0
  74. package/dist/nostr/secp256k1.d.ts +10 -0
  75. package/dist/nostr/secp256k1.js +171 -0
  76. package/dist/nostr/secp256k1.js.map +1 -0
  77. package/dist/nostr/socket.d.ts +48 -0
  78. package/dist/nostr/socket.js +141 -0
  79. package/dist/nostr/socket.js.map +1 -0
  80. package/dist/onelobby.bundle.js +146 -0
  81. package/dist/onelobby.bundle.js.map +7 -0
  82. package/dist/onelobby.bundle.mjs +11 -5
  83. package/dist/onelobby.bundle.mjs.map +4 -4
  84. package/dist/onelobby.d.ts +54 -4
  85. package/dist/onelobby.global.js +2932 -49
  86. package/dist/onelobby.global.js.map +4 -4
  87. package/dist/onelobby.global.min.js +11 -5
  88. package/dist/onelobby.global.min.js.map +4 -4
  89. package/dist/onelobby.js +158 -15
  90. package/dist/onelobby.js.map +1 -1
  91. package/dist/pause.d.ts +85 -0
  92. package/dist/pause.js +160 -0
  93. package/dist/pause.js.map +1 -0
  94. package/dist/transport.d.ts +3 -1
  95. package/dist/transport.js.map +1 -1
  96. package/dist/webrtc.d.ts +1 -0
  97. package/dist/webrtc.js +6 -1
  98. package/dist/webrtc.js.map +1 -1
  99. package/package.json +4 -2
@@ -0,0 +1,315 @@
1
+ import { sha256, toHex, fromHex } from "./crypto.js";
2
+ import { schnorrPublicKey, schnorrSign } from "./secp256k1.js";
3
+ export const NOSTR_KIND = 25050;
4
+ /** NIP-01 event id: SHA-256 of `[0, pubkey, created_at, kind, tags, content]` serialised without whitespace. */
5
+ export async function eventId(pubkey, createdAt, kind, tags, content) {
6
+ return toHex(await sha256(new TextEncoder().encode(JSON.stringify([0, pubkey, createdAt, kind, tags, content]))));
7
+ }
8
+ /** A signed kind-25050 event (BIP340 Schnorr over the id). */
9
+ export async function makeEvent(secret, topic, content, createdAt) {
10
+ const pubkey = toHex(schnorrPublicKey(secret));
11
+ const tags = [["t", topic]];
12
+ const id = await eventId(pubkey, createdAt, NOSTR_KIND, tags, content);
13
+ const sig = toHex(await schnorrSign(fromHex(id), secret));
14
+ return { id, pubkey, created_at: createdAt, kind: NOSTR_KIND, tags, content, sig };
15
+ }
16
+ export const defaultRelayConnect = (url) => {
17
+ const WS = globalThis.WebSocket;
18
+ if (!WS)
19
+ throw new Error("no WebSocket in this runtime (Node 22+ has one; older Node needs advanced.nostrConnect)");
20
+ return new WS(url);
21
+ };
22
+ const SEEN_MAX = 4096;
23
+ const SINCE_SLACK_S = 30;
24
+ /** Every relay at once, each reconnected with backoff (1 s doubling to 30 s); events deduped by id. */
25
+ export class RelayPool {
26
+ o;
27
+ relays;
28
+ seen = new Set();
29
+ sub = "ol" + Math.random().toString(16).slice(2, 10);
30
+ closed = false;
31
+ waiters = [];
32
+ constructor(o) {
33
+ this.o = o;
34
+ this.relays = o.urls.map((url) => ({ url, ws: null, open: false, attempted: false, skipped: false, backoff: 1000, cancelRetry: null }));
35
+ }
36
+ get total() {
37
+ return this.relays.length;
38
+ }
39
+ /** Relays connected right now (and not skipped). */
40
+ get connected() {
41
+ return this.relays.filter((r) => r.open && !r.skipped).length;
42
+ }
43
+ /** Every relay has either connected, failed a connect, or been skipped: waiting longer for the rest is pointless. */
44
+ get settled() {
45
+ return this.relays.every((r) => r.attempted || r.skipped);
46
+ }
47
+ start() {
48
+ for (const r of this.relays)
49
+ this.dial(r);
50
+ }
51
+ /** Resolves true once one relay is connected, false after `timeoutMs` (or when closed). */
52
+ whenUp(timeoutMs) {
53
+ if (this.connected > 0)
54
+ return Promise.resolve(true);
55
+ return new Promise((resolve) => {
56
+ const cancel = this.o.clock.setTimeout(() => done(false), timeoutMs);
57
+ const done = (v) => {
58
+ cancel();
59
+ this.waiters = this.waiters.filter((w) => w !== check);
60
+ resolve(v);
61
+ };
62
+ const check = () => {
63
+ if (this.connected > 0)
64
+ done(true);
65
+ else if (this.closed)
66
+ done(false);
67
+ };
68
+ this.waiters.push(check);
69
+ });
70
+ }
71
+ publish(ev) {
72
+ const msg = JSON.stringify(["EVENT", ev]);
73
+ for (const r of this.relays)
74
+ if (r.open && !r.skipped)
75
+ this.trySend(r, msg);
76
+ }
77
+ close() {
78
+ this.closed = true;
79
+ for (const r of this.relays) {
80
+ r.cancelRetry?.();
81
+ r.cancelRetry = null;
82
+ this.drop(r, false);
83
+ }
84
+ this.changed();
85
+ }
86
+ changed() {
87
+ for (const w of [...this.waiters])
88
+ w();
89
+ this.o.onChange?.();
90
+ }
91
+ trySend(r, msg) {
92
+ try {
93
+ r.ws?.send(msg);
94
+ }
95
+ catch {
96
+ /* a dying socket: its onclose reconnects */
97
+ }
98
+ }
99
+ dial(r) {
100
+ if (this.closed || r.skipped)
101
+ return;
102
+ let ws;
103
+ try {
104
+ ws = this.o.connect(r.url);
105
+ }
106
+ catch {
107
+ r.attempted = true;
108
+ this.retryLater(r);
109
+ return;
110
+ }
111
+ r.ws = ws;
112
+ ws.onopen = () => {
113
+ if (r.ws !== ws)
114
+ return;
115
+ r.open = true;
116
+ r.attempted = true;
117
+ r.backoff = 1000;
118
+ const since = Math.floor(this.o.wall() / 1000) - SINCE_SLACK_S;
119
+ this.trySend(r, JSON.stringify(["REQ", this.sub, { kinds: [NOSTR_KIND], "#t": [this.o.topic], since }]));
120
+ this.changed();
121
+ };
122
+ ws.onmessage = (e) => {
123
+ if (r.ws === ws)
124
+ this.onMessage(r, String(e.data));
125
+ };
126
+ const lost = () => {
127
+ if (r.ws !== ws)
128
+ return;
129
+ const wasOpen = r.open;
130
+ r.ws = null;
131
+ r.open = false;
132
+ r.attempted = true;
133
+ if (wasOpen)
134
+ this.changed();
135
+ this.retryLater(r);
136
+ };
137
+ ws.onclose = lost;
138
+ ws.onerror = () => {
139
+ /* onclose follows; Node's WebSocket may not fire it for a failed connect, so retry from here too */
140
+ lost();
141
+ };
142
+ }
143
+ drop(r, retry) {
144
+ const ws = r.ws;
145
+ r.ws = null;
146
+ r.open = false;
147
+ try {
148
+ ws?.close();
149
+ }
150
+ catch {
151
+ /* ignore */
152
+ }
153
+ if (retry)
154
+ this.retryLater(r);
155
+ }
156
+ retryLater(r) {
157
+ if (this.closed || r.skipped || r.cancelRetry)
158
+ return;
159
+ const wait = r.backoff;
160
+ r.backoff = Math.min(30_000, r.backoff * 2);
161
+ r.cancelRetry = this.o.clock.setTimeout(() => {
162
+ r.cancelRetry = null;
163
+ this.dial(r);
164
+ }, wait);
165
+ }
166
+ skip(r) {
167
+ if (r.skipped)
168
+ return;
169
+ r.skipped = true;
170
+ r.cancelRetry?.();
171
+ r.cancelRetry = null;
172
+ this.drop(r, false);
173
+ this.changed();
174
+ }
175
+ onMessage(r, text) {
176
+ let m;
177
+ try {
178
+ m = JSON.parse(text);
179
+ }
180
+ catch {
181
+ return;
182
+ }
183
+ if (!Array.isArray(m))
184
+ return;
185
+ switch (m[0]) {
186
+ case "EVENT": {
187
+ const ev = m[2];
188
+ if (!ev || typeof ev.id !== "string" || typeof ev.pubkey !== "string" || typeof ev.content !== "string" || typeof ev.created_at !== "number")
189
+ return;
190
+ if (ev.kind !== NOSTR_KIND || !Array.isArray(ev.tags) || !ev.tags.some((t) => t[0] === "t" && t[1] === this.o.topic))
191
+ return;
192
+ if (this.seen.has(ev.id))
193
+ return;
194
+ this.seen.add(ev.id);
195
+ if (this.seen.size > SEEN_MAX)
196
+ this.seen.delete(this.seen.values().next().value);
197
+ this.o.onEvent(ev);
198
+ return;
199
+ }
200
+ case "OK":
201
+ // refused (not a plain duplicate): this relay does not take our events, skip it for the session
202
+ if (m[2] === false && !String(m[3] ?? "").startsWith("duplicate"))
203
+ this.skip(r);
204
+ return;
205
+ case "CLOSED":
206
+ this.skip(r);
207
+ return;
208
+ }
209
+ }
210
+ }
211
+ /** Default public relays (probed 2026-10-26, docs/design/no-server-fallback.md): all accept and echo ephemeral kind 25050. */
212
+ export const DEFAULT_NOSTR_RELAYS = [
213
+ "wss://nos.lol",
214
+ "wss://relay.primal.net",
215
+ "wss://nostr-pub.wellorder.net",
216
+ "wss://nostr.oxtr.dev",
217
+ "wss://nostr.bitcoiner.social",
218
+ "wss://relay.nostr.net",
219
+ "wss://nostr.sathoarder.com",
220
+ ];
221
+ /** Public STUN servers for Nostr joiners (spec s.5). */
222
+ export const DEFAULT_NOSTR_STUN = ["stun:stun.l.google.com:19302", "stun:stun.cloudflare.com:3478"];
223
+ /**
224
+ * Probes each relay: connect, subscribe, publish one kind-25050 event to itself under a random topic with a random
225
+ * key, and see it echoed. Anonymous (nothing about the player is sent). Use it to pick `hosting.nostrRelays`.
226
+ */
227
+ export async function checkNostrRelays(urls, timeoutMs = 8000, opts = {}) {
228
+ const connect = opts.connect ?? defaultRelayConnect;
229
+ const setT = (fn, ms) => {
230
+ const h = setTimeout(fn, ms);
231
+ return () => clearTimeout(h);
232
+ };
233
+ return Promise.all(urls.map((url) => checkOne(url, timeoutMs, connect, opts.clock?.setTimeout.bind(opts.clock) ?? setT)));
234
+ }
235
+ async function checkOne(url, timeoutMs, connect, setT) {
236
+ const t0 = Date.now();
237
+ const secret = new Uint8Array(32);
238
+ globalThis.crypto.getRandomValues(secret);
239
+ secret[0] &= 0x7f; // far below the curve order
240
+ if (secret.every((b) => b === 0))
241
+ secret[31] = 1;
242
+ const topic = toHex(globalThis.crypto.getRandomValues(new Uint8Array(32)));
243
+ let ev;
244
+ try {
245
+ ev = await makeEvent(secret, topic, "probe", Math.floor(Date.now() / 1000));
246
+ }
247
+ catch (e) {
248
+ return { url, ok: false, ms: 0, error: String(e) };
249
+ }
250
+ return new Promise((resolve) => {
251
+ let ws = null;
252
+ let finished = false;
253
+ let accepted = false;
254
+ const finish = (ok, error) => {
255
+ if (finished)
256
+ return;
257
+ finished = true;
258
+ cancel();
259
+ try {
260
+ ws?.close();
261
+ }
262
+ catch {
263
+ /* ignore */
264
+ }
265
+ resolve({ url, ok, ms: Date.now() - t0, ...(error ? { error } : {}) });
266
+ };
267
+ const cancel = setT(() => finish(false, accepted ? "accepted but not echoed" : "timeout"), timeoutMs);
268
+ try {
269
+ ws = connect(url);
270
+ }
271
+ catch (e) {
272
+ finish(false, String(e));
273
+ return;
274
+ }
275
+ const sub = "chk" + topic.slice(0, 8);
276
+ let published = false;
277
+ const publish = () => {
278
+ if (published)
279
+ return;
280
+ published = true;
281
+ ws.send(JSON.stringify(["EVENT", ev]));
282
+ };
283
+ ws.onopen = () => {
284
+ ws.send(JSON.stringify(["REQ", sub, { kinds: [NOSTR_KIND], "#t": [topic], since: Math.floor(Date.now() / 1000) - 30 }]));
285
+ // some relays never send EOSE for a subscription with no stored events: publish shortly anyway
286
+ setT(publish, 1500);
287
+ };
288
+ ws.onerror = () => finish(false, "connection failed");
289
+ ws.onclose = () => finish(false, "closed");
290
+ ws.onmessage = (e) => {
291
+ let m;
292
+ try {
293
+ m = JSON.parse(String(e.data));
294
+ }
295
+ catch {
296
+ return;
297
+ }
298
+ if (!Array.isArray(m))
299
+ return;
300
+ if (m[0] === "EOSE")
301
+ publish();
302
+ else if (m[0] === "OK" && m[1] === ev.id) {
303
+ if (m[2] === true)
304
+ accepted = true;
305
+ else
306
+ finish(false, `refused: ${String(m[3] ?? "")}`.slice(0, 120));
307
+ }
308
+ else if (m[0] === "CLOSED")
309
+ finish(false, `closed: ${String(m[2] ?? "")}`.slice(0, 120));
310
+ else if (m[0] === "EVENT" && m[2]?.id === ev.id)
311
+ finish(true);
312
+ };
313
+ });
314
+ }
315
+ //# sourceMappingURL=relays.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relays.js","sourceRoot":"","sources":["../../src/nostr/relays.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC;AAYhC,gHAAgH;AAChH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAc,EAAE,SAAiB,EAAE,IAAY,EAAE,IAAgB,EAAE,OAAe;IAC9G,OAAO,KAAK,CAAC,MAAM,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpH,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAkB,EAAE,KAAa,EAAE,OAAe,EAAE,SAAiB;IACnG,MAAM,MAAM,GAAG,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AACrF,CAAC;AAKD,MAAM,CAAC,MAAM,mBAAmB,GAAiB,CAAC,GAAG,EAAE,EAAE;IACvD,MAAM,EAAE,GAAI,UAA2D,CAAC,SAAS,CAAC;IAClF,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;IACpH,OAAO,IAAI,EAAE,CAAC,GAAG,CAAe,CAAC;AACnC,CAAC,CAAC;AAyBF,MAAM,QAAQ,GAAG,IAAI,CAAC;AACtB,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,uGAAuG;AACvG,MAAM,OAAO,SAAS;IAOS;IANZ,MAAM,CAAU;IAChB,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IACzB,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,GAAG,KAAK,CAAC;IACf,OAAO,GAAmB,EAAE,CAAC;IAErC,YAA6B,CAAmB;QAAnB,MAAC,GAAD,CAAC,CAAkB;QAC9C,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1I,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,oDAAoD;IACpD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IAChE,CAAC;IAED,qHAAqH;IACrH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK;QACH,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,2FAA2F;IAC3F,MAAM,CAAC,SAAiB;QACtB,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;YACrE,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,EAAE;gBAC1B,MAAM,EAAE,CAAC;gBACT,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC;gBACvD,OAAO,CAAC,CAAC,CAAC,CAAC;YACb,CAAC,CAAC;YACF,MAAM,KAAK,GAAG,GAAG,EAAE;gBACjB,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,CAAC;qBAC9B,IAAI,IAAI,CAAC,MAAM;oBAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,EAAc;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,OAAO;gBAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9E,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC5B,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YAClB,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACtB,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAEO,OAAO;QACb,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAAE,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;IACtB,CAAC;IAEO,OAAO,CAAC,CAAQ,EAAE,GAAW;QACnC,IAAI,CAAC;YACH,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACP,4CAA4C;QAC9C,CAAC;IACH,CAAC;IAEO,IAAI,CAAC,CAAQ;QACnB,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO;YAAE,OAAO;QACrC,IAAI,EAAc,CAAC;QACnB,IAAI,CAAC;YACH,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QACD,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC;QACV,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE;YACf,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE;gBAAE,OAAO;YACxB,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;YACd,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC;YACnB,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;YACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,aAAa,CAAC;YAC/D,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YACzG,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC;QACF,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,EAAE;YACnB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE;gBAAE,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,EAAE;YAChB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE;gBAAE,OAAO;YACxB,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC;YACvB,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC;YACZ,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC;YACf,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC;YACnB,IAAI,OAAO;gBAAE,IAAI,CAAC,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC,CAAC;QACF,EAAE,CAAC,OAAO,GAAG,IAAI,CAAC;QAClB,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YAChB,oGAAoG;YACpG,IAAI,EAAE,CAAC;QACT,CAAC,CAAC;IACJ,CAAC;IAEO,IAAI,CAAC,CAAQ,EAAE,KAAc;QACnC,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;QAChB,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC;QACZ,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC;QACf,IAAI,CAAC;YACH,EAAE,EAAE,KAAK,EAAE,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;QACD,IAAI,KAAK;YAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;IAEO,UAAU,CAAC,CAAQ;QACzB,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,WAAW;YAAE,OAAO;QACtD,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC;QACvB,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE;YAC3C,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACf,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC;IAEO,IAAI,CAAC,CAAQ;QACnB,IAAI,CAAC,CAAC,OAAO;YAAE,OAAO;QACtB,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QAClB,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACpB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAEO,SAAS,CAAC,CAAQ,EAAE,IAAY;QACtC,IAAI,CAAU,CAAC;QACf,IAAI,CAAC;YACH,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,OAAO;QAC9B,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACb,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAA2B,CAAC;gBAC1C,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,IAAI,OAAO,EAAE,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,EAAE,CAAC,UAAU,KAAK,QAAQ;oBAAE,OAAO;gBACrJ,IAAI,EAAE,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;oBAAE,OAAO;gBAC7H,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;oBAAE,OAAO;gBACjC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACrB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,QAAQ;oBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAe,CAAC,CAAC;gBAC3F,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,KAAK,IAAI;gBACP,gGAAgG;gBAChG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC;oBAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAChF,OAAO;YACT,KAAK,QAAQ;gBACX,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACb,OAAO;QACX,CAAC;IACH,CAAC;CACF;AAED,8HAA8H;AAC9H,MAAM,CAAC,MAAM,oBAAoB,GAAsB;IACrD,eAAe;IACf,wBAAwB;IACxB,+BAA+B;IAC/B,sBAAsB;IACtB,8BAA8B;IAC9B,uBAAuB;IACvB,4BAA4B;CAC7B,CAAC;AAEF,wDAAwD;AACxD,MAAM,CAAC,MAAM,kBAAkB,GAAsB,CAAC,8BAA8B,EAAE,+BAA+B,CAAC,CAAC;AASvH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAuB,EACvB,SAAS,GAAG,IAAI,EAChB,OAAkD,EAAE;IAEpD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,mBAAmB,CAAC;IACpD,MAAM,IAAI,GAAG,CAAC,EAAc,EAAE,EAAU,EAAE,EAAE;QAC1C,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC;IACF,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;AAC5H,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,GAAW,EAAE,SAAiB,EAAE,OAAqB,EAAE,IAAgD;IAC7H,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACtB,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IAClC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,4BAA4B;IAC/C,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAAE,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3E,IAAI,EAAc,CAAC;IACnB,IAAI,CAAC;QACH,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,OAAO,CAAa,CAAC,OAAO,EAAE,EAAE;QACzC,IAAI,EAAE,GAAsB,IAAI,CAAC;QACjC,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,KAAc,EAAE,EAAE;YAC7C,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,MAAM,EAAE,CAAC;YACT,IAAI,CAAC;gBACH,EAAE,EAAE,KAAK,EAAE,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;YACD,OAAO,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACzE,CAAC,CAAC;QACF,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;QACtG,IAAI,CAAC;YACH,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YACzB,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACtC,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,IAAI,SAAS;gBAAE,OAAO;YACtB,SAAS,GAAG,IAAI,CAAC;YACjB,EAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC1C,CAAC,CAAC;QACF,EAAE,CAAC,MAAM,GAAG,GAAG,EAAE;YACf,EAAG,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1H,+FAA+F;YAC/F,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC;QACF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;QACtD,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC3C,EAAE,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,EAAE;YACnB,IAAI,CAAU,CAAC;YACf,IAAI,CAAC;gBACH,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO;YACT,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAAE,OAAO;YAC9B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM;gBAAE,OAAO,EAAE,CAAC;iBAC1B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;gBACzC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;oBAAE,QAAQ,GAAG,IAAI,CAAC;;oBAC9B,MAAM,CAAC,KAAK,EAAE,YAAY,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YACrE,CAAC;iBAAM,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ;gBAAE,MAAM,CAAC,KAAK,EAAE,WAAW,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;iBACtF,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,IAAK,CAAC,CAAC,CAAC,CAA4B,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QAC5F,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Nostr events and the relay pool (spec/nostr-signaling.md s.3 and s.6). Anonymous: every connection uses a throw-away\n * key and carries encrypted frames only.\n */\nimport type { Clock } from \"../clock.js\";\nimport type { SocketLike } from \"../transport.js\";\nimport { sha256, toHex, fromHex } from \"./crypto.js\";\nimport { schnorrPublicKey, schnorrSign } from \"./secp256k1.js\";\n\nexport const NOSTR_KIND = 25050;\n\nexport interface NostrEvent {\n id: string;\n pubkey: string;\n created_at: number;\n kind: number;\n tags: string[][];\n content: string;\n sig: string;\n}\n\n/** NIP-01 event id: SHA-256 of `[0, pubkey, created_at, kind, tags, content]` serialised without whitespace. */\nexport async function eventId(pubkey: string, createdAt: number, kind: number, tags: string[][], content: string): Promise<string> {\n return toHex(await sha256(new TextEncoder().encode(JSON.stringify([0, pubkey, createdAt, kind, tags, content]))));\n}\n\n/** A signed kind-25050 event (BIP340 Schnorr over the id). */\nexport async function makeEvent(secret: Uint8Array, topic: string, content: string, createdAt: number): Promise<NostrEvent> {\n const pubkey = toHex(schnorrPublicKey(secret));\n const tags = [[\"t\", topic]];\n const id = await eventId(pubkey, createdAt, NOSTR_KIND, tags, content);\n const sig = toHex(await schnorrSign(fromHex(id), secret));\n return { id, pubkey, created_at: createdAt, kind: NOSTR_KIND, tags, content, sig };\n}\n\n/** The WebSocket constructor to a relay; the browser's, or Node 22+'s global one. Tests pass their own. */\nexport type RelayConnect = (url: string) => SocketLike;\n\nexport const defaultRelayConnect: RelayConnect = (url) => {\n const WS = (globalThis as { WebSocket?: new (url: string) => unknown }).WebSocket;\n if (!WS) throw new Error(\"no WebSocket in this runtime (Node 22+ has one; older Node needs advanced.nostrConnect)\");\n return new WS(url) as SocketLike;\n};\n\nexport interface RelayPoolOptions {\n urls: readonly string[];\n topic: string;\n connect: RelayConnect;\n clock: Clock;\n /** Unix ms. */\n wall: () => number;\n onEvent: (ev: NostrEvent) => void;\n /** Called when the number of connected relays changes. */\n onChange?: () => void;\n}\n\ninterface Relay {\n url: string;\n ws: SocketLike | null;\n open: boolean;\n /** The first connect to this relay has succeeded or failed (a dead relay is known to be dead). */\n attempted: boolean;\n skipped: boolean;\n backoff: number;\n cancelRetry: (() => void) | null;\n}\n\nconst SEEN_MAX = 4096;\nconst SINCE_SLACK_S = 30;\n\n/** Every relay at once, each reconnected with backoff (1 s doubling to 30 s); events deduped by id. */\nexport class RelayPool {\n private readonly relays: Relay[];\n private readonly seen = new Set<string>();\n private readonly sub = \"ol\" + Math.random().toString(16).slice(2, 10);\n private closed = false;\n private waiters: (() => void)[] = [];\n\n constructor(private readonly o: RelayPoolOptions) {\n this.relays = o.urls.map((url) => ({ url, ws: null, open: false, attempted: false, skipped: false, backoff: 1000, cancelRetry: null }));\n }\n\n get total(): number {\n return this.relays.length;\n }\n\n /** Relays connected right now (and not skipped). */\n get connected(): number {\n return this.relays.filter((r) => r.open && !r.skipped).length;\n }\n\n /** Every relay has either connected, failed a connect, or been skipped: waiting longer for the rest is pointless. */\n get settled(): boolean {\n return this.relays.every((r) => r.attempted || r.skipped);\n }\n\n start(): void {\n for (const r of this.relays) this.dial(r);\n }\n\n /** Resolves true once one relay is connected, false after `timeoutMs` (or when closed). */\n whenUp(timeoutMs: number): Promise<boolean> {\n if (this.connected > 0) return Promise.resolve(true);\n return new Promise((resolve) => {\n const cancel = this.o.clock.setTimeout(() => done(false), timeoutMs);\n const done = (v: boolean) => {\n cancel();\n this.waiters = this.waiters.filter((w) => w !== check);\n resolve(v);\n };\n const check = () => {\n if (this.connected > 0) done(true);\n else if (this.closed) done(false);\n };\n this.waiters.push(check);\n });\n }\n\n publish(ev: NostrEvent): void {\n const msg = JSON.stringify([\"EVENT\", ev]);\n for (const r of this.relays) if (r.open && !r.skipped) this.trySend(r, msg);\n }\n\n close(): void {\n this.closed = true;\n for (const r of this.relays) {\n r.cancelRetry?.();\n r.cancelRetry = null;\n this.drop(r, false);\n }\n this.changed();\n }\n\n private changed(): void {\n for (const w of [...this.waiters]) w();\n this.o.onChange?.();\n }\n\n private trySend(r: Relay, msg: string): void {\n try {\n r.ws?.send(msg);\n } catch {\n /* a dying socket: its onclose reconnects */\n }\n }\n\n private dial(r: Relay): void {\n if (this.closed || r.skipped) return;\n let ws: SocketLike;\n try {\n ws = this.o.connect(r.url);\n } catch {\n r.attempted = true;\n this.retryLater(r);\n return;\n }\n r.ws = ws;\n ws.onopen = () => {\n if (r.ws !== ws) return;\n r.open = true;\n r.attempted = true;\n r.backoff = 1000;\n const since = Math.floor(this.o.wall() / 1000) - SINCE_SLACK_S;\n this.trySend(r, JSON.stringify([\"REQ\", this.sub, { kinds: [NOSTR_KIND], \"#t\": [this.o.topic], since }]));\n this.changed();\n };\n ws.onmessage = (e) => {\n if (r.ws === ws) this.onMessage(r, String(e.data));\n };\n const lost = () => {\n if (r.ws !== ws) return;\n const wasOpen = r.open;\n r.ws = null;\n r.open = false;\n r.attempted = true;\n if (wasOpen) this.changed();\n this.retryLater(r);\n };\n ws.onclose = lost;\n ws.onerror = () => {\n /* onclose follows; Node's WebSocket may not fire it for a failed connect, so retry from here too */\n lost();\n };\n }\n\n private drop(r: Relay, retry: boolean): void {\n const ws = r.ws;\n r.ws = null;\n r.open = false;\n try {\n ws?.close();\n } catch {\n /* ignore */\n }\n if (retry) this.retryLater(r);\n }\n\n private retryLater(r: Relay): void {\n if (this.closed || r.skipped || r.cancelRetry) return;\n const wait = r.backoff;\n r.backoff = Math.min(30_000, r.backoff * 2);\n r.cancelRetry = this.o.clock.setTimeout(() => {\n r.cancelRetry = null;\n this.dial(r);\n }, wait);\n }\n\n private skip(r: Relay): void {\n if (r.skipped) return;\n r.skipped = true;\n r.cancelRetry?.();\n r.cancelRetry = null;\n this.drop(r, false);\n this.changed();\n }\n\n private onMessage(r: Relay, text: string): void {\n let m: unknown;\n try {\n m = JSON.parse(text);\n } catch {\n return;\n }\n if (!Array.isArray(m)) return;\n switch (m[0]) {\n case \"EVENT\": {\n const ev = m[2] as NostrEvent | undefined;\n if (!ev || typeof ev.id !== \"string\" || typeof ev.pubkey !== \"string\" || typeof ev.content !== \"string\" || typeof ev.created_at !== \"number\") return;\n if (ev.kind !== NOSTR_KIND || !Array.isArray(ev.tags) || !ev.tags.some((t) => t[0] === \"t\" && t[1] === this.o.topic)) return;\n if (this.seen.has(ev.id)) return;\n this.seen.add(ev.id);\n if (this.seen.size > SEEN_MAX) this.seen.delete(this.seen.values().next().value as string);\n this.o.onEvent(ev);\n return;\n }\n case \"OK\":\n // refused (not a plain duplicate): this relay does not take our events, skip it for the session\n if (m[2] === false && !String(m[3] ?? \"\").startsWith(\"duplicate\")) this.skip(r);\n return;\n case \"CLOSED\":\n this.skip(r);\n return;\n }\n }\n}\n\n/** Default public relays (probed 2026-10-26, docs/design/no-server-fallback.md): all accept and echo ephemeral kind 25050. */\nexport const DEFAULT_NOSTR_RELAYS: readonly string[] = [\n \"wss://nos.lol\",\n \"wss://relay.primal.net\",\n \"wss://nostr-pub.wellorder.net\",\n \"wss://nostr.oxtr.dev\",\n \"wss://nostr.bitcoiner.social\",\n \"wss://relay.nostr.net\",\n \"wss://nostr.sathoarder.com\",\n];\n\n/** Public STUN servers for Nostr joiners (spec s.5). */\nexport const DEFAULT_NOSTR_STUN: readonly string[] = [\"stun:stun.l.google.com:19302\", \"stun:stun.cloudflare.com:3478\"];\n\nexport interface RelayCheck {\n url: string;\n ok: boolean;\n ms: number;\n error?: string;\n}\n\n/**\n * Probes each relay: connect, subscribe, publish one kind-25050 event to itself under a random topic with a random\n * key, and see it echoed. Anonymous (nothing about the player is sent). Use it to pick `hosting.nostrRelays`.\n */\nexport async function checkNostrRelays(\n urls: readonly string[],\n timeoutMs = 8000,\n opts: { connect?: RelayConnect; clock?: Clock } = {},\n): Promise<RelayCheck[]> {\n const connect = opts.connect ?? defaultRelayConnect;\n const setT = (fn: () => void, ms: number) => {\n const h = setTimeout(fn, ms);\n return () => clearTimeout(h);\n };\n return Promise.all(urls.map((url) => checkOne(url, timeoutMs, connect, opts.clock?.setTimeout.bind(opts.clock) ?? setT)));\n}\n\nasync function checkOne(url: string, timeoutMs: number, connect: RelayConnect, setT: (fn: () => void, ms: number) => () => void): Promise<RelayCheck> {\n const t0 = Date.now();\n const secret = new Uint8Array(32);\n globalThis.crypto.getRandomValues(secret);\n secret[0] &= 0x7f; // far below the curve order\n if (secret.every((b) => b === 0)) secret[31] = 1;\n const topic = toHex(globalThis.crypto.getRandomValues(new Uint8Array(32)));\n let ev: NostrEvent;\n try {\n ev = await makeEvent(secret, topic, \"probe\", Math.floor(Date.now() / 1000));\n } catch (e) {\n return { url, ok: false, ms: 0, error: String(e) };\n }\n return new Promise<RelayCheck>((resolve) => {\n let ws: SocketLike | null = null;\n let finished = false;\n let accepted = false;\n const finish = (ok: boolean, error?: string) => {\n if (finished) return;\n finished = true;\n cancel();\n try {\n ws?.close();\n } catch {\n /* ignore */\n }\n resolve({ url, ok, ms: Date.now() - t0, ...(error ? { error } : {}) });\n };\n const cancel = setT(() => finish(false, accepted ? \"accepted but not echoed\" : \"timeout\"), timeoutMs);\n try {\n ws = connect(url);\n } catch (e) {\n finish(false, String(e));\n return;\n }\n const sub = \"chk\" + topic.slice(0, 8);\n let published = false;\n const publish = () => {\n if (published) return;\n published = true;\n ws!.send(JSON.stringify([\"EVENT\", ev]));\n };\n ws.onopen = () => {\n ws!.send(JSON.stringify([\"REQ\", sub, { kinds: [NOSTR_KIND], \"#t\": [topic], since: Math.floor(Date.now() / 1000) - 30 }]));\n // some relays never send EOSE for a subscription with no stored events: publish shortly anyway\n setT(publish, 1500);\n };\n ws.onerror = () => finish(false, \"connection failed\");\n ws.onclose = () => finish(false, \"closed\");\n ws.onmessage = (e) => {\n let m: unknown;\n try {\n m = JSON.parse(String(e.data));\n } catch {\n return;\n }\n if (!Array.isArray(m)) return;\n if (m[0] === \"EOSE\") publish();\n else if (m[0] === \"OK\" && m[1] === ev.id) {\n if (m[2] === true) accepted = true;\n else finish(false, `refused: ${String(m[3] ?? \"\")}`.slice(0, 120));\n } else if (m[0] === \"CLOSED\") finish(false, `closed: ${String(m[2] ?? \"\")}`.slice(0, 120));\n else if (m[0] === \"EVENT\" && (m[2] as NostrEvent | undefined)?.id === ev.id) finish(true);\n };\n });\n}\n"]}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * One side's connection to a Nostr room: a fresh session key pair, the relay pool, and encrypted frames in and out
3
+ * (spec/nostr-signaling.md s.2-4). {@link NostrSocket} (joiner) and {@link NostrHostBridge} (host) sit on top.
4
+ */
5
+ import type { Clock } from "../clock.js";
6
+ import type { Frame, OutFrame } from "./link.js";
7
+ import { type RelayConnect } from "./relays.js";
8
+ export interface NostrRoomOptions {
9
+ /** The version 1 join code, any display form. */
10
+ code: string;
11
+ relays?: readonly string[];
12
+ connect?: RelayConnect;
13
+ clock: Clock;
14
+ /** Unix ms; default Date.now. */
15
+ wall?: () => number;
16
+ /** The number of connected relays changed. */
17
+ onChange?: () => void;
18
+ }
19
+ export declare class NostrRoom {
20
+ private readonly secret;
21
+ private readonly keys;
22
+ private readonly wall;
23
+ readonly pub: string;
24
+ /** Called for each valid frame addressed to nobody in particular or to us (`to` empty or our pubkey). */
25
+ onFrame: ((f: Frame) => void) | null;
26
+ private readonly pool;
27
+ private closed;
28
+ private readonly pending;
29
+ private constructor();
30
+ /** Derives the keys and starts connecting. Throws on a code that is not 11 alphabet characters. */
31
+ static open(o: NostrRoomOptions): Promise<NostrRoom>;
32
+ get relaysConnected(): number;
33
+ get relaysTotal(): number;
34
+ /** Every relay has connected, failed or been skipped. */
35
+ get relaysSettled(): boolean;
36
+ whenUp(timeoutMs: number): Promise<boolean>;
37
+ /** Stamps, encrypts, signs and publishes a frame to every relay. */
38
+ send(to: string, f: OutFrame): Promise<void>;
39
+ private sendNow;
40
+ /** Closes now, or (`flush`) once the frames already handed to `send` are published (a final `close` frame). */
41
+ close(flush?: boolean): void;
42
+ private onEvent;
43
+ }
@@ -0,0 +1,104 @@
1
+ import { normalizeJoinCode } from "../joincode.js";
2
+ import { decryptContent, deriveRoomKeys, encryptContent, toHex } from "./crypto.js";
3
+ import { DEFAULT_NOSTR_RELAYS, RelayPool, defaultRelayConnect, makeEvent } from "./relays.js";
4
+ import { randomSecretKey, schnorrPublicKey } from "./secp256k1.js";
5
+ const MAX_AGE_S = 120;
6
+ export class NostrRoom {
7
+ secret;
8
+ keys;
9
+ wall;
10
+ pub;
11
+ /** Called for each valid frame addressed to nobody in particular or to us (`to` empty or our pubkey). */
12
+ onFrame = null;
13
+ pool;
14
+ closed = false;
15
+ pending = new Set();
16
+ constructor(secret, keys, o, wall) {
17
+ this.secret = secret;
18
+ this.keys = keys;
19
+ this.wall = wall;
20
+ this.pub = toHex(schnorrPublicKey(secret));
21
+ this.pool = new RelayPool({
22
+ urls: o.relays && o.relays.length > 0 ? o.relays : DEFAULT_NOSTR_RELAYS,
23
+ topic: keys.topic,
24
+ connect: o.connect ?? defaultRelayConnect,
25
+ clock: o.clock,
26
+ wall,
27
+ onEvent: (ev) => void this.onEvent(ev),
28
+ onChange: o.onChange,
29
+ });
30
+ }
31
+ /** Derives the keys and starts connecting. Throws on a code that is not 11 alphabet characters. */
32
+ static async open(o) {
33
+ const norm = normalizeJoinCode(o.code);
34
+ if (!norm)
35
+ throw new Error(`not a join code: ${o.code}`);
36
+ const keys = await deriveRoomKeys(norm);
37
+ const room = new NostrRoom(randomSecretKey(), keys, o, o.wall ?? (() => Date.now()));
38
+ room.pool.start();
39
+ return room;
40
+ }
41
+ get relaysConnected() {
42
+ return this.pool.connected;
43
+ }
44
+ get relaysTotal() {
45
+ return this.pool.total;
46
+ }
47
+ /** Every relay has connected, failed or been skipped. */
48
+ get relaysSettled() {
49
+ return this.pool.settled;
50
+ }
51
+ whenUp(timeoutMs) {
52
+ return this.pool.whenUp(timeoutMs);
53
+ }
54
+ /** Stamps, encrypts, signs and publishes a frame to every relay. */
55
+ send(to, f) {
56
+ const p = this.sendNow(to, f);
57
+ this.pending.add(p);
58
+ void p.finally(() => this.pending.delete(p));
59
+ return p;
60
+ }
61
+ async sendNow(to, f) {
62
+ if (this.closed)
63
+ return;
64
+ const frame = { v: 1, type: f.type, from: this.pub, to, seq: f.seq, ack: f.ack, ...(f.msgs ? { msgs: f.msgs } : {}), ...(f.reason ? { reason: f.reason } : {}) };
65
+ const content = await encryptContent(this.keys.key, JSON.stringify(frame));
66
+ const ev = await makeEvent(this.secret, this.keys.topic, content, Math.floor(this.wall() / 1000));
67
+ if (!this.closed)
68
+ this.pool.publish(ev);
69
+ }
70
+ /** Closes now, or (`flush`) once the frames already handed to `send` are published (a final `close` frame). */
71
+ close(flush = false) {
72
+ if (flush && this.pending.size > 0) {
73
+ void Promise.allSettled([...this.pending]).then(() => this.close());
74
+ return;
75
+ }
76
+ this.closed = true;
77
+ this.onFrame = null;
78
+ this.pool.close();
79
+ }
80
+ async onEvent(ev) {
81
+ if (this.closed || ev.pubkey === this.pub)
82
+ return;
83
+ if (this.wall() / 1000 - ev.created_at > MAX_AGE_S)
84
+ return;
85
+ const text = await decryptContent(this.keys.key, ev.content);
86
+ if (text === null || this.closed)
87
+ return;
88
+ let f;
89
+ try {
90
+ f = JSON.parse(text);
91
+ }
92
+ catch {
93
+ return;
94
+ }
95
+ if (!f || f.v !== 1 || f.from !== ev.pubkey || f.from === this.pub)
96
+ return;
97
+ if (f.type !== "open" && f.type !== "data" && f.type !== "beat" && f.type !== "close")
98
+ return;
99
+ if (typeof f.to !== "string" || (f.to !== "" && f.to !== this.pub))
100
+ return;
101
+ this.onFrame?.(f);
102
+ }
103
+ }
104
+ //# sourceMappingURL=room.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room.js","sourceRoot":"","sources":["../../src/nostr/room.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One side's connection to a Nostr room: a fresh session key pair, the relay pool, and encrypted frames in and out\n * (spec/nostr-signaling.md s.2-4). {@link NostrSocket} (joiner) and {@link NostrHostBridge} (host) sit on top.\n */\nimport type { Clock } from \"../clock.js\";\nimport { normalizeJoinCode } from \"../joincode.js\";\nimport { decryptContent, deriveRoomKeys, encryptContent, toHex } from \"./crypto.js\";\nimport type { Frame, OutFrame } from \"./link.js\";\nimport { DEFAULT_NOSTR_RELAYS, RelayPool, defaultRelayConnect, makeEvent, type NostrEvent, type RelayConnect } from \"./relays.js\";\nimport { randomSecretKey, schnorrPublicKey } from \"./secp256k1.js\";\n\nexport interface NostrRoomOptions {\n /** The version 1 join code, any display form. */\n code: string;\n relays?: readonly string[];\n connect?: RelayConnect;\n clock: Clock;\n /** Unix ms; default Date.now. */\n wall?: () => number;\n /** The number of connected relays changed. */\n onChange?: () => void;\n}\n\nconst MAX_AGE_S = 120;\n\nexport class NostrRoom {\n readonly pub: string;\n /** Called for each valid frame addressed to nobody in particular or to us (`to` empty or our pubkey). */\n onFrame: ((f: Frame) => void) | null = null;\n private readonly pool: RelayPool;\n private closed = false;\n private readonly pending = new Set<Promise<void>>();\n\n private constructor(private readonly secret: Uint8Array, private readonly keys: { key: Uint8Array; topic: string }, o: NostrRoomOptions, private readonly wall: () => number) {\n this.pub = toHex(schnorrPublicKey(secret));\n this.pool = new RelayPool({\n urls: o.relays && o.relays.length > 0 ? o.relays : DEFAULT_NOSTR_RELAYS,\n topic: keys.topic,\n connect: o.connect ?? defaultRelayConnect,\n clock: o.clock,\n wall,\n onEvent: (ev) => void this.onEvent(ev),\n onChange: o.onChange,\n });\n }\n\n /** Derives the keys and starts connecting. Throws on a code that is not 11 alphabet characters. */\n static async open(o: NostrRoomOptions): Promise<NostrRoom> {\n const norm = normalizeJoinCode(o.code);\n if (!norm) throw new Error(`not a join code: ${o.code}`);\n const keys = await deriveRoomKeys(norm);\n const room = new NostrRoom(randomSecretKey(), keys, o, o.wall ?? (() => Date.now()));\n room.pool.start();\n return room;\n }\n\n get relaysConnected(): number {\n return this.pool.connected;\n }\n\n get relaysTotal(): number {\n return this.pool.total;\n }\n\n /** Every relay has connected, failed or been skipped. */\n get relaysSettled(): boolean {\n return this.pool.settled;\n }\n\n whenUp(timeoutMs: number): Promise<boolean> {\n return this.pool.whenUp(timeoutMs);\n }\n\n /** Stamps, encrypts, signs and publishes a frame to every relay. */\n send(to: string, f: OutFrame): Promise<void> {\n const p = this.sendNow(to, f);\n this.pending.add(p);\n void p.finally(() => this.pending.delete(p));\n return p;\n }\n\n private async sendNow(to: string, f: OutFrame): Promise<void> {\n if (this.closed) return;\n const frame: Frame = { v: 1, type: f.type, from: this.pub, to, seq: f.seq, ack: f.ack, ...(f.msgs ? { msgs: f.msgs } : {}), ...(f.reason ? { reason: f.reason } : {}) };\n const content = await encryptContent(this.keys.key, JSON.stringify(frame));\n const ev = await makeEvent(this.secret, this.keys.topic, content, Math.floor(this.wall() / 1000));\n if (!this.closed) this.pool.publish(ev);\n }\n\n /** Closes now, or (`flush`) once the frames already handed to `send` are published (a final `close` frame). */\n close(flush = false): void {\n if (flush && this.pending.size > 0) {\n void Promise.allSettled([...this.pending]).then(() => this.close());\n return;\n }\n this.closed = true;\n this.onFrame = null;\n this.pool.close();\n }\n\n private async onEvent(ev: NostrEvent): Promise<void> {\n if (this.closed || ev.pubkey === this.pub) return;\n if (this.wall() / 1000 - ev.created_at > MAX_AGE_S) return;\n const text = await decryptContent(this.keys.key, ev.content);\n if (text === null || this.closed) return;\n let f: Frame;\n try {\n f = JSON.parse(text) as Frame;\n } catch {\n return;\n }\n if (!f || f.v !== 1 || f.from !== ev.pubkey || f.from === this.pub) return;\n if (f.type !== \"open\" && f.type !== \"data\" && f.type !== \"beat\" && f.type !== \"close\") return;\n if (typeof f.to !== \"string\" || (f.to !== \"\" && f.to !== this.pub)) return;\n this.onFrame?.(f);\n }\n}\n"]}
@@ -0,0 +1,10 @@
1
+ export declare const bytesToInt: (b: Uint8Array) => bigint;
2
+ export declare function intToBytes(n: bigint, len?: number): Uint8Array;
3
+ /** The 32-byte x-only public key for a 32-byte secret key. */
4
+ export declare function schnorrPublicKey(secret: Uint8Array): Uint8Array;
5
+ /** A fresh random secret key (valid scalar) from the platform CSPRNG. */
6
+ export declare function randomSecretKey(): Uint8Array;
7
+ /** BIP340 signature (64 bytes) of a 32-byte message. `aux` is 32 bytes of randomness (default: fresh). */
8
+ export declare function schnorrSign(msg: Uint8Array, secret: Uint8Array, aux?: Uint8Array): Promise<Uint8Array>;
9
+ /** BIP340 verification. */
10
+ export declare function schnorrVerify(sig: Uint8Array, msg: Uint8Array, pub: Uint8Array): Promise<boolean>;