@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,361 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Worker Protocol Types
4
+ *
5
+ * Message types for communication between main thread and hashtree worker.
6
+ * Worker owns: HashTree, WebRTC, Nostr (via nostr-tools)
7
+ * Main thread owns: UI, NIP-07 extension access (signing/encryption)
8
+ */
9
+
10
+ import type { CID } from '@hashtree/core';
11
+ import type { BlossomBandwidthStats } from '@hashtree/core/worker';
12
+
13
+ // Re-export common types from hashtree's worker protocol
14
+ export type {
15
+ NostrFilter,
16
+ UnsignedEvent,
17
+ SignedEvent,
18
+ SocialGraphEvent,
19
+ BlossomBandwidthStats,
20
+ BlossomBandwidthServerStats,
21
+ } from '@hashtree/core/worker';
22
+
23
+ // Tree visibility levels
24
+ export type TreeVisibility = 'public' | 'link-visible' | 'private';
25
+
26
+ export interface TreeRootInfo {
27
+ hash: Uint8Array;
28
+ key?: Uint8Array;
29
+ visibility: TreeVisibility;
30
+ labels?: string[];
31
+ updatedAt: number;
32
+ snapshotNhash?: string;
33
+ encryptedKey?: string;
34
+ keyId?: string;
35
+ selfEncryptedKey?: string;
36
+ selfEncryptedLinkKey?: string;
37
+ }
38
+
39
+ // Extended peer stats with pool classification for apps that group peers.
40
+ export interface PeerStats {
41
+ peerId: string;
42
+ pubkey: string;
43
+ connected: boolean;
44
+ pool: 'follows' | 'other';
45
+ requestsSent: number;
46
+ requestsReceived: number;
47
+ responsesSent: number;
48
+ responsesReceived: number;
49
+ bytesSent: number;
50
+ bytesReceived: number;
51
+ forwardedRequests: number;
52
+ forwardedResolved: number;
53
+ forwardedSuppressed: number;
54
+ }
55
+
56
+ // ============================================================================
57
+ // Main Thread → Worker Messages
58
+ // ============================================================================
59
+
60
+ export type WorkerRequest =
61
+ // Lifecycle
62
+ | { type: 'init'; id: string; config: WorkerConfig }
63
+ | { type: 'close'; id: string }
64
+ | { type: 'setIdentity'; id: string; pubkey: string; nsec?: string }
65
+
66
+ // Store operations (low-level hash-based)
67
+ | { type: 'get'; id: string; hash: Uint8Array }
68
+ | { type: 'put'; id: string; hash: Uint8Array; data: Uint8Array }
69
+ | { type: 'has'; id: string; hash: Uint8Array }
70
+ | { type: 'delete'; id: string; hash: Uint8Array }
71
+
72
+ // Tree operations (high-level CID-based)
73
+ | { type: 'readFile'; id: string; cid: CID }
74
+ | { type: 'readFileRange'; id: string; cid: CID; start: number; end?: number }
75
+ | { type: 'readFileStream'; id: string; cid: CID }
76
+ | { type: 'writeFile'; id: string; parentCid: CID | null; path: string; data: Uint8Array }
77
+ | { type: 'deleteFile'; id: string; parentCid: CID; path: string }
78
+ | { type: 'listDir'; id: string; cid: CID }
79
+ | { type: 'resolveRoot'; id: string; npub: string; path?: string }
80
+
81
+ // Tree root cache and subscriptions
82
+ | {
83
+ type: 'setTreeRootCache';
84
+ id: string;
85
+ npub: string;
86
+ treeName: string;
87
+ hash: Uint8Array;
88
+ key?: Uint8Array;
89
+ visibility: TreeVisibility;
90
+ labels?: string[];
91
+ encryptedKey?: string;
92
+ keyId?: string;
93
+ selfEncryptedKey?: string;
94
+ selfEncryptedLinkKey?: string;
95
+ }
96
+ | { type: 'getTreeRootInfo'; id: string; npub: string; treeName: string }
97
+ | { type: 'mergeTreeRootKey'; id: string; npub: string; treeName: string; hash: Uint8Array; key: Uint8Array }
98
+ | { type: 'subscribeTreeRoots'; id: string; pubkey: string }
99
+ | { type: 'unsubscribeTreeRoots'; id: string; pubkey: string }
100
+
101
+ // Nostr subscriptions
102
+ | { type: 'subscribe'; id: string; filters: NostrFilter[] }
103
+ | { type: 'unsubscribe'; id: string; subId: string }
104
+ | { type: 'publish'; id: string; event: SignedEvent }
105
+
106
+ // Media streaming (service worker registers a MessagePort)
107
+ | { type: 'registerMediaPort'; port: MessagePort; debug?: boolean }
108
+
109
+ // Stats
110
+ | { type: 'getPeerStats'; id: string }
111
+ | { type: 'getRelayStats'; id: string }
112
+ | { type: 'getStorageStats'; id: string }
113
+
114
+ // WebRTC pool configuration
115
+ | { type: 'setWebRTCPools'; id: string; pools: { follows: { max: number; satisfied: number }; other: { max: number; satisfied: number } } }
116
+ | { type: 'setWebRTCForwardRateLimit'; id: string; forwardRateLimit?: ForwardRateLimitConfig }
117
+ | { type: 'sendWebRTCHello'; id: string }
118
+ | { type: 'setFollows'; id: string; follows: string[] }
119
+
120
+ // Blossom configuration
121
+ | { type: 'setBlossomServers'; id: string; servers: BlossomServerConfig[] }
122
+
123
+ // Storage configuration
124
+ | { type: 'setStorageMaxBytes'; id: string; maxBytes: number }
125
+
126
+ // Blossom upload
127
+ | { type: 'pushToBlossom'; id: string; cidHash: Uint8Array; cidKey?: Uint8Array; treeName?: string }
128
+ | { type: 'startBlossomSession'; id: string; sessionId: string; totalChunks: number }
129
+ | { type: 'endBlossomSession'; id: string }
130
+
131
+ // Republish cached events
132
+ | { type: 'republishTrees'; id: string }
133
+ | { type: 'republishTree'; id: string; pubkey: string; treeName: string }
134
+
135
+ // Heartbeat
136
+ | { type: 'ping'; id: string }
137
+
138
+ // SocialGraph
139
+ | { type: 'initSocialGraph'; id: string; rootPubkey?: string }
140
+ | { type: 'setSocialGraphRoot'; id: string; pubkey: string }
141
+ | { type: 'handleSocialGraphEvents'; id: string; events: SocialGraphEvent[] }
142
+ | { type: 'getFollowDistance'; id: string; pubkey: string }
143
+ | { type: 'isFollowing'; id: string; follower: string; followed: string }
144
+ | { type: 'getFollows'; id: string; pubkey: string }
145
+ | { type: 'getFollowers'; id: string; pubkey: string }
146
+ | { type: 'getFollowedByFriends'; id: string; pubkey: string }
147
+ | { type: 'getSocialGraphSize'; id: string }
148
+
149
+ // NIP-07 responses (main thread → worker, after signing/encryption)
150
+ | { type: 'signed'; id: string; event?: SignedEvent; error?: string }
151
+ | { type: 'encrypted'; id: string; ciphertext?: string; error?: string }
152
+ | { type: 'decrypted'; id: string; plaintext?: string; error?: string }
153
+
154
+ // WebRTC proxy events (main thread reports to worker)
155
+ | WebRTCEvent;
156
+
157
+ /** Blossom server configuration */
158
+ export interface BlossomServerConfig {
159
+ url: string;
160
+ read?: boolean; // Whether to read from this server (default true)
161
+ write?: boolean; // Whether to write to this server
162
+ }
163
+
164
+ export interface ForwardRateLimitConfig {
165
+ maxForwardsPerPeerWindow?: number;
166
+ windowMs?: number;
167
+ }
168
+
169
+ export interface WorkerConfig {
170
+ relays: string[];
171
+ blossomServers?: BlossomServerConfig[]; // Blossom servers with read/write config
172
+ pubkey: string; // User's pubkey (required - user always logged in)
173
+ nsec?: string; // Hex-encoded secret key (only for nsec login, not extension)
174
+ storeName?: string; // IndexedDB database name, defaults to 'hashtree-worker'
175
+ forwardRateLimit?: ForwardRateLimitConfig;
176
+ }
177
+
178
+ // ============================================================================
179
+ // Worker → Main Thread Messages
180
+ // ============================================================================
181
+
182
+ export type WorkerResponse =
183
+ // Lifecycle
184
+ | { type: 'ready' }
185
+ | { type: 'pong'; id: string }
186
+ | { type: 'error'; id?: string; error: string }
187
+
188
+ // Generic responses
189
+ | { type: 'result'; id: string; data?: Uint8Array; error?: string }
190
+ | { type: 'bool'; id: string; value: boolean; error?: string }
191
+ | { type: 'cid'; id: string; cid?: CID; error?: string }
192
+ | { type: 'void'; id: string; error?: string }
193
+
194
+ // Tree operations
195
+ | { type: 'dirListing'; id: string; entries?: DirEntry[]; error?: string }
196
+ | { type: 'streamChunk'; id: string; chunk: Uint8Array; done: boolean }
197
+
198
+ // Nostr events
199
+ | { type: 'event'; subId: string; event: SignedEvent }
200
+ | { type: 'eose'; subId: string }
201
+
202
+ // Stats
203
+ | { type: 'peerStats'; id: string; stats: PeerStats[] }
204
+ | { type: 'relayStats'; id: string; stats: RelayStats[] }
205
+ | { type: 'storageStats'; id: string; items: number; bytes: number }
206
+
207
+ // Blossom notifications
208
+ | { type: 'blossomBandwidth'; stats: BlossomBandwidthStats }
209
+ | { type: 'blossomUploadError'; hash: string; error: string }
210
+ | { type: 'blossomUploadProgress'; progress: BlossomUploadProgress }
211
+ | { type: 'blossomPushResult'; id: string; pushed: number; skipped: number; failed: number; error?: string; errors?: string[] }
212
+
213
+ // Republish result
214
+ | { type: 'republishResult'; id: string; count: number; error?: string; encryptionErrors?: string[] }
215
+
216
+ // Background Blossom push progress (for automatic pushes)
217
+ | { type: 'blossomPushStarted'; treeName: string; totalChunks: number }
218
+ | { type: 'blossomPushProgress'; treeName: string; current: number; total: number }
219
+ | { type: 'blossomPushComplete'; treeName: string; pushed: number; skipped: number; failed: number }
220
+
221
+ // Tree root updates (worker → main thread notification)
222
+ | { type: 'treeRootUpdate'; npub: string; treeName: string; hash: Uint8Array; key?: Uint8Array; visibility: TreeVisibility; labels?: string[]; updatedAt: number; snapshotNhash?: string; encryptedKey?: string; keyId?: string; selfEncryptedKey?: string; selfEncryptedLinkKey?: string }
223
+ | { type: 'treeRootInfo'; id: string; record?: TreeRootInfo; error?: string }
224
+
225
+ // SocialGraph responses
226
+ | { type: 'socialGraphInit'; id: string; version: number; size: number; error?: string }
227
+ | { type: 'socialGraphVersion'; version: number }
228
+ | { type: 'followDistance'; id: string; distance: number; error?: string }
229
+ | { type: 'isFollowingResult'; id: string; result: boolean; error?: string }
230
+ | { type: 'pubkeyList'; id: string; pubkeys: string[]; error?: string }
231
+ | { type: 'socialGraphSize'; id: string; size: number; error?: string }
232
+
233
+ // NIP-07 requests (worker → main thread, needs extension)
234
+ | { type: 'signEvent'; id: string; event: UnsignedEvent }
235
+ | { type: 'nip44Encrypt'; id: string; pubkey: string; plaintext: string }
236
+ | { type: 'nip44Decrypt'; id: string; pubkey: string; ciphertext: string }
237
+
238
+ // WebRTC proxy commands (worker tells main thread what to do)
239
+ | WebRTCCommand;
240
+
241
+ export interface DirEntry {
242
+ name: string;
243
+ isDir: boolean;
244
+ size?: number;
245
+ cid?: CID;
246
+ }
247
+
248
+ export interface RelayStats {
249
+ url: string;
250
+ connected: boolean;
251
+ eventsReceived: number;
252
+ eventsSent: number;
253
+ }
254
+
255
+ /** Per-server upload status */
256
+ export interface BlossomServerStatus {
257
+ url: string;
258
+ uploaded: number; // Number of chunks uploaded to this server
259
+ failed: number; // Number of chunks failed on this server
260
+ skipped: number; // Number of chunks already existed on this server
261
+ }
262
+
263
+ /** Overall blossom upload progress */
264
+ export interface BlossomUploadProgress {
265
+ sessionId: string; // Unique session identifier
266
+ totalChunks: number; // Total chunks to upload
267
+ processedChunks: number; // Chunks processed (uploaded + skipped + failed)
268
+ servers: BlossomServerStatus[]; // Per-server status
269
+ }
270
+
271
+ // ============================================================================
272
+ // Service Worker ↔ Worker Messages (via MessagePort)
273
+ // ============================================================================
274
+
275
+ // Request by direct CID (for cached/known content)
276
+ export interface MediaRequestByCid {
277
+ type: 'media';
278
+ requestId: string;
279
+ cid: string; // hex encoded CID hash
280
+ start: number;
281
+ end?: number;
282
+ mimeType?: string;
283
+ }
284
+
285
+ // Request by npub/path (supports live streaming via tree root updates)
286
+ export interface MediaRequestByPath {
287
+ type: 'mediaByPath';
288
+ requestId: string;
289
+ npub: string;
290
+ path: string; // e.g., "public/video.webm"
291
+ start: number;
292
+ end?: number;
293
+ mimeType?: string;
294
+ }
295
+
296
+ export type MediaRequest = MediaRequestByCid | MediaRequestByPath;
297
+
298
+ export type MediaResponse =
299
+ | { type: 'headers'; requestId: string; totalSize: number; mimeType: string; isLive?: boolean }
300
+ | { type: 'chunk'; requestId: string; data: Uint8Array }
301
+ | { type: 'done'; requestId: string }
302
+ | { type: 'error'; requestId: string; message: string };
303
+
304
+ // ============================================================================
305
+ // WebRTC Proxy Protocol (Worker ↔ Main Thread)
306
+ // Worker controls logic, main thread owns RTCPeerConnection
307
+ // ============================================================================
308
+
309
+ /** Worker → Main: Commands to control WebRTC connections */
310
+ export type WebRTCCommand =
311
+ // Connection lifecycle
312
+ | { type: 'rtc:createPeer'; peerId: string; pubkey: string }
313
+ | { type: 'rtc:closePeer'; peerId: string }
314
+
315
+ // SDP handling
316
+ | { type: 'rtc:createOffer'; peerId: string }
317
+ | { type: 'rtc:createAnswer'; peerId: string }
318
+ | { type: 'rtc:setLocalDescription'; peerId: string; sdp: RTCSessionDescriptionInit }
319
+ | { type: 'rtc:setRemoteDescription'; peerId: string; sdp: RTCSessionDescriptionInit }
320
+
321
+ // ICE handling
322
+ | { type: 'rtc:addIceCandidate'; peerId: string; candidate: RTCIceCandidateInit }
323
+
324
+ // Data channel
325
+ | { type: 'rtc:sendData'; peerId: string; data: Uint8Array };
326
+
327
+ /** Main → Worker: Events from WebRTC connections */
328
+ export type WebRTCEvent =
329
+ // Connection state
330
+ | { type: 'rtc:peerCreated'; peerId: string }
331
+ | { type: 'rtc:peerStateChange'; peerId: string; state: RTCPeerConnectionState }
332
+ | { type: 'rtc:peerClosed'; peerId: string }
333
+
334
+ // SDP results
335
+ | { type: 'rtc:offerCreated'; peerId: string; sdp: RTCSessionDescriptionInit }
336
+ | { type: 'rtc:answerCreated'; peerId: string; sdp: RTCSessionDescriptionInit }
337
+ | { type: 'rtc:descriptionSet'; peerId: string; error?: string }
338
+
339
+ // ICE events
340
+ | { type: 'rtc:iceCandidate'; peerId: string; candidate: RTCIceCandidateInit | null }
341
+ | { type: 'rtc:iceGatheringComplete'; peerId: string }
342
+
343
+ // Data channel
344
+ | { type: 'rtc:dataChannelOpen'; peerId: string }
345
+ | { type: 'rtc:dataChannelMessage'; peerId: string; data: Uint8Array }
346
+ | { type: 'rtc:dataChannelClose'; peerId: string }
347
+ | { type: 'rtc:dataChannelError'; peerId: string; error: string }
348
+
349
+ // Backpressure signals (main thread buffer state)
350
+ | { type: 'rtc:bufferHigh'; peerId: string }
351
+ | { type: 'rtc:bufferLow'; peerId: string };
352
+
353
+ // ============================================================================
354
+ // Helper functions
355
+ // ============================================================================
356
+
357
+ let requestIdCounter = 0;
358
+
359
+ export function generateRequestId(): string {
360
+ return `req_${Date.now()}_${++requestIdCounter}`;
361
+ }
@@ -0,0 +1,25 @@
1
+ export interface ResolveWorkerPublicAssetUrlOptions {
2
+ importMetaUrl: string;
3
+ origin: string;
4
+ }
5
+
6
+ export function resolveWorkerPublicAssetUrl(
7
+ baseUrl: string | undefined,
8
+ assetPath: string,
9
+ options: ResolveWorkerPublicAssetUrlOptions,
10
+ ): string {
11
+ const normalizedAssetPath = assetPath.replace(/^\/+/, '');
12
+ const normalizedBaseUrl = typeof baseUrl === 'string' && baseUrl.trim() ? baseUrl.trim() : '/';
13
+
14
+ if (/^https?:\/\//i.test(normalizedBaseUrl)) {
15
+ return new URL(normalizedAssetPath, normalizedBaseUrl).toString();
16
+ }
17
+
18
+ if (normalizedBaseUrl === '.' || normalizedBaseUrl === './') {
19
+ return new URL(`../${normalizedAssetPath}`, options.importMetaUrl).toString();
20
+ }
21
+
22
+ const rootedBaseUrl = normalizedBaseUrl.startsWith('/') ? normalizedBaseUrl : `/${normalizedBaseUrl}`;
23
+ const basePath = rootedBaseUrl.endsWith('/') ? rootedBaseUrl : `${rootedBaseUrl}/`;
24
+ return new URL(`${basePath}${normalizedAssetPath}`, options.origin).toString();
25
+ }
@@ -0,0 +1,50 @@
1
+ import type { CID, HashTree } from '@hashtree/core';
2
+ import { resolveTreeRootNow } from './treeRootSubscription';
3
+
4
+ export const DEFAULT_ROOT_PATH_RESOLVE_TIMEOUT_MS = 15_000;
5
+
6
+ export interface ParsedRootPath {
7
+ treeName: string;
8
+ subPath: string[];
9
+ }
10
+
11
+ export function parseRootPath(path?: string): ParsedRootPath {
12
+ const pathParts = path?.split('/').filter(Boolean) ?? [];
13
+ return {
14
+ treeName: pathParts[0] || 'public',
15
+ subPath: pathParts.slice(1),
16
+ };
17
+ }
18
+
19
+ export async function resolveRootPath(
20
+ tree: Pick<HashTree, 'resolvePath'> | null,
21
+ npub: string,
22
+ path?: string,
23
+ timeoutMs: number = DEFAULT_ROOT_PATH_RESOLVE_TIMEOUT_MS,
24
+ ): Promise<CID | null> {
25
+ const exactTreeName = path?.split('/').filter(Boolean).join('/') || 'public';
26
+ const exactRootCid = await resolveTreeRootNow(npub, exactTreeName, timeoutMs);
27
+ if (exactRootCid) {
28
+ return exactRootCid;
29
+ }
30
+
31
+ const { treeName, subPath } = parseRootPath(path);
32
+ if (subPath.length === 0) {
33
+ return null;
34
+ }
35
+
36
+ const rootCid = await resolveTreeRootNow(npub, treeName, timeoutMs);
37
+ if (!rootCid) {
38
+ return null;
39
+ }
40
+
41
+ if (subPath.length === 0) {
42
+ return rootCid;
43
+ }
44
+
45
+ if (!tree) {
46
+ throw new Error('Tree not initialized');
47
+ }
48
+
49
+ return (await tree.resolvePath(rootCid, subPath))?.cid ?? null;
50
+ }
@@ -0,0 +1,17 @@
1
+ declare module 'ndk' {
2
+ const NDK: any;
3
+ export default NDK;
4
+ export const NDKEvent: any;
5
+ export const NDKPrivateKeySigner: any;
6
+ export type NDKFilter = any;
7
+ }
8
+
9
+ declare module 'ndk-cache' {
10
+ const NDKCacheAdapterDexie: any;
11
+ export default NDKCacheAdapterDexie;
12
+ }
13
+
14
+ declare module 'nostr-social-graph' {
15
+ export const SocialGraph: any;
16
+ export type NostrEvent = any;
17
+ }