@hashtree/worker 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +7 -3
  2. package/src/app-runtime.ts +393 -0
  3. package/src/capabilities/blossomBandwidthTracker.ts +74 -0
  4. package/src/capabilities/blossomTransport.ts +179 -0
  5. package/src/capabilities/connectivity.ts +54 -0
  6. package/src/capabilities/idbStorage.ts +94 -0
  7. package/src/capabilities/meshRouterStore.ts +426 -0
  8. package/src/capabilities/rootResolver.ts +497 -0
  9. package/src/client-id.ts +137 -0
  10. package/src/client.ts +501 -0
  11. package/src/entry.ts +3 -0
  12. package/src/htree-path.ts +53 -0
  13. package/src/htree-url.ts +156 -0
  14. package/src/index.ts +76 -0
  15. package/src/mediaStreaming.ts +64 -0
  16. package/src/p2p/boundedQueue.ts +168 -0
  17. package/src/p2p/errorMessage.ts +6 -0
  18. package/src/p2p/index.ts +48 -0
  19. package/src/p2p/lruCache.ts +78 -0
  20. package/src/p2p/meshQueryRouter.ts +361 -0
  21. package/src/p2p/protocol.ts +11 -0
  22. package/src/p2p/queryForwardingMachine.ts +197 -0
  23. package/src/p2p/signaling.ts +284 -0
  24. package/src/p2p/uploadRateLimiter.ts +85 -0
  25. package/src/p2p/webrtcController.ts +1168 -0
  26. package/src/p2p/webrtcProxy.ts +519 -0
  27. package/src/privacyGuards.ts +31 -0
  28. package/src/protocol.ts +124 -0
  29. package/src/relay/identity.ts +86 -0
  30. package/src/relay/mediaHandler.ts +1633 -0
  31. package/src/relay/ndk.ts +590 -0
  32. package/src/relay/nostr-wasm.ts +249 -0
  33. package/src/relay/nostr.ts +249 -0
  34. package/src/relay/protocol.ts +361 -0
  35. package/src/relay/publicAssetUrl.ts +25 -0
  36. package/src/relay/rootPathResolver.ts +50 -0
  37. package/src/relay/shims.d.ts +17 -0
  38. package/src/relay/signing.ts +332 -0
  39. package/src/relay/treeRootCache.ts +354 -0
  40. package/src/relay/treeRootSubscription.ts +577 -0
  41. package/src/relay/utils/constants.ts +139 -0
  42. package/src/relay/utils/errorMessage.ts +7 -0
  43. package/src/relay/utils/lruCache.ts +79 -0
  44. package/src/relay/webrtc.ts +5 -0
  45. package/src/relay/webrtcSignaling.ts +108 -0
  46. package/src/relay/worker.ts +1787 -0
  47. package/src/relay-client.ts +265 -0
  48. package/src/relay-entry.ts +1 -0
  49. package/src/runtime-network.ts +134 -0
  50. package/src/runtime.ts +153 -0
  51. package/src/transferableBytes.ts +5 -0
  52. package/src/tree-root.ts +851 -0
  53. package/src/types.ts +8 -0
  54. package/src/worker.ts +975 -0
@@ -0,0 +1,249 @@
1
+ // @ts-nocheck
2
+ function defineWasmEnv(label) {
3
+ label += ': ';
4
+ let AB_HEAP;
5
+ let ATU8_HEAP;
6
+ let ATU32_HEAP;
7
+
8
+ const console_out = (s_channel, s_out) => console[s_channel](label + s_out.replace(/\0/g, '\n'));
9
+ let s_error = '';
10
+ // for converting bytes to text
11
+ const utf8 = new TextDecoder();
12
+ const h_fds = {
13
+ // stdout
14
+ 1(s_out) {
15
+ console_out('debug', s_out);
16
+ },
17
+ // stderr
18
+ 2(s_out) {
19
+ console_out('error', (s_error = s_out));
20
+ }
21
+ };
22
+ const imports = {
23
+ abort() {
24
+ throw Error(label + (s_error || 'An unknown error occurred'));
25
+ },
26
+ memcpy: (ip_dst, ip_src, nb_size) => ATU8_HEAP.copyWithin(ip_dst, ip_src, ip_src + nb_size),
27
+ resize(w) {
28
+ throw Error(label + `Out of memory (resizing ${w})`);
29
+ },
30
+ write(i_fd, ip_iov, nl_iovs, ip_written) {
31
+ // output string
32
+ let s_out = '';
33
+ // track number of bytes read from buffers
34
+ let cb_read = 0;
35
+ // each pending iov
36
+ for (let i_iov = 0; i_iov < nl_iovs; i_iov++) {
37
+ // start of buffer in memory
38
+ const ip_start = ATU32_HEAP[ip_iov >> 2];
39
+ // size of buffer
40
+ const nb_len = ATU32_HEAP[(ip_iov + 4) >> 2];
41
+ ip_iov += 8;
42
+ // extract text from buffer
43
+ s_out += utf8.decode(ATU8_HEAP.subarray(ip_start, ip_start + nb_len));
44
+ // update number of bytes read
45
+ cb_read += nb_len;
46
+ }
47
+ // route to fd
48
+ if (h_fds[i_fd]) {
49
+ h_fds[i_fd](s_out);
50
+ }
51
+ else {
52
+ // no fd found
53
+ throw new Error(`libsecp256k1 tried writing to non-open file descriptor: ${i_fd}\n${s_out}`);
54
+ }
55
+ // write bytes read
56
+ ATU32_HEAP[ip_written >> 2] = cb_read;
57
+ // no error
58
+ return 0;
59
+ }
60
+ };
61
+ return [
62
+ imports,
63
+ (d_memory) => [
64
+ (AB_HEAP = d_memory.buffer),
65
+ (ATU8_HEAP = new Uint8Array(AB_HEAP)),
66
+ (ATU32_HEAP = new Uint32Array(AB_HEAP))
67
+ ]
68
+ ];
69
+ }
70
+
71
+ /*
72
+ * ================================
73
+ * GENERATED FILE WARNING
74
+ * Do not edit this file manually.
75
+ * ================================
76
+ */
77
+ const map_wasm_imports = (g_imports) => ({
78
+ a: {
79
+ a: g_imports.abort,
80
+ f: g_imports.memcpy,
81
+ d: g_imports.resize,
82
+ e: () => 52, // _fd_close,
83
+ c: () => 70, // _fd_seek,
84
+ b: g_imports.write,
85
+ },
86
+ });
87
+ const map_wasm_exports = (g_exports) => ({
88
+ malloc: g_exports['i'],
89
+ free: g_exports['j'],
90
+ sha256_initialize: g_exports['l'],
91
+ sha256_write: g_exports['m'],
92
+ sha256_finalize: g_exports['n'],
93
+ context_create: g_exports['o'],
94
+ xonly_pubkey_parse: g_exports['p'],
95
+ xonly_pubkey_serialize: g_exports['q'],
96
+ keypair_create: g_exports['r'],
97
+ keypair_xonly_pub: g_exports['s'],
98
+ schnorrsig_sign32: g_exports['t'],
99
+ schnorrsig_verify: g_exports['u'],
100
+ sbrk: g_exports['sbrk'],
101
+ memory: g_exports['g'],
102
+ init: () => g_exports['h'](),
103
+ });
104
+
105
+ /**
106
+ * Creates a new instance of the secp256k1 WASM and returns the Nostr wrapper
107
+ * @param z_src - a Response containing the WASM binary, a Promise that resolves to one,
108
+ * or the raw bytes to the WASM binary as a {@link BufferSource}
109
+ * @returns the wrapper API
110
+ */
111
+ const NostrWasm = async (z_src) => {
112
+ // prepare the runtime
113
+ const [defs, f_bind_heap] = defineWasmEnv('nostr-wasm');
114
+ const g_imports = map_wasm_imports(defs);
115
+ // prep the wasm module
116
+ let d_wasm;
117
+ // instantiate wasm binary by streaming the response bytes
118
+ if (z_src instanceof Response || z_src instanceof Promise) {
119
+ d_wasm = await WebAssembly.instantiateStreaming(z_src, g_imports);
120
+ }
121
+ else {
122
+ // instantiate using raw binary
123
+ d_wasm = await WebAssembly.instantiate(z_src, g_imports);
124
+ }
125
+ // create the exports struct
126
+ const g_wasm = map_wasm_exports(d_wasm.instance.exports);
127
+ // bind the heap and ref its view(s)
128
+ const [, ATU8_HEAP] = f_bind_heap(g_wasm.memory);
129
+ // call into the wasm module's init method
130
+ g_wasm.init();
131
+ const ip_sk = g_wasm.malloc(32 /* ByteLens.PRIVATE_KEY */);
132
+ const ip_ent = g_wasm.malloc(32 /* ByteLens.NONCE_ENTROPY */);
133
+ const ip_msg_hash = g_wasm.malloc(32 /* ByteLens.MSG_HASH */);
134
+ // scratch spaces
135
+ const ip_pubkey_scratch = g_wasm.malloc(32 /* ByteLens.XONLY_PUBKEY */);
136
+ const ip_sig_scratch = g_wasm.malloc(64 /* ByteLens.BIP340_SIG */);
137
+ // library handle: secp256k1_keypair;
138
+ const ip_keypair = g_wasm.malloc(96 /* ByteLens.KEYPAIR_LIB */);
139
+ // library handle: secp256k1_xonly_pubkey;
140
+ const ip_xonly_pubkey = g_wasm.malloc(64 /* ByteLens.XONLY_KEY_LIB */);
141
+ // library handle: secp256k1_sha256;
142
+ const ip_sha256 = g_wasm.malloc(104 /* ByteLens.SHA256_LIB */);
143
+ // create a reusable context
144
+ const ip_ctx = g_wasm.context_create(513 /* Flags.CONTEXT_SIGN */ | 257 /* Flags.CONTEXT_VERIFY */);
145
+ // an encoder for hashing strings
146
+ const utf8 = new TextEncoder();
147
+ /**
148
+ * Puts the given private key into program memory, runs the given callback, then zeroes out the key
149
+ * @param atu8_sk - the private key
150
+ * @param f_use - callback to use the key
151
+ * @returns whatever the callback returns
152
+ */
153
+ const with_keypair = (atu8_sk, f_use) => {
154
+ // prep callback return
155
+ let w_return;
156
+ // in case of any exception..
157
+ try {
158
+ // copy input bytes into place
159
+ ATU8_HEAP.set(atu8_sk, ip_sk);
160
+ // instantiate keypair
161
+ g_wasm.keypair_create(ip_ctx, ip_keypair, ip_sk);
162
+ // use private key
163
+ w_return = f_use();
164
+ }
165
+ finally {
166
+ // zero-out private key and keypair
167
+ ATU8_HEAP.fill(1, ip_sk, ip_sk + 32 /* ByteLens.PRIVATE_KEY */);
168
+ ATU8_HEAP.fill(2, ip_keypair, ip_keypair + 96 /* ByteLens.KEYPAIR_LIB */);
169
+ }
170
+ // forward result
171
+ return w_return;
172
+ };
173
+ const compute_event_id = (event) => {
174
+ const message = utf8.encode(`[0,"${event.pubkey}",${event.created_at},${event.kind},${JSON.stringify(event.tags)},${JSON.stringify(event.content)}]`);
175
+ const ip_message = g_wasm.malloc(message.length);
176
+ ATU8_HEAP.set(message, ip_message);
177
+ g_wasm.sha256_initialize(ip_sha256);
178
+ g_wasm.sha256_write(ip_sha256, ip_message, message.length);
179
+ g_wasm.sha256_finalize(ip_sha256, ip_msg_hash);
180
+ g_wasm.free(ip_message);
181
+ return ATU8_HEAP.slice(ip_msg_hash, ip_msg_hash + 32 /* ByteLens.MSG_HASH */);
182
+ };
183
+ return {
184
+ generateSecretKey: () => crypto.getRandomValues(new Uint8Array(32 /* ByteLens.PRIVATE_KEY */)),
185
+ getPublicKey(sk) {
186
+ if (1 /* BinaryResult.SUCCESS */ !==
187
+ with_keypair(sk, () => g_wasm.keypair_xonly_pub(ip_ctx, ip_xonly_pubkey, null, ip_keypair))) {
188
+ throw Error('failed to get pubkey from keypair');
189
+ }
190
+ // serialize the public key
191
+ g_wasm.xonly_pubkey_serialize(ip_ctx, ip_pubkey_scratch, ip_xonly_pubkey);
192
+ // extract result
193
+ return ATU8_HEAP.slice(ip_pubkey_scratch, ip_pubkey_scratch + 32 /* ByteLens.XONLY_PUBKEY */);
194
+ },
195
+ finalizeEvent(event, seckey, ent) {
196
+ with_keypair(seckey, () => {
197
+ // get public key (as in getPublicKey function above)
198
+ g_wasm.keypair_xonly_pub(ip_ctx, ip_xonly_pubkey, null, ip_keypair);
199
+ g_wasm.xonly_pubkey_serialize(ip_ctx, ip_pubkey_scratch, ip_xonly_pubkey);
200
+ const pubkey = ATU8_HEAP.slice(ip_pubkey_scratch, ip_pubkey_scratch + 32 /* ByteLens.XONLY_PUBKEY */);
201
+ event.pubkey = toHex(pubkey);
202
+ // compute event id
203
+ event.id = toHex(compute_event_id(event));
204
+ // copy entropy bytes into place, if they are provided
205
+ if (!ent && crypto.getRandomValues) {
206
+ ATU8_HEAP.set(crypto.getRandomValues(new Uint8Array(32)), ip_ent);
207
+ }
208
+ // perform signature (ip_msg_hash is already set from procedure above)
209
+ if (1 /* BinaryResult.SUCCESS */ !==
210
+ g_wasm.schnorrsig_sign32(ip_ctx, ip_sig_scratch, ip_msg_hash, ip_keypair, ip_ent)) {
211
+ throw Error('failed to sign');
212
+ }
213
+ });
214
+ const sig = ATU8_HEAP.slice(ip_sig_scratch, ip_sig_scratch + 64 /* ByteLens.BIP340_SIG */);
215
+ event.sig = toHex(sig);
216
+ },
217
+ verifyEvent(event) {
218
+ const id = fromHex(event.id);
219
+ // check event hash
220
+ const computed = compute_event_id(event);
221
+ for (let i = 0; i < id.length; i++) {
222
+ if (id[i] !== computed[i])
223
+ throw Error('id is invalid');
224
+ }
225
+ // copy event data into place
226
+ ATU8_HEAP.set(fromHex(event.sig), ip_sig_scratch);
227
+ ATU8_HEAP.set(fromHex(event.id), ip_msg_hash);
228
+ ATU8_HEAP.set(fromHex(event.pubkey), ip_pubkey_scratch);
229
+ // parse the public key
230
+ if (1 /* BinaryResult.SUCCESS */ !==
231
+ g_wasm.xonly_pubkey_parse(ip_ctx, ip_xonly_pubkey, ip_pubkey_scratch)) {
232
+ throw Error('pubkey is invalid');
233
+ }
234
+ // verify the signature
235
+ if (1 /* BinaryResult.SUCCESS */ !==
236
+ g_wasm.schnorrsig_verify(ip_ctx, ip_sig_scratch, ip_msg_hash, 32 /* ByteLens.MSG_HASH */, ip_xonly_pubkey)) {
237
+ throw Error('signature is invalid');
238
+ }
239
+ }
240
+ };
241
+ };
242
+ function toHex(bytes) {
243
+ return bytes.reduce((hex, byte) => hex + byte.toString(16).padStart(2, '0'), '');
244
+ }
245
+ function fromHex(hex) {
246
+ return new Uint8Array(hex.length / 2).map((_, i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16));
247
+ }
248
+
249
+ export { NostrWasm };
@@ -0,0 +1,249 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Nostr Relay Manager for Worker
4
+ *
5
+ * Manages WebSocket connections to Nostr relays using nostr-tools.
6
+ * Provides subscribe/publish functionality for the worker.
7
+ *
8
+ * Used for:
9
+ * - WebRTC signaling (kind 25050 ephemeral)
10
+ * - Tree root resolution (kind 30078)
11
+ */
12
+
13
+ import { SimplePool, type Filter, type Event } from 'nostr-tools';
14
+ import type { NostrFilter, SignedEvent, RelayStats } from './protocol';
15
+
16
+ // Subscription entry
17
+ interface Subscription {
18
+ id: string;
19
+ filters: NostrFilter[];
20
+ subs: ReturnType<SimplePool['subscribe']>[];
21
+ }
22
+
23
+ // Relay connection stats
24
+ interface RelayStatsInternal {
25
+ url: string;
26
+ connected: boolean;
27
+ eventsReceived: number;
28
+ eventsSent: number;
29
+ }
30
+
31
+ export class NostrManager {
32
+ private pool: SimplePool;
33
+ private relays: string[] = [];
34
+ private subscriptions = new Map<string, Subscription>();
35
+ private relayStats = new Map<string, RelayStatsInternal>();
36
+ private onEvent: ((subId: string, event: SignedEvent) => void) | null = null;
37
+ private onEose: ((subId: string) => void) | null = null;
38
+
39
+ constructor() {
40
+ this.pool = new SimplePool();
41
+ }
42
+
43
+ /**
44
+ * Initialize with relay URLs
45
+ */
46
+ init(relays: string[]): void {
47
+ this.relays = relays;
48
+
49
+ // Initialize stats for each relay
50
+ for (const url of relays) {
51
+ this.relayStats.set(url, {
52
+ url,
53
+ connected: false,
54
+ eventsReceived: 0,
55
+ eventsSent: 0,
56
+ });
57
+ }
58
+
59
+ console.log('[NostrManager] Initialized with relays:', relays);
60
+ }
61
+
62
+ /**
63
+ * Set event callback
64
+ */
65
+ setOnEvent(callback: (subId: string, event: SignedEvent) => void): void {
66
+ this.onEvent = callback;
67
+ }
68
+
69
+ /**
70
+ * Set EOSE callback
71
+ */
72
+ setOnEose(callback: (subId: string) => void): void {
73
+ this.onEose = callback;
74
+ }
75
+
76
+ /**
77
+ * Subscribe to events matching filters
78
+ */
79
+ subscribe(subId: string, filters: NostrFilter[]): void {
80
+ // Close existing subscription with same ID if any
81
+ this.unsubscribe(subId);
82
+
83
+ console.log('[NostrManager] Creating subscription:', subId, 'to relays:', this.relays, 'filters:', filters);
84
+
85
+ // Convert our NostrFilter to nostr-tools Filter
86
+ // Subscribe to each filter separately and track them
87
+ const subs: ReturnType<SimplePool['subscribe']>[] = [];
88
+
89
+ for (const f of filters) {
90
+ // Build filter with any tag filters (e.g., #e, #p, #d, #l)
91
+ const poolFilter: Filter = {
92
+ ids: f.ids,
93
+ authors: f.authors,
94
+ kinds: f.kinds,
95
+ since: f.since,
96
+ until: f.until,
97
+ limit: f.limit,
98
+ };
99
+
100
+ // Copy any tag filters (keys starting with #)
101
+ for (const key of Object.keys(f)) {
102
+ if (key.startsWith('#') && f[key]) {
103
+ (poolFilter as Record<string, unknown>)[key] = f[key];
104
+ }
105
+ }
106
+
107
+ try {
108
+ console.log('[NostrManager] pool.subscribe called with filter:', poolFilter);
109
+ const sub = this.pool.subscribe(this.relays, poolFilter, {
110
+ onevent: (event: Event) => {
111
+ console.log('[NostrManager] Received event:', event.kind, 'from:', event.pubkey?.slice(0, 8), 'id:', event.id?.slice(0, 8));
112
+ // Convert to SignedEvent
113
+ const signedEvent: SignedEvent = {
114
+ id: event.id,
115
+ pubkey: event.pubkey,
116
+ kind: event.kind,
117
+ content: event.content,
118
+ tags: event.tags,
119
+ created_at: event.created_at,
120
+ sig: event.sig,
121
+ };
122
+
123
+ this.onEvent?.(subId, signedEvent);
124
+ },
125
+ oneose: () => {
126
+ console.log('[NostrManager] EOSE for sub:', subId);
127
+ this.onEose?.(subId);
128
+ },
129
+ onerror: (err: Error) => {
130
+ console.error('[NostrManager] Subscription error:', subId, err);
131
+ },
132
+ });
133
+ console.log('[NostrManager] Subscription object:', typeof sub, sub);
134
+ subs.push(sub);
135
+ } catch (err) {
136
+ console.error('[NostrManager] Error creating subscription:', subId, err);
137
+ }
138
+ }
139
+
140
+ // Store all subs for this subscription ID
141
+ this.subscriptions.set(subId, { id: subId, filters, subs });
142
+ console.log('[NostrManager] Subscribed:', subId, filters);
143
+ }
144
+
145
+ /**
146
+ * Unsubscribe from a subscription
147
+ */
148
+ unsubscribe(subId: string): void {
149
+ const sub = this.subscriptions.get(subId);
150
+ if (sub) {
151
+ for (const s of sub.subs) {
152
+ s.close();
153
+ }
154
+ this.subscriptions.delete(subId);
155
+ console.log('[NostrManager] Unsubscribed:', subId);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Publish an event to all relays
161
+ */
162
+ async publish(event: SignedEvent): Promise<void> {
163
+ // Convert to nostr-tools Event
164
+ const poolEvent: Event = {
165
+ id: event.id,
166
+ pubkey: event.pubkey,
167
+ kind: event.kind,
168
+ content: event.content,
169
+ tags: event.tags,
170
+ created_at: event.created_at,
171
+ sig: event.sig,
172
+ };
173
+
174
+ try {
175
+ await Promise.any(
176
+ this.pool.publish(this.relays, poolEvent)
177
+ );
178
+
179
+ // Update stats for successful publish
180
+ for (const [, stats] of this.relayStats) {
181
+ stats.eventsSent++;
182
+ }
183
+
184
+ console.log('[NostrManager] Published event:', event.id);
185
+ } catch (err) {
186
+ console.error('[NostrManager] Failed to publish:', err);
187
+ throw err;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get relay connection stats
193
+ */
194
+ getRelayStats(): RelayStats[] {
195
+ const result: RelayStats[] = [];
196
+ for (const [url, stats] of this.relayStats) {
197
+ result.push({
198
+ url,
199
+ connected: stats.connected,
200
+ eventsReceived: stats.eventsReceived,
201
+ eventsSent: stats.eventsSent,
202
+ });
203
+ }
204
+ return result;
205
+ }
206
+
207
+ /**
208
+ * Update relay connection status
209
+ * Called when connection state changes
210
+ */
211
+ setRelayConnected(url: string, connected: boolean): void {
212
+ const stats = this.relayStats.get(url);
213
+ if (stats) {
214
+ stats.connected = connected;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Close all subscriptions and connections
220
+ */
221
+ close(): void {
222
+ for (const [subId, sub] of this.subscriptions) {
223
+ for (const s of sub.subs) {
224
+ s.close();
225
+ }
226
+ console.log('[NostrManager] Closed subscription:', subId);
227
+ }
228
+ this.subscriptions.clear();
229
+ this.pool.close(this.relays);
230
+ console.log('[NostrManager] Closed');
231
+ }
232
+ }
233
+
234
+ // Singleton instance for the worker
235
+ let instance: NostrManager | null = null;
236
+
237
+ export function getNostrManager(): NostrManager {
238
+ if (!instance) {
239
+ instance = new NostrManager();
240
+ }
241
+ return instance;
242
+ }
243
+
244
+ export function closeNostrManager(): void {
245
+ if (instance) {
246
+ instance.close();
247
+ instance = null;
248
+ }
249
+ }