@freqhole/midden 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # midden
2
+
3
+ browser WASM client for freqhole P2P federation.
4
+
5
+ ## what it does
6
+
7
+ midden provides a `MiddenNode` class that lets browsers connect to freqhole peers over iroh P2P. just need the peer's node_id (public key) to connect - iroh handles discovery via relay.
8
+
9
+ ## building
10
+
11
+ requires wasm-pack and LLVM toolchain:
12
+
13
+ ```bash
14
+ # install wasm-pack
15
+ cargo install wasm-pack
16
+
17
+ # on macOS, ensure LLVM is installed
18
+ brew install llvm
19
+
20
+ # build (dev mode)
21
+ make build
22
+
23
+ # build (release mode, optimized)
24
+ make build-release
25
+ ```
26
+
27
+ output goes to `pkg/` directory.
28
+
29
+ ## usage
30
+
31
+ ```typescript
32
+ import { MiddenNode } from "midden";
33
+
34
+ // create node (waits for relay connection)
35
+ const node = await MiddenNode.create();
36
+ console.log("my node_id:", node.node_id());
37
+
38
+ // make API request to peer - accepts plain node_id or full endpoint JSON
39
+ const response = await node.proxy_request(
40
+ peerNodeId, // e.g. "abc123def456..." or '{"id":"...","addrs":[...]}'
41
+ "GET",
42
+ "/api/music/songs?limit=10",
43
+ null,
44
+ );
45
+ console.log(response.status, response.body);
46
+
47
+ // fetch blob from peer
48
+ const blob = await node.fetch_blob(peerNodeId, blobId);
49
+ console.log(blob.size(), blob.content_type());
50
+ // blob.data() returns Uint8Array
51
+ ```
52
+
53
+ ### peer address formats
54
+
55
+ midden accepts two formats for `peer_addr`:
56
+
57
+ 1. **plain node_id** (64 hex chars): uses iroh relay for discovery
58
+
59
+ ```
60
+ 13a257b5367d6b5b7ceb67ec6246c3dafbe886af8ed429408cd7619c7a4787b1
61
+ ```
62
+
63
+ 2. **full endpoint JSON**: includes relay URL and/or direct IP hints
64
+ ```json
65
+ {
66
+ "id": "13a257b5...",
67
+ "addrs": [{ "Relay": "https://..." }, { "Ip": "192.168.1.100:57383" }]
68
+ }
69
+ ```
70
+
71
+ ## protocol
72
+
73
+ uses same protocol as grimoire's federation transport:
74
+
75
+ - ALPN: `freqhole/1`
76
+ - messages: `ProxyRequest`, `ProxyResponse`, `BlobStreamRequest`, `BlobStreamResponse`
77
+ - blob streaming: length-prefixed header followed by raw bytes
78
+
79
+ see `grimoire/src/federation/transport/protocol.rs` for details.
package/midden.d.ts ADDED
@@ -0,0 +1,388 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /**
4
+ * The `ReadableStreamType` enum.
5
+ *
6
+ * *This API requires the following crate features to be activated: `ReadableStreamType`*
7
+ */
8
+
9
+ type ReadableStreamType = "bytes";
10
+
11
+ /**
12
+ * a bidirectional QUIC stream for length-delimited message exchange.
13
+ *
14
+ * wraps an iroh (SendStream, RecvStream) pair. messages are framed with
15
+ * a 4-byte big-endian u32 length prefix, matching `LengthDelimitedCodec`
16
+ * from tokio-util.
17
+ *
18
+ * the send and recv halves use RefCell<Option<...>> so that async read
19
+ * and write operations can proceed concurrently (safe because WASM is
20
+ * single-threaded).
21
+ */
22
+ export class BiStream {
23
+ private constructor();
24
+ free(): void;
25
+ [Symbol.dispose](): void;
26
+ /**
27
+ * the ALPN protocol this stream was established on.
28
+ */
29
+ alpn(): string;
30
+ /**
31
+ * close the stream.
32
+ *
33
+ * finishes the send half and drops both halves.
34
+ */
35
+ close(): void;
36
+ /**
37
+ * the remote peer's node ID (iroh public key as hex string).
38
+ */
39
+ peer_node_id(): string;
40
+ /**
41
+ * read a newline-terminated utf-8 line.
42
+ *
43
+ * returns the line WITHOUT the trailing `\n`. returns null on clean
44
+ * stream close (EOF before any bytes). used for ndjson framing.
45
+ */
46
+ read_line(): Promise<any>;
47
+ /**
48
+ * read a length-delimited message.
49
+ *
50
+ * reads a 4-byte big-endian u32 length prefix, then reads that many
51
+ * bytes of payload. returns the payload as a Uint8Array.
52
+ *
53
+ * returns null (JsValue::NULL) if the stream has been closed cleanly
54
+ * by the remote peer (EOF on the length prefix read).
55
+ */
56
+ read_message(): Promise<any>;
57
+ /**
58
+ * read all remaining bytes from the recv stream (no length prefix).
59
+ *
60
+ * reads until the remote peer finishes the stream or `max_size` bytes
61
+ * are read. this matches grimoire's `read_to_end()` framing where
62
+ * the message is terminated by the sender calling `finish()`.
63
+ */
64
+ read_to_end(max_size: number): Promise<any>;
65
+ /**
66
+ * write a newline-delimited utf-8 line.
67
+ *
68
+ * appends `\n` if not already present, then writes. used for the ndjson
69
+ * framing the `freqhole-events/1` protocol speaks.
70
+ */
71
+ write_line(line: string): Promise<void>;
72
+ /**
73
+ * write a length-delimited message.
74
+ *
75
+ * writes a 4-byte big-endian u32 length prefix followed by the payload.
76
+ * this matches the `LengthDelimitedCodec` framing used by the
77
+ * iroh-automerge-repo example.
78
+ */
79
+ write_message(data: Uint8Array): Promise<void>;
80
+ /**
81
+ * write raw bytes without a length prefix, then finish the send stream.
82
+ *
83
+ * this matches grimoire's `send_response()` framing where the message
84
+ * is terminated by calling `finish()` on the send stream. the receiver
85
+ * uses `read_to_end()` to read all bytes.
86
+ *
87
+ * after `finish()` we await `stopped()` so the peer's ack is observed
88
+ * before this method returns. without this, JS callers that drop /
89
+ * `close()` the stream immediately after `write_raw_and_finish` can
90
+ * race the QUIC flush -- the peer's `read_to_end` then errors with
91
+ * "connection lost" mid-payload because the in-flight frames are
92
+ * torn down with the connection. matters most for large payloads
93
+ * (e.g. base64-encoded blob bodies in `proxy_response`).
94
+ */
95
+ write_raw_and_finish(data: Uint8Array): Promise<void>;
96
+ }
97
+
98
+ /**
99
+ * result from fetching the server hello image from a peer
100
+ */
101
+ export class HelloImageResult {
102
+ private constructor();
103
+ free(): void;
104
+ [Symbol.dispose](): void;
105
+ readonly content_type: string | undefined;
106
+ readonly data: Uint8Array;
107
+ }
108
+
109
+ export class IntoUnderlyingByteSource {
110
+ private constructor();
111
+ free(): void;
112
+ [Symbol.dispose](): void;
113
+ cancel(): void;
114
+ pull(controller: ReadableByteStreamController): Promise<any>;
115
+ start(controller: ReadableByteStreamController): void;
116
+ readonly autoAllocateChunkSize: number;
117
+ readonly type: ReadableStreamType;
118
+ }
119
+
120
+ export class IntoUnderlyingSink {
121
+ private constructor();
122
+ free(): void;
123
+ [Symbol.dispose](): void;
124
+ abort(reason: any): Promise<any>;
125
+ close(): Promise<any>;
126
+ write(chunk: any): Promise<any>;
127
+ }
128
+
129
+ export class IntoUnderlyingSource {
130
+ private constructor();
131
+ free(): void;
132
+ [Symbol.dispose](): void;
133
+ cancel(): void;
134
+ pull(controller: ReadableStreamDefaultController): Promise<any>;
135
+ }
136
+
137
+ /**
138
+ * browser P2P node for freqhole federation
139
+ *
140
+ * supports two protocols:
141
+ * - freqhole/1: API proxying and small blob streaming
142
+ * - iroh-blobs: verified streaming for audio files
143
+ */
144
+ export class MiddenNode {
145
+ private constructor();
146
+ free(): void;
147
+ [Symbol.dispose](): void;
148
+ /**
149
+ * accept the next incoming connection and bidirectional stream.
150
+ *
151
+ * blocks until an incoming connection arrives on any registered ALPN.
152
+ * returns a BiStream with the peer's node ID and the negotiated ALPN.
153
+ *
154
+ * returns null (JsValue::NULL) if the endpoint has been closed.
155
+ *
156
+ * the caller should check `stream.alpn()` to route the connection
157
+ * to the appropriate handler.
158
+ */
159
+ accept(): Promise<any>;
160
+ /**
161
+ * return the number of blobs currently held in the store via active TempTags.
162
+ */
163
+ active_blob_count(): number;
164
+ /**
165
+ * compute blake3 hash for a blob on demand
166
+ *
167
+ * use this when the client doesn't have the blake3 hash yet (not in API response).
168
+ * the server will compute the hash, save it to the database, and add the file
169
+ * to FsStore for verified streaming.
170
+ *
171
+ * returns the blake3 hash (64 hex chars) if successful, null if blob not found.
172
+ */
173
+ compute_blake3(peer_addr: string, blob_id: string): Promise<string | undefined>;
174
+ /**
175
+ * create a new node with random identity
176
+ * waits for relay connection before returning
177
+ */
178
+ static create(): Promise<MiddenNode>;
179
+ /**
180
+ * create a node from existing secret key bytes (for persistence)
181
+ * key_bytes must be exactly 32 bytes
182
+ */
183
+ static create_from_key(key_bytes: Uint8Array): Promise<MiddenNode>;
184
+ /**
185
+ * create a node from existing secret key with additional ALPN protocols.
186
+ *
187
+ * `extra_alpns` is a JS array of strings (e.g. ["iroh/automerge-repo/1"]).
188
+ * the node always registers "freqhole/1" plus whatever extra ALPNs are given.
189
+ */
190
+ static create_with_alpns(key_bytes: Uint8Array, extra_alpns: Array<any>): Promise<MiddenNode>;
191
+ /**
192
+ * download a blob using iroh-blobs verified streaming
193
+ *
194
+ * this is the preferred method for audio files - provides:
195
+ * - verified streaming (each chunk is cryptographically verified)
196
+ * - resume support (can restart interrupted transfers)
197
+ * - efficient parallel chunk fetching
198
+ *
199
+ * peer_addr: plain node_id or full endpoint JSON
200
+ * blake3_hash: the blake3 hash of the blob (64 hex chars)
201
+ */
202
+ download_verified(peer_addr: string, blake3_hash: string): Promise<Uint8Array>;
203
+ /**
204
+ * download a blob by blob_id using verified streaming with on-demand blake3
205
+ *
206
+ * use this when the client doesn't have the blake3 hash yet (not in API response).
207
+ * computes blake3 on the server, then uses iroh-blobs verified streaming.
208
+ *
209
+ * returns (blob_data, blake3_hash) for caching the hash for future requests.
210
+ */
211
+ download_verified_by_id(peer_addr: string, blob_id: string): Promise<Array<any>>;
212
+ /**
213
+ * download a verified blob and stream chunks to JS via callback
214
+ *
215
+ * this is the preferred path for large blobs (audio files). instead of
216
+ * materializing the full blob in wasm linear memory (which fails around
217
+ * 32MB+ due to allocator pressure on a single contiguous Bytes), this:
218
+ *
219
+ * 1. downloads the blob into MemStore using the verified iroh-blobs path
220
+ * 2. opens a streaming reader and pulls chunks
221
+ * 3. delivers each chunk to the JS callback as a Uint8Array
222
+ *
223
+ * JS side accumulates chunks (e.g. into a Blob via array of BlobParts) and
224
+ * can release each chunk as it goes. wasm peak memory stays bounded by
225
+ * chunk_size + the original MemStore copy.
226
+ *
227
+ * callback signature: `on_chunk(chunk: Uint8Array, offset: u64) -> void`
228
+ * progress callback: `on_progress(fraction: f64) -> void`
229
+ *
230
+ * returns total bytes streamed.
231
+ */
232
+ download_verified_streaming(peer_addr: string, blake3_hash: string, total_size: number, on_chunk: Function, on_progress: Function): Promise<number>;
233
+ /**
234
+ * streaming download with auto ensure+retry. first attempts the streaming
235
+ * download; if the verified download fails (blob not in peer's store), calls
236
+ * ensure_blob to load it, then retries.
237
+ */
238
+ download_verified_streaming_with_ensure(peer_addr: string, blake3_hash: string, total_size: number, on_chunk: Function, on_progress: Function): Promise<number>;
239
+ /**
240
+ * download a blob using iroh-blobs with automatic ensure + retry
241
+ *
242
+ * tries download_verified first. if blob not in peer's FsStore,
243
+ * calls ensure_blob to load it, then retries.
244
+ */
245
+ download_verified_with_ensure(peer_addr: string, blake3_hash: string): Promise<Uint8Array>;
246
+ /**
247
+ * ensure a blob is loaded into the peer's FsStore by blake3 hash
248
+ *
249
+ * call this before retrying download_verified if the first attempt fails.
250
+ * the server will look up the file by blake3 hash and add it to FsStore.
251
+ *
252
+ * returns true if blob is now available, false if not found.
253
+ */
254
+ ensure_blob(peer_addr: string, blake3_hash: string): Promise<boolean>;
255
+ /**
256
+ * fetch server image from a peer (public, no auth required)
257
+ * used during "add remote" flow before user is authenticated
258
+ * peer_addr can be plain node_id or full endpoint JSON with relay/IP hints
259
+ */
260
+ fetch_hello_image(peer_addr: string): Promise<HelloImageResult>;
261
+ /**
262
+ * check whether a blob with the given blake3 hash is currently held in the MemStore
263
+ * via an active TempTag. avoids expensive OPFS read + bao recomputation when the
264
+ * blob is already loaded.
265
+ */
266
+ has_active_blob(blake3_hash: string): boolean;
267
+ /**
268
+ * import a blob from its pre-computed bao-encoded bytes, skipping the
269
+ * expensive bao tree computation. `blake3_hash` is the 64-char hex hash,
270
+ * `bao_data` is the bao-encoded bytes previously returned by
271
+ * `import_blob_and_export_bao`.
272
+ *
273
+ * uses `import_bao_bytes` (iroh-blobs internal API) to feed the pre-computed
274
+ * bao stream directly into the store, then creates a global TempTag via
275
+ * `Tags::temp_tag` to prevent GC.
276
+ */
277
+ import_bao(blake3_hash: string, bao_data: Uint8Array): Promise<string>;
278
+ /**
279
+ * import raw bytes into the iroh-blobs store, returning the blake3 hash.
280
+ * this makes the blob available for verified download by peers.
281
+ * the blob stays in the store as long as its TempTag is held in active_tags.
282
+ * call release_blob() to allow GC, or it will be evicted when the map exceeds 3 entries.
283
+ */
284
+ import_blob(data: Uint8Array): Promise<string>;
285
+ /**
286
+ * import raw bytes into the iroh-blobs store, returning both the blake3 hash
287
+ * AND the bao-encoded bytes. the bao bytes can be cached in OPFS and later
288
+ * fed to `import_bao` to skip the expensive bao tree recomputation on re-import.
289
+ *
290
+ * returns a JS object: `{ hash: string, bao: Uint8Array }`
291
+ */
292
+ import_blob_and_export_bao(data: Uint8Array): Promise<any>;
293
+ /**
294
+ * get our node_id (iroh public key)
295
+ */
296
+ node_id(): string;
297
+ /**
298
+ * open a bidirectional stream to a peer on a specific ALPN.
299
+ *
300
+ * `peer_addr` can be a plain node_id hex string or a full endpoint
301
+ * address JSON (same format as proxy_request). `alpn` is the protocol
302
+ * to negotiate (e.g. "iroh/automerge-repo/1").
303
+ *
304
+ * returns a BiStream for length-delimited message exchange.
305
+ */
306
+ open_bi(peer_addr: string, alpn: string): Promise<BiStream>;
307
+ /**
308
+ * dispatch a typed admin command to a peer over the freqhole-admin/1 ALPN.
309
+ *
310
+ * `args` is a JSON string (the literal `"null"` is accepted for no-payload
311
+ * commands). returns a JS object envelope `{ success, message, data, errors }`
312
+ * matching the wire format. validation of `data` against the per-command
313
+ * schema happens in the spume `AdminClient`.
314
+ */
315
+ proxy_admin(peer_addr: string, command: string, args: string): Promise<any>;
316
+ /**
317
+ * send an API request to a peer
318
+ * peer_addr can be plain node_id or full endpoint JSON with relay/IP hints
319
+ */
320
+ proxy_request(peer_addr: string, method: string, path: string, body?: string | null): Promise<any>;
321
+ /**
322
+ * release a blob's TempTag, allowing the store to garbage-collect it.
323
+ * blake3_hash should be the 64-char hex string returned by import_blob.
324
+ */
325
+ release_blob(blake3_hash: string): void;
326
+ /**
327
+ * get the secret key bytes for persistence (32 bytes)
328
+ * store this in IndexedDB to maintain the same identity across sessions
329
+ */
330
+ secret_key(): Uint8Array;
331
+ /**
332
+ * start a background accept loop that handles incoming iroh-blobs connections.
333
+ *
334
+ * call this once after creating the node to allow remote peers to pull blobs
335
+ * from this node (e.g., for P2P music upload where the server pulls from browser).
336
+ *
337
+ * only handles iroh-blobs connections — other ALPNs are ignored (dropped).
338
+ * safe to call multiple times (subsequent calls are no-ops).
339
+ *
340
+ * WARNING: if you also call `accept()` from JS, both loops will compete for
341
+ * incoming connections and each will only see a subset. use one or the other,
342
+ * not both. freqhole uses `start_blob_server()`, skein uses `accept()`.
343
+ *
344
+ * NOTE: no application-level peer auth is applied here. iroh-blobs transfers
345
+ * are content-addressed (blake3 verified), so a peer can only download blobs
346
+ * they already know the hash of. peer filtering can be added later if needed.
347
+ */
348
+ start_blob_server(): void;
349
+ /**
350
+ * connect to a freqhole radio broadcaster.
351
+ *
352
+ * callbacks (all called from JS land):
353
+ * - `on_hello(json: string)` — fires once when the server's Hello
354
+ * message arrives. payload is the JSON-encoded `HelloMessage`.
355
+ * - `on_meta(json: string)` — fires on each track change with the
356
+ * JSON-encoded `MetaMessage`.
357
+ * - `on_chunk(seq: number, is_init: boolean, bytes: Uint8Array)` —
358
+ * fires per audio chunk. `is_init = true` marks the start of a new
359
+ * track; the JS side should append it to the same SourceBuffer.
360
+ *
361
+ * returns a [`RadioHandle`] — keep a reference to it; dropping it stops
362
+ * playback and closes the iroh connection.
363
+ */
364
+ tune_radio(peer_addr: string, station_id: string | null | undefined, on_hello: Function, on_meta: Function, on_chunk: Function): Promise<RadioHandle>;
365
+ }
366
+
367
+ /**
368
+ * handle returned to JS for a tuned-in radio session. dropping the handle
369
+ * (or calling `leave()`) closes the iroh connection, which tears down both
370
+ * read loops.
371
+ */
372
+ export class RadioHandle {
373
+ private constructor();
374
+ free(): void;
375
+ [Symbol.dispose](): void;
376
+ /**
377
+ * stop receiving audio + meta and close the connection.
378
+ */
379
+ leave(): void;
380
+ }
381
+
382
+ /**
383
+ * compute the blake3 hash of the given bytes and return as a hex string.
384
+ * this runs entirely in the browser — no network call needed.
385
+ */
386
+ export function hash_blake3(data: Uint8Array): string;
387
+
388
+ export function start(): void;
package/midden.js ADDED
@@ -0,0 +1,9 @@
1
+ /* @ts-self-types="./midden.d.ts" */
2
+
3
+ import * as wasm from "./midden_bg.wasm";
4
+ import { __wbg_set_wasm } from "./midden_bg.js";
5
+ __wbg_set_wasm(wasm);
6
+ wasm.__wbindgen_start();
7
+ export {
8
+ BiStream, HelloImageResult, IntoUnderlyingByteSource, IntoUnderlyingSink, IntoUnderlyingSource, MiddenNode, RadioHandle, hash_blake3, start
9
+ } from "./midden_bg.js";